diff --git a/docs/scripts/graph-dependencies.js b/docs/scripts/graph-dependencies.js index d3bc738d0b51..4da89eae4e5f 100644 --- a/docs/scripts/graph-dependencies.js +++ b/docs/scripts/graph-dependencies.js @@ -38,6 +38,11 @@ packages.forEach(function(packagePath) { return; } + // Don't include the metapackage. + if (data.name === '@jupyterlab/metapackage') { + return; + } + // Construct a URL to the package on GitHub. var Url = url.resolve(baseUrl, 'packages/' + path.basename(packagePath)); diff --git a/docs/source/developer/dependency-graph.svg b/docs/source/developer/dependency-graph.svg index 7855ee7f2fc7..7fd04ab6d46e 100644 --- a/docs/source/developer/dependency-graph.svg +++ b/docs/source/developer/dependency-graph.svg @@ -4,17 +4,17 @@ - - + + G - + application - -application + +application @@ -22,38 +22,38 @@ docregistry - -docregistry + +docregistry application->docregistry - - + + rendermime - -rendermime + +rendermime docregistry->rendermime - - + + apputils - -apputils + +apputils @@ -61,397 +61,442 @@ services - -services + +services apputils->services - - + + coreutils - -coreutils + +coreutils services->coreutils - - + + observables - -observables + +observables services->observables - - + + rendermime-interfaces - -rendermime-interfaces + +rendermime-interfaces rendermime->rendermime-interfaces - - + + codemirror - -codemirror + +codemirror rendermime->codemirror - - + + - - -codemirror->apputils - - - - - -codeeditor - - -codeeditor + + +statusbar + + +statusbar - - -codemirror->codeeditor - - + + +codemirror->statusbar + + attachments - -attachments + +attachments attachments->rendermime - - + + cells - -cells + +cells cells->attachments - - + + outputarea - -outputarea + +outputarea cells->outputarea - - + + - + outputarea->rendermime - - + + + + + +codeeditor + + +codeeditor + + codeeditor->coreutils - - + + codeeditor->observables - - + + + + + +statusbar->apputils + + + + + +statusbar->codeeditor + + - + completer - - -completer + + +completer - + completer->apputils - - + + - + completer->codeeditor - - + + - + console - - -console + + +console - + console->cells - - + + - + csvviewer - - -csvviewer + + +csvviewer - + csvviewer->docregistry - - + + - + docmanager - - -docmanager + + +docmanager - + docmanager->docregistry - - + + + + + +extensionmanager + + +extensionmanager + + + + + +extensionmanager->apputils + + - + filebrowser - - -filebrowser + + +filebrowser - + filebrowser->docmanager - - + + - + fileeditor - - -fileeditor + + +fileeditor - + fileeditor->docregistry - - + + - + imageviewer - - -imageviewer + + +imageviewer - + imageviewer->docregistry - - + + - + inspector - - -inspector + + +inspector - + inspector->rendermime - - + + - + launcher - - -launcher + + +launcher - + launcher->apputils - - + + - + mainmenu - - -mainmenu + + +mainmenu - + mainmenu->apputils - - + + + + + +mathjax2 + + +mathjax2 + + + + + +mathjax2->rendermime-interfaces + + - + notebook - - -notebook + + +notebook - + notebook->docregistry - - + + - + notebook->cells - - + + - + running - - -running + + +running - + running->apputils - - + + - + settingeditor - - -settingeditor + + +settingeditor - + settingeditor->inspector - - + + - + terminal - - -terminal + + +terminal - + terminal->apputils - - + + - + tooltip - - -tooltip + + +tooltip - + tooltip->rendermime - - + + diff --git a/packages/codemirror-extension/src/index.ts b/packages/codemirror-extension/src/index.ts index dda0ae660ef2..bef42de31ae5 100644 --- a/packages/codemirror-extension/src/index.ts +++ b/packages/codemirror-extension/src/index.ts @@ -83,14 +83,18 @@ export const editorSyntaxStatus: JupyterLabPlugin = { >).content.editor; } }); - statusBar.registerStatusItem('editor-syntax-item', item, { - align: 'left', - rank: 0, - isActive: () => - app.shell.currentWidget && - tracker.currentWidget && - app.shell.currentWidget === tracker.currentWidget - }); + statusBar.registerStatusItem( + '@jupyterlab/codemirror-extension:editor-syntax-status', + { + item, + align: 'left', + rank: 0, + isActive: () => + app.shell.currentWidget && + tracker.currentWidget && + app.shell.currentWidget === tracker.currentWidget + } + ); } }; diff --git a/packages/csvviewer-extension/package.json b/packages/csvviewer-extension/package.json index 667f594d5451..39534bf42152 100644 --- a/packages/csvviewer-extension/package.json +++ b/packages/csvviewer-extension/package.json @@ -35,8 +35,7 @@ "@jupyterlab/csvviewer": "^0.19.1", "@jupyterlab/docregistry": "^0.19.1", "@jupyterlab/mainmenu": "^0.8.1", - "@phosphor/datagrid": "^0.1.6", - "@phosphor/widgets": "^1.6.0" + "@phosphor/datagrid": "^0.1.6" }, "devDependencies": { "rimraf": "~2.6.2", diff --git a/packages/docmanager-extension/src/index.ts b/packages/docmanager-extension/src/index.ts index aa250362acf1..15c2e4b78b1a 100644 --- a/packages/docmanager-extension/src/index.ts +++ b/packages/docmanager-extension/src/index.ts @@ -179,13 +179,17 @@ export const savingStatusPlugin: JupyterLabPlugin = { () => (item.model!.widget = app.shell.currentWidget) ); - statusBar.registerStatusItem('saving-status', item, { - align: 'middle', - isActive: () => { - return true; - }, - stateChanged: item.model!.stateChanged - }); + statusBar.registerStatusItem( + '@jupyterlab.docmanager-extension:saving-status', + { + item, + align: 'middle', + isActive: () => { + return true; + }, + activeStateChanged: item.model!.stateChanged + } + ); } }; @@ -209,13 +213,17 @@ export const pathStatusPlugin: JupyterLabPlugin = { item.model!.widget = app.shell.currentWidget; }); - statusBar.registerStatusItem('path-status', item, { - align: 'right', - rank: 0, - isActive: () => { - return true; + statusBar.registerStatusItem( + '@jupyterlab/docmanager-extension:path-status', + { + item, + align: 'right', + rank: 0, + isActive: () => { + return true; + } } - }); + ); } }; diff --git a/packages/filebrowser-extension/package.json b/packages/filebrowser-extension/package.json index 0b402b4a5e08..28b99b187b6f 100644 --- a/packages/filebrowser-extension/package.json +++ b/packages/filebrowser-extension/package.json @@ -41,8 +41,7 @@ "@phosphor/algorithm": "^1.1.2", "@phosphor/commands": "^1.6.1", "@phosphor/messaging": "^1.2.2", - "@phosphor/widgets": "^1.6.0", - "react": "~16.4.2" + "@phosphor/widgets": "^1.6.0" }, "devDependencies": { "rimraf": "~2.6.2", diff --git a/packages/filebrowser-extension/src/index.ts b/packages/filebrowser-extension/src/index.ts index 15fee01879ad..34c482e852bc 100644 --- a/packages/filebrowser-extension/src/index.ts +++ b/packages/filebrowser-extension/src/index.ts @@ -22,15 +22,16 @@ import { IDocumentManager } from '@jupyterlab/docmanager'; import { FileBrowserModel, FileBrowser, + FileUploadStatus, IFileBrowserFactory } from '@jupyterlab/filebrowser'; -import { fileUploadStatus } from './uploadstatus'; - import { Launcher } from '@jupyterlab/launcher'; import { Contents } from '@jupyterlab/services'; +import { IStatusBar } from '@jupyterlab/statusbar'; + import { IIterator, map, reduce, toArray } from '@phosphor/algorithm'; import { CommandRegistry } from '@phosphor/commands'; @@ -125,6 +126,36 @@ const shareFile: JupyterLabPlugin = { autoStart: true }; +/** + * A plugin providing file upload status. + */ +export const fileUploadStatus: JupyterLabPlugin = { + id: '@jupyterlab/filebrowser-extension:file-upload-status', + autoStart: true, + requires: [IStatusBar, IFileBrowserFactory], + activate: ( + app: JupyterLab, + statusBar: IStatusBar, + browser: IFileBrowserFactory + ) => { + const item = new FileUploadStatus({ + tracker: browser.tracker + }); + + statusBar.registerStatusItem( + '@jupyterlab/filebrowser-extension:file-upload-status', + { + item, + align: 'middle', + isActive: () => { + return !!item.model && item.model.items.length > 0; + }, + activeStateChanged: item.model.stateChanged + } + ); + } +}; + /** * The file browser namespace token. */ diff --git a/packages/filebrowser/package.json b/packages/filebrowser/package.json index 48c6419df9a7..720105427d78 100644 --- a/packages/filebrowser/package.json +++ b/packages/filebrowser/package.json @@ -36,6 +36,7 @@ "@jupyterlab/docmanager": "^0.19.1", "@jupyterlab/docregistry": "^0.19.1", "@jupyterlab/services": "^3.2.1", + "@jupyterlab/statusbar": "^0.7.1", "@phosphor/algorithm": "^1.1.2", "@phosphor/commands": "^1.6.1", "@phosphor/coreutils": "^1.3.0", @@ -44,7 +45,8 @@ "@phosphor/dragdrop": "^1.3.0", "@phosphor/messaging": "^1.2.2", "@phosphor/signaling": "^1.2.2", - "@phosphor/widgets": "^1.6.0" + "@phosphor/widgets": "^1.6.0", + "react": "~16.4.2" }, "devDependencies": { "rimraf": "~2.6.2", diff --git a/packages/filebrowser/src/index.ts b/packages/filebrowser/src/index.ts index 56126312642f..135873305c05 100644 --- a/packages/filebrowser/src/index.ts +++ b/packages/filebrowser/src/index.ts @@ -9,3 +9,4 @@ export * from './factory'; export * from './listing'; export * from './model'; export * from './upload'; +export * from './uploadstatus'; diff --git a/packages/filebrowser-extension/src/uploadstatus.tsx b/packages/filebrowser/src/uploadstatus.tsx similarity index 81% rename from packages/filebrowser-extension/src/uploadstatus.tsx rename to packages/filebrowser/src/uploadstatus.tsx index 852701302679..54250e297304 100644 --- a/packages/filebrowser-extension/src/uploadstatus.tsx +++ b/packages/filebrowser/src/uploadstatus.tsx @@ -1,21 +1,18 @@ -import React from 'react'; -import { TextItem } from '@jupyterlab/statusbar'; - -import { JupyterLabPlugin, JupyterLab } from '@jupyterlab/application'; +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +// -import { - IUploadModel, - FileBrowserModel, - IFileBrowserFactory, - FileBrowser -} from '@jupyterlab/filebrowser'; +import { VDomRenderer, InstanceTracker, VDomModel } from '@jupyterlab/apputils'; import { IChangedArgs } from '@jupyterlab/coreutils'; -import { ProgressBar } from '@jupyterlab/statusbar'; -import { VDomRenderer, InstanceTracker, VDomModel } from '@jupyterlab/apputils'; +import { GroupItem, ProgressBar, TextItem } from '@jupyterlab/statusbar'; + import { ArrayExt } from '@phosphor/algorithm'; -import { IStatusBar, GroupItem } from '@jupyterlab/statusbar'; + +import { IUploadModel, FileBrowserModel, FileBrowser } from '.'; + +import React from 'react'; /** * Half-spacing between items in the overall status item. @@ -49,7 +46,7 @@ namespace FileUploadComponent { */ export interface IProps { /** - * The current upload fraction. + * The current upload percentage, from 0 to 100. */ upload: number; } @@ -63,16 +60,16 @@ const UPLOAD_COMPLETE_MESSAGE_MILLIS: number = 2000; /** * Status bar item to display file upload progress. */ -class FileUpload extends VDomRenderer { +export class FileUploadStatus extends VDomRenderer { /** * Construct a new FileUpload status item. */ - constructor(opts: FileUpload.IOptions) { + constructor(opts: FileUploadStatus.IOptions) { super(); this._tracker = opts.tracker; this._tracker.currentChanged.connect(this._onBrowserChange); - this.model = new FileUpload.Model( + this.model = new FileUploadStatus.Model( this._tracker.currentWidget && this._tracker.currentWidget.model ); } @@ -117,7 +114,7 @@ class FileUpload extends VDomRenderer { /** * A namespace for FileUpload class statics. */ -namespace FileUpload { +export namespace FileUploadStatus { /** * The VDomModel for the FileUpload renderer. */ @@ -238,29 +235,3 @@ interface IFileUploadItem { */ complete: boolean; } - -/** - * A plugin providing file upload status. - */ -export const fileUploadStatus: JupyterLabPlugin = { - id: '@jupyterlab/filebrowser-extension:file-upload-item', - autoStart: true, - requires: [IStatusBar, IFileBrowserFactory], - activate: ( - app: JupyterLab, - statusBar: IStatusBar, - browser: IFileBrowserFactory - ) => { - const item = new FileUpload({ - tracker: browser.tracker - }); - - statusBar.registerStatusItem('file-upload-item', item, { - align: 'middle', - isActive: () => { - return !!item.model && item.model.items.length > 0; - }, - stateChanged: item.model.stateChanged - }); - } -}; diff --git a/packages/filebrowser/tsconfig.json b/packages/filebrowser/tsconfig.json index dfe6d4e782d2..cb06e46e40c7 100644 --- a/packages/filebrowser/tsconfig.json +++ b/packages/filebrowser/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "../services" + }, + { + "path": "../statusbar" } ] } diff --git a/packages/fileeditor-extension/src/index.ts b/packages/fileeditor-extension/src/index.ts index 982f7e0267c2..2a4647f0f070 100644 --- a/packages/fileeditor-extension/src/index.ts +++ b/packages/fileeditor-extension/src/index.ts @@ -111,7 +111,7 @@ const plugin: JupyterLabPlugin = { * switch tabs vs spaces and tab widths for text editors. */ export const tabSpaceStatus: JupyterLabPlugin = { - id: '@jupyterlab/fileeditor-extension:tab-space-item', + id: '@jupyterlab/fileeditor-extension:tab-space-status', autoStart: true, requires: [IStatusBar, IEditorTracker, ISettingRegistry], activate: ( @@ -161,15 +161,20 @@ export const tabSpaceStatus: JupyterLabPlugin = { }); // Add the status item. - statusBar.registerStatusItem('tab-space-item', item, { - align: 'right', - rank: 1, - isActive: () => { - return ( - app.shell.currentWidget && editorTracker.has(app.shell.currentWidget) - ); + statusBar.registerStatusItem( + '@jupyterlab/fileeditor-extension:tab-space-status', + { + item, + align: 'right', + rank: 1, + isActive: () => { + return ( + app.shell.currentWidget && + editorTracker.has(app.shell.currentWidget) + ); + } } - }); + ); } }; diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index c7678c0d65bb..311719022767 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -302,10 +302,11 @@ export const commandEditItem: JupyterLabPlugin = { // Keep the status item up-to-date with the current notebook. tracker.currentChanged.connect(() => { const current = tracker.currentWidget; - item.model.notebook = current.content; + item.model.notebook = current && current.content; }); - statusBar.registerStatusItem('command-edit-item', item, { + statusBar.registerStatusItem('@jupyterlab/notebook-extension:mode-status', { + item, align: 'right', rank: 4, isActive: () => @@ -333,17 +334,21 @@ export const notebookTrustItem: JupyterLabPlugin = { // Keep the status item up-to-date with the current notebook. tracker.currentChanged.connect(() => { const current = tracker.currentWidget; - item.model.notebook = current.content; + item.model.notebook = current && current.content; }); - statusBar.registerStatusItem('notebook-trust-item', item, { - align: 'right', - rank: 3, - isActive: () => - app.shell.currentWidget && - tracker.currentWidget && - app.shell.currentWidget === tracker.currentWidget - }); + statusBar.registerStatusItem( + '@jupyterlab/notebook-extension:trust-status', + { + item, + align: 'right', + rank: 3, + isActive: () => + app.shell.currentWidget && + tracker.currentWidget && + app.shell.currentWidget === tracker.currentWidget + } + ); } }; diff --git a/packages/statusbar-extension/src/index.ts b/packages/statusbar-extension/src/index.ts index 67ccb9513e9d..2761097462bc 100644 --- a/packages/statusbar-extension/src/index.ts +++ b/packages/statusbar-extension/src/index.ts @@ -63,7 +63,7 @@ const statusBar: JupyterLabPlugin = { * A plugin that provides a kernel status item to the status bar. */ export const kernelStatus: JupyterLabPlugin = { - id: '@jupyterlab/statusbar:kernel-status', + id: '@jupyterlab/statusbar-extension:kernel-status', autoStart: true, requires: [IStatusBar, INotebookTracker, IConsoleTracker], activate: ( @@ -117,17 +117,21 @@ export const kernelStatus: JupyterLabPlugin = { item.model!.session = currentSession; }); - statusBar.registerStatusItem('kernel-status-item', item, { - align: 'left', - rank: 1, - isActive: () => { - const current = app.shell.currentWidget; - return ( - current && - (notebookTracker.has(current) || consoleTracker.has(current)) - ); + statusBar.registerStatusItem( + '@jupyterlab/statusbar-extension:kernel-status', + { + item, + align: 'left', + rank: 1, + isActive: () => { + const current = app.shell.currentWidget; + return ( + current && + (notebookTracker.has(current) || consoleTracker.has(current)) + ); + } } - }); + ); } }; @@ -135,7 +139,7 @@ export const kernelStatus: JupyterLabPlugin = { * A plugin providing a line/column status item to the application. */ export const lineColItem: JupyterLabPlugin = { - id: '@jupyterlab/statusbar:line-col-item', + id: '@jupyterlab/statusbar-extension:line-col-status', autoStart: true, requires: [IStatusBar, INotebookTracker, IEditorTracker, IConsoleTracker], activate: ( @@ -193,19 +197,23 @@ export const lineColItem: JupyterLabPlugin = { }); // Add the status item to the status bar. - statusBar.registerStatusItem('line-col-item', item, { - align: 'right', - rank: 2, - isActive: () => { - const current = app.shell.currentWidget; - return ( - current && - (notebookTracker.has(current) || - editorTracker.has(current) || - consoleTracker.has(current)) - ); + statusBar.registerStatusItem( + '@jupyterlab/statusbar-extension:line-col-status', + { + item, + align: 'right', + rank: 2, + isActive: () => { + const current = app.shell.currentWidget; + return ( + current && + (notebookTracker.has(current) || + editorTracker.has(current) || + consoleTracker.has(current)) + ); + } } - }); + ); } }; @@ -217,18 +225,22 @@ export const lineColItem: JupyterLabPlugin = { * is installed. */ export const memoryUsageItem: JupyterLabPlugin = { - id: '@jupyterlab/statusbar:memory-usage-item', + id: '@jupyterlab/statusbar-extension:memory-usage-status', autoStart: true, requires: [IStatusBar], activate: (app: JupyterLab, statusBar: IStatusBar) => { let item = new MemoryUsage(); - statusBar.registerStatusItem('memory-usage-item', item, { - align: 'left', - rank: 2, - isActive: () => item.model!.metricsAvailable, - stateChanged: item.model!.stateChanged - }); + statusBar.registerStatusItem( + '@jupyterlab/statusbar-extension:memory-usage-status', + { + item, + align: 'left', + rank: 2, + isActive: () => item.model!.metricsAvailable, + activeStateChanged: item.model!.stateChanged + } + ); } }; @@ -237,7 +249,7 @@ export const memoryUsageItem: JupyterLabPlugin = { * to the status bar. */ export const runningSessionsItem: JupyterLabPlugin = { - id: '@jupyterlab/statusbar:running-sessions-item', + id: '@jupyterlab/statusbar-extension:running-sessions-status', autoStart: true, requires: [IStatusBar], activate: (app: JupyterLab, statusBar: IStatusBar) => { @@ -246,10 +258,14 @@ export const runningSessionsItem: JupyterLabPlugin = { serviceManager: app.serviceManager }); - statusBar.registerStatusItem('running-sessions-item', item, { - align: 'left', - rank: 0 - }); + statusBar.registerStatusItem( + '@jupyterlab/statusbar-extension:running-sessions-status', + { + item, + align: 'left', + rank: 0 + } + ); } }; diff --git a/packages/statusbar/src/components/group.tsx b/packages/statusbar/src/components/group.tsx index 0b34f01cb31f..3b8d87bacd30 100644 --- a/packages/statusbar/src/components/group.tsx +++ b/packages/statusbar/src/components/group.tsx @@ -1,19 +1,20 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -/** - * @ignore - */ import * as React from 'react'; + import { style, classes } from 'typestyle/lib'; + import { centeredFlex, leftToRight } from '../style/layout'; const groupItemLayout = style(centeredFlex, leftToRight); -// tslint:disable-next-line:variable-name -export const GroupItem = ( +/** + * A tsx component for a set of items logically grouped together. + */ +export function GroupItem( props: GroupItem.IProps & React.HTMLAttributes -): React.ReactElement => { +): React.ReactElement { const { spacing, children, className, ...rest } = props; const numChildren = React.Children.count(children); @@ -30,11 +31,24 @@ export const GroupItem = ( })} ); -}; +} +/** + * A namespace for GroupItem statics. + */ export namespace GroupItem { + /** + * Props for the GroupItem. + */ export interface IProps { + /** + * The spacing, in px, between the items in the goup. + */ spacing: number; + + /** + * The items to arrange in a group. + */ children: JSX.Element[]; } } diff --git a/packages/statusbar/src/components/hover.tsx b/packages/statusbar/src/components/hover.tsx index 31a696c3ae23..8fdeb1df2def 100644 --- a/packages/statusbar/src/components/hover.tsx +++ b/packages/statusbar/src/components/hover.tsx @@ -1,25 +1,40 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { Widget, PanelLayout } from '@phosphor/widgets'; import { HoverBox } from '@jupyterlab/apputils'; import { Message } from '@phosphor/messaging'; -import { clickedItem, interactiveItem } from '../style/statusbar'; + +import { Widget, PanelLayout } from '@phosphor/widgets'; import { style } from 'typestyle/lib'; +import { clickedItem, interactiveItem } from '../style/statusbar'; + const hoverItem = style({ boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)' }); -export function showPopup(options: Popup.IOptions): Popup | null { +/** + * Create and show a popup component. + * + * @param options - options for the popup + * + * @returns the popup that was created. + */ +export function showPopup(options: Popup.IOptions): Popup { let dialog = new Popup(options); dialog.launch(); return dialog; } +/** + * A class for a Popup widget. + */ export class Popup extends Widget { + /** + * Construct a new Popup. + */ constructor(options: Popup.IOptions) { super(); this._body = options.body; @@ -33,89 +48,62 @@ export class Popup extends Widget { }); } + /** + * Attach the popup widget to the page. + */ launch() { - this.setGeometry(); + this._setGeometry(); Widget.attach(this, document.body); this.update(); this._anchor.addClass(clickedItem); this._anchor.removeClass(interactiveItem); } - setGeometry() { - let aligned = 0; - const anchorRect = this._anchor.node.getBoundingClientRect(); - const bodyRect = this._body.node.getBoundingClientRect(); - if (this._align === 'right') { - aligned = -(bodyRect.width - anchorRect.width); - } - const style = window.getComputedStyle(this._body.node); - HoverBox.setGeometry({ - anchor: anchorRect, - host: document.body, - maxHeight: 500, - minHeight: 20, - node: this._body.node, - offset: { - horizontal: aligned - }, - privilege: 'forceAbove', - style - }); - } - + /** + * Handle `'update'` messages for the widget. + */ protected onUpdateRequest(msg: Message): void { - this.setGeometry(); - this.setGeometry(); + this._setGeometry(); super.onUpdateRequest(msg); } + /** + * Handle `'after-attach'` messages for the widget. + */ protected onAfterAttach(msg: Message): void { document.addEventListener('click', this, false); this.node.addEventListener('keypress', this, false); window.addEventListener('resize', this, false); } + /** + * Handle `'after-detach'` messages for the widget. + */ protected onAfterDetach(msg: Message): void { document.removeEventListener('click', this, false); this.node.removeEventListener('keypress', this, false); window.removeEventListener('resize', this, false); } - protected _evtClick(event: MouseEvent): void { - if ( - !!event.target && - !( - this._body.node.contains(event.target as HTMLElement) || - this._anchor.node.contains(event.target as HTMLElement) - ) - ) { - this.dispose(); - } - } - + /** + * Handle `'resize'` messages for the widget. + */ protected onResize(): void { this.update(); } + /** + * Dispose of the widget. + */ dispose() { super.dispose(); this._anchor.removeClass(clickedItem); this._anchor.addClass(interactiveItem); } - protected _evtKeydown(event: KeyboardEvent): void { - // Check for escape key - switch (event.keyCode) { - case 27: // Escape. - event.stopPropagation(); - event.preventDefault(); - this.dispose(); - break; - default: - break; - } - } - + /** + * Handle DOM events for the widget. + */ handleEvent(event: Event): void { switch (event.type) { case 'keydown': @@ -132,15 +120,80 @@ export class Popup extends Widget { } } + private _evtClick(event: MouseEvent): void { + if ( + !!event.target && + !( + this._body.node.contains(event.target as HTMLElement) || + this._anchor.node.contains(event.target as HTMLElement) + ) + ) { + this.dispose(); + } + } + + private _evtKeydown(event: KeyboardEvent): void { + // Check for escape key + switch (event.keyCode) { + case 27: // Escape. + event.stopPropagation(); + event.preventDefault(); + this.dispose(); + break; + default: + break; + } + } + + private _setGeometry(): void { + this._setGeometry(); + let aligned = 0; + const anchorRect = this._anchor.node.getBoundingClientRect(); + const bodyRect = this._body.node.getBoundingClientRect(); + if (this._align === 'right') { + aligned = -(bodyRect.width - anchorRect.width); + } + const style = window.getComputedStyle(this._body.node); + HoverBox.setGeometry({ + anchor: anchorRect, + host: document.body, + maxHeight: 500, + minHeight: 20, + node: this._body.node, + offset: { + horizontal: aligned + }, + privilege: 'forceAbove', + style + }); + } + private _body: Widget; private _anchor: Widget; private _align: 'left' | 'right' | undefined; } +/** + * A namespace for Popup statics. + */ export namespace Popup { + /** + * Options for creating a Popup widget. + */ export interface IOptions { + /** + * The content of the popup. + */ body: Widget; + + /** + * The widget to which we are attaching the popup. + */ anchor: Widget; + + /** + * Whether to align the popup to the left or the right of the anchor. + */ align?: 'left' | 'right'; } } diff --git a/packages/statusbar/src/components/icon.tsx b/packages/statusbar/src/components/icon.tsx index 9d1efa771dc8..d5450b584d72 100644 --- a/packages/statusbar/src/components/icon.tsx +++ b/packages/statusbar/src/components/icon.tsx @@ -3,22 +3,34 @@ import * as React from 'react'; -import icon from '../style/icon'; import { classes, style } from 'typestyle/lib'; +import icon from '../style/icon'; + +/** + * A namespace for IconItem statics. + */ export namespace IconItem { + /** + * Props for an IconItem + */ export interface IProps { + /** + * A CSS class name for the icon. + */ source: string; } } -// tslint:disable-next-line:variable-name -export const IconItem = ( +/** + * A functional tsx component for an icon. + */ +export function IconItem( props: IconItem.IProps & React.HTMLAttributes & { offset: { x: number; y: number }; } -): React.ReactElement => { +): React.ReactElement { const { source, className, offset, ...rest } = props; return (
); -}; +} diff --git a/packages/statusbar/src/components/progressBar.tsx b/packages/statusbar/src/components/progressBar.tsx index aef4793cd56b..6163c317680a 100644 --- a/packages/statusbar/src/components/progressBar.tsx +++ b/packages/statusbar/src/components/progressBar.tsx @@ -1,29 +1,55 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + import * as React from 'react'; + import { progressBarItem, fillerItem } from '../style/progressBar'; +/** + * A namespace for ProgressBar statics. + */ export namespace ProgressBar { + /** + * Props for the ProgressBar. + */ export interface IProps { + /** + * The current progress percentage, from 0 to 100 + */ percentage: number; } } -// tslint:disable-next-line:variable-name -export const ProgressBar = (props: ProgressBar.IProps) => { +/** + * A functional tsx component for a progress bar. + */ +export function ProgressBar(props: ProgressBar.IProps) { return (
); -}; +} -export namespace Filler { +/** + * A namespace for Filler statics. + */ +namespace Filler { + /** + * Props for the Filler component. + */ export interface IProps { + /** + * The current percentage filled, from 0 to 100 + */ percentage: number; } } -// tslint:disable-next-line:variable-name -export const Filler = (props: Filler.IProps) => { +/** + * A functional tsx component for a partially filled div. + */ +function Filler(props: Filler.IProps) { return (
{ }} /> ); -}; +} diff --git a/packages/statusbar/src/components/text.tsx b/packages/statusbar/src/components/text.tsx index dc998e20492b..d9065086a0ca 100644 --- a/packages/statusbar/src/components/text.tsx +++ b/packages/statusbar/src/components/text.tsx @@ -3,24 +3,40 @@ import * as React from 'react'; -import { textItem } from '../style/text'; import { classes } from 'typestyle/lib'; +import { textItem } from '../style/text'; + +/** + * A namespace for TextItem statics. + */ export namespace TextItem { + /** + * Props for a TextItem. + */ export interface IProps { - source: any; + /** + * The content of the text item. + */ + source: string | number; + + /** + * Hover text to give to the node. + */ title?: string; } } -// tslint:disable-next-line:variable-name -export const TextItem = ( +/** + * A functional tsx component for a text item. + */ +export function TextItem( props: TextItem.IProps & React.HTMLAttributes -): React.ReactElement => { +): React.ReactElement { const { title, source, className, ...rest } = props; return ( {source} ); -}; +} diff --git a/packages/statusbar/src/defaults/lineCol.tsx b/packages/statusbar/src/defaults/lineCol.tsx index 2cda5c9edf43..a8f251785e7e 100644 --- a/packages/statusbar/src/defaults/lineCol.tsx +++ b/packages/statusbar/src/defaults/lineCol.tsx @@ -303,7 +303,7 @@ export namespace LineCol { const oldState = this._getAllState(); this._editor = editor; - if (this._editor === null) { + if (!this._editor) { this._column = 1; this._line = 1; } else { diff --git a/packages/statusbar/src/statusbar.ts b/packages/statusbar/src/statusbar.ts index 90354e0cd57f..cc860aedc88d 100644 --- a/packages/statusbar/src/statusbar.ts +++ b/packages/statusbar/src/statusbar.ts @@ -7,7 +7,11 @@ import { ISignal } from '@phosphor/signaling'; import { Token } from '@phosphor/coreutils'; -import { DisposableDelegate, IDisposable } from '@phosphor/disposable'; +import { + DisposableDelegate, + DisposableSet, + IDisposable +} from '@phosphor/disposable'; import { Message } from '@phosphor/messaging'; @@ -35,17 +39,11 @@ export interface IStatusBar { * * @param id - a unique id for the status item. * - * @param widget - The item to add to the status bar. - * * @param options - The options for how to add the status item. * * @returns an `IDisposable` that can be disposed to remove the item. */ - registerStatusItem( - id: string, - widget: Widget, - options: IStatusBar.IItemOptions - ): IDisposable; + registerStatusItem(id: string, statusItem: IStatusBar.IItem): IDisposable; } /** @@ -57,28 +55,38 @@ export namespace IStatusBar { /** * Options for status bar items. */ - export interface IItemOptions { + export interface IItem { + /** + * The item to add to the status bar. + */ + item: Widget; + /** - * Which side to place widget. Permanent widgets are intended for the right and left side, with more transient widgets in the middle. + * Which side to place item. + * Permanent items are intended for the right and left side, + * with more transient items in the middle. */ align?: Alignment; + /** * Ordering of Items -- higher rank items are closer to the middle. */ rank?: number; + /** - * Whether the widget is shown or hidden. + * Whether the item is shown or hidden. */ isActive?: () => boolean; + /** - * Determine when the widget updates. + * A signal that is fired when the item active state changes. */ - stateChanged?: ISignal; + activeStateChanged?: ISignal; } } /** - * Main status bar object which contains all widgets. + * Main status bar object which contains all items. */ export class StatusBar extends Widget implements IStatusBar { constructor() { @@ -109,107 +117,80 @@ export class StatusBar extends Widget implements IStatusBar { * * @param id - a unique id for the status item. * - * @param widget - The item to add to the status bar. - * - * @param options - The options for how to add the status item. + * @param statusItem - The item to add to the status bar. */ - registerStatusItem( - id: string, - widget: Widget, - options: IStatusBar.IItemOptions = {} - ): IDisposable { + registerStatusItem(id: string, statusItem: IStatusBar.IItem): IDisposable { if (id in this._statusItems) { throw new Error(`Status item ${id} already registered.`); } - let align = options.align || 'left'; - let rank = options.rank || 0; - let isActive = options.isActive || (() => true); - let stateChanged = options.stateChanged || null; - let changeCallback = - options.stateChanged !== undefined - ? () => { - this._onIndividualStateChange(id); - } - : null; - - let wrapper = { - widget, - align, - rank, - isActive, - stateChanged, - changeCallback - }; + // Populate defaults for the optional properties of the status item. + statusItem = { ...Private.statusItemDefaults, ...statusItem }; + const { align, item, rank } = statusItem; - let rankItem = { - id, - rank + // Connect the activeStateChanged signal to refreshing the status item, + // if the signal was provided. + const onActiveStateChanged = () => { + this._refreshItem(id); }; + if (statusItem.activeStateChanged) { + statusItem.activeStateChanged.connect(onActiveStateChanged); + } - widget.addClass(itemStyle); + let rankItem = { id, rank }; - this._statusItems[id] = wrapper; - this._statusIds.push(id); - - if (stateChanged) { - stateChanged.connect(changeCallback!); - } + statusItem.item.addClass(itemStyle); + this._statusItems[id] = statusItem; if (align === 'left') { let insertIndex = this._findInsertIndex(this._leftRankItems, rankItem); if (insertIndex === -1) { - this._leftSide.addWidget(widget); + this._leftSide.addWidget(item); this._leftRankItems.push(rankItem); } else { ArrayExt.insert(this._leftRankItems, insertIndex, rankItem); - this._leftSide.insertWidget(insertIndex, widget); + this._leftSide.insertWidget(insertIndex, item); } } else if (align === 'right') { let insertIndex = this._findInsertIndex(this._rightRankItems, rankItem); if (insertIndex === -1) { - this._rightSide.addWidget(widget); + this._rightSide.addWidget(item); this._rightRankItems.push(rankItem); } else { ArrayExt.insert(this._rightRankItems, insertIndex, rankItem); - this._rightSide.insertWidget(insertIndex, widget); + this._rightSide.insertWidget(insertIndex, item); } } else { - this._middlePanel.addWidget(widget); + this._middlePanel.addWidget(item); } + this._refreshItem(id); // Initially refresh the status item. - return new DisposableDelegate(() => { + const disposable = new DisposableDelegate(() => { delete this._statusItems[id]; - ArrayExt.removeFirstOf(this._statusIds, id); - widget.parent = null; - widget.dispose(); + if (statusItem.activeStateChanged) { + statusItem.activeStateChanged.disconnect(onActiveStateChanged); + } + item.parent = null; + item.dispose(); }); + this._disposables.add(disposable); + return disposable; } /** * Dispose of the status bar. */ dispose() { + this._leftRankItems.length = 0; + this._rightRankItems.length = 0; + this._disposables.dispose(); super.dispose(); - this._statusIds.forEach(id => { - const { stateChanged, changeCallback, widget } = this._statusItems[id]; - - if (stateChanged) { - stateChanged.disconnect(changeCallback!); - } - - widget.dispose(); - }); } /** * Handle an 'update-request' message to the status bar. */ protected onUpdateRequest(msg: Message) { - this._statusIds.forEach(statusId => { - this._statusItems[statusId].widget.update(); - }); - this._refreshAll(); super.onUpdateRequest(msg); } @@ -221,52 +202,46 @@ export class StatusBar extends Widget implements IStatusBar { return ArrayExt.findFirstIndex(side, item => item.rank > newItem.rank); } - private _refreshItem({ isActive, widget }: StatusBar.IItem) { - if (isActive()) { - widget.show(); + private _refreshItem(id: string) { + const statusItem = this._statusItems[id]; + if (statusItem.isActive()) { + statusItem.item.show(); + statusItem.item.update(); } else { - widget.hide(); + statusItem.item.hide(); } } private _refreshAll(): void { - this._statusIds.forEach(statusId => { - this._refreshItem(this._statusItems[statusId]); + Object.keys(this._statusItems).forEach(id => { + this._refreshItem(id); }); } - private _onIndividualStateChange = (statusId: string) => { - this._refreshItem(this._statusItems[statusId]); - }; - private _leftRankItems: Private.IRankItem[] = []; private _rightRankItems: Private.IRankItem[] = []; - private _statusItems: { [id: string]: StatusBar.IItem } = Object.create(null); - private _statusIds: Array = []; - + private _statusItems: { [id: string]: IStatusBar.IItem } = {}; + private _disposables = new DisposableSet(); private _leftSide: Panel; private _middlePanel: Panel; private _rightSide: Panel; } -export namespace StatusBar { - /** - * The interface for a status bar item. - */ - export interface IItem { - align: IStatusBar.Alignment; - rank: number; - widget: Widget; - isActive: () => boolean; - stateChanged: ISignal | null; - changeCallback: (() => void) | null; - } -} - /** * A namespace for private functionality. */ namespace Private { + type Omit = Pick>; + /** + * Default options for a status item, less the item itself. + */ + export const statusItemDefaults: Omit = { + align: 'left', + rank: 0, + isActive: () => true, + activeStateChanged: undefined + }; + /** * An interface for storing the rank of a status item. */ diff --git a/tests/test-statusbar/jest.config.js b/tests/test-statusbar/jest.config.js new file mode 100644 index 000000000000..1cd5c9db5525 --- /dev/null +++ b/tests/test-statusbar/jest.config.js @@ -0,0 +1,2 @@ +const func = require('@jupyterlab/testutils/lib/jest-config'); +module.exports = func('statusbar', __dirname); diff --git a/tests/test-statusbar/package.json b/tests/test-statusbar/package.json new file mode 100644 index 000000000000..810bd5822e6c --- /dev/null +++ b/tests/test-statusbar/package.json @@ -0,0 +1,30 @@ +{ + "name": "@jupyterlab/test-statusbar", + "version": "0.5.1", + "private": true, + "scripts": { + "build": "tsc -b", + "clean": "rimraf build && rimraf coverage", + "coverage": "python run.py --coverage", + "test": "python run.py", + "watch": "python run.py --debug", + "watch:all": "python run.py --debug --watchAll", + "watch:src": "tsc -b --watch" + }, + "dependencies": { + "@jupyterlab/statusbar": "^0.7.1", + "@jupyterlab/testutils": "^0.3.1", + "@phosphor/signaling": "^1.2.2", + "@phosphor/widgets": "^1.6.0", + "chai": "~4.1.2", + "jest": "^23.5.0", + "ts-jest": "^23.1.4" + }, + "devDependencies": { + "@types/chai": "~4.0.10", + "@types/jest": "^23.3.1", + "puppeteer": "^1.5.0", + "rimraf": "~2.6.2", + "typescript": "~3.1.1" + } +} diff --git a/tests/test-statusbar/run.py b/tests/test-statusbar/run.py new file mode 100644 index 000000000000..b9bd9c302be4 --- /dev/null +++ b/tests/test-statusbar/run.py @@ -0,0 +1,8 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import os.path as osp +from jupyterlab.tests.test_app import run_jest + +if __name__ == '__main__': + run_jest(osp.dirname(osp.realpath(__file__))) diff --git a/tests/test-statusbar/src/statusbar.spec.ts b/tests/test-statusbar/src/statusbar.spec.ts new file mode 100644 index 000000000000..89fb36489a18 --- /dev/null +++ b/tests/test-statusbar/src/statusbar.spec.ts @@ -0,0 +1,151 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { expect } from 'chai'; + +import { Signal } from '@phosphor/signaling'; + +import { Widget } from '@phosphor/widgets'; + +import { StatusBar } from '@jupyterlab/statusbar/src'; + +describe('@jupyterlab/statusbar', () => { + describe('StatusBar', () => { + let statusBar: StatusBar; + + beforeEach(() => { + statusBar = new StatusBar(); + Widget.attach(statusBar, document.body); + }); + + afterEach(() => { + statusBar.parent = null; + statusBar.dispose(); + }); + + describe('#constructor()', () => { + it('should construct a new status bar', () => { + const statusBar = new StatusBar(); + expect(statusBar).to.be.an.instanceof(StatusBar); + }); + }); + + describe('#registerStatusItem', () => { + it('should add a new status item to the status bar', () => { + const item = new Widget(); + expect(item.isAttached).to.equal(false); + statusBar.registerStatusItem('item', { item }); + expect(item.isAttached).to.be.true; + expect(statusBar.contains(item)).to.be.true; + }); + + it('should raise an error if the same key is added twice', () => { + const item1 = new Widget(); + const item2 = new Widget(); + statusBar.registerStatusItem('item', { item: item1 }); + expect( + statusBar.registerStatusItem.bind(statusBar, 'item', { item: item2 }) + ).to.throw(); + }); + + it('should put higher rank left items closer to the middle', () => { + const item1 = new Widget(); + const item2 = new Widget(); + statusBar.registerStatusItem('item1', { + item: item1, + align: 'left', + rank: 1 + }); + statusBar.registerStatusItem('item2', { + item: item2, + align: 'left', + rank: 0 + }); + expect(item2.node.nextSibling).to.equal(item1.node); + }); + + it('should put higher rank right items closer to the middle', () => { + const item1 = new Widget(); + const item2 = new Widget(); + statusBar.registerStatusItem('item1', { + item: item1, + align: 'right', + rank: 0 + }); + statusBar.registerStatusItem('item2', { + item: item2, + align: 'right', + rank: 1 + }); + // Reverse order than what one might expect, as right-to-left + // is set in the styling of the right panel. + expect(item1.node.nextSibling).to.equal(item2.node); + }); + + it('should allow insertion of status items in the middle', () => { + const item = new Widget(); + statusBar.registerStatusItem('item', { + item: item, + align: 'middle' + }); + expect(item.isAttached).to.be.true; + }); + + it('should only show if isActive returns true', () => { + const item = new Widget(); + statusBar.registerStatusItem('item', { + item, + isActive: () => false + }); + expect(item.isHidden).to.be.true; + }); + + it('should update whether it is shown if activeStateChanged fires', () => { + const item = new Widget(); + let active = false; + const isActive = () => active; + const activeStateChanged = new Signal({}); + statusBar.registerStatusItem('item', { + item, + isActive, + activeStateChanged + }); + expect(item.isHidden).to.be.true; + active = true; + activeStateChanged.emit(void 0); + expect(item.isHidden).to.be.false; + }); + + it('should be removed from the status bar if disposed', () => { + const item = new Widget(); + const disposable = statusBar.registerStatusItem('item', { item }); + expect(item.isVisible).to.be.true; + disposable.dispose(); + expect(item.isVisible).to.be.false; + }); + }); + + describe('#dispose', () => { + it('should dispose of the status bar', () => { + expect(statusBar.isDisposed).to.be.false; + statusBar.dispose(); + expect(statusBar.isDisposed).to.be.true; + }); + + it('should be safe to call more than once', () => { + statusBar.dispose(); + expect(statusBar.isDisposed).to.be.true; + statusBar.dispose(); + expect(statusBar.isDisposed).to.be.true; + }); + + it('should dispose of the status items added to it', () => { + const item = new Widget(); + statusBar.registerStatusItem('item', { item }); + expect(item.isDisposed).to.be.false; + statusBar.dispose(); + expect(item.isDisposed).to.be.true; + }); + }); + }); +}); diff --git a/tests/test-statusbar/tsconfig.json b/tests/test-statusbar/tsconfig.json new file mode 100644 index 000000000000..443aebb80322 --- /dev/null +++ b/tests/test-statusbar/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfigbase", + "compilerOptions": { + "outDir": "build", + "types": ["jest"], + "composite": false, + "rootDir": "src" + }, + "include": ["src/*"], + "references": [ + { + "path": "../../packages/statusbar" + }, + { + "path": "../../testutils" + } + ] +}