123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import { Message } from '@phosphor/messaging';
- import { BoxLayout, Widget } from '@phosphor/widgets';
- import { Spinner } from './spinner';
- import { Toolbar } from './toolbar';
- import { DOMUtils } from './domutils';
- import { printSymbol, deferPrinting } from './printing';
- /**
- * A widget meant to be contained in the JupyterLab main area.
- *
- * #### Notes
- * Mirrors all of the `title` attributes of the content.
- * This widget is `closable` by default.
- * This widget is automatically disposed when closed.
- * This widget ensures its own focus when activated.
- */
- export class MainAreaWidget<T extends Widget = Widget> extends Widget {
- /**
- * Construct a new main area widget.
- *
- * @param options - The options for initializing the widget.
- */
- constructor(options: MainAreaWidget.IOptions<T>) {
- super(options);
- this.addClass('jp-MainAreaWidget');
- this.id = DOMUtils.createDomID();
- const content = (this._content = options.content);
- const toolbar = (this._toolbar = options.toolbar || new Toolbar());
- const spinner = this._spinner;
- const layout = (this.layout = new BoxLayout({ spacing: 0 }));
- layout.direction = 'top-to-bottom';
- BoxLayout.setStretch(toolbar, 0);
- BoxLayout.setStretch(content, 1);
- layout.addWidget(toolbar);
- layout.addWidget(content);
- deferPrinting(this, content);
- if (!content.id) {
- content.id = DOMUtils.createDomID();
- }
- content.node.tabIndex = -1;
- this._updateTitle();
- content.title.changed.connect(
- this._updateTitle,
- this
- );
- this.title.closable = true;
- this.title.changed.connect(
- this._updateContentTitle,
- this
- );
- if (options.reveal) {
- this.node.appendChild(spinner.node);
- this._revealed = options.reveal
- .then(() => {
- if (content.isDisposed) {
- this.dispose();
- return;
- }
- content.disposed.connect(() => this.dispose());
- const active = document.activeElement === spinner.node;
- this.node.removeChild(spinner.node);
- spinner.dispose();
- this._isRevealed = true;
- if (active) {
- this._focusContent();
- }
- })
- .catch(e => {
- // Show a revealed promise error.
- const error = new Widget();
- // Show the error to the user.
- const pre = document.createElement('pre');
- pre.textContent = String(e);
- error.node.appendChild(pre);
- BoxLayout.setStretch(error, 1);
- this.node.removeChild(spinner.node);
- spinner.dispose();
- content.dispose();
- this._content = null;
- toolbar.dispose();
- this._toolbar = null;
- layout.addWidget(error);
- this._isRevealed = true;
- throw error;
- });
- } else {
- // Handle no reveal promise.
- spinner.dispose();
- content.disposed.connect(() => this.dispose());
- this._isRevealed = true;
- this._revealed = Promise.resolve(undefined);
- }
- }
- /**
- * Print method. Defered to content.
- */
- [printSymbol]: () => void;
- /**
- * The content hosted by the widget.
- */
- get content(): T {
- return this._content;
- }
- /**
- * The toolbar hosted by the widget.
- */
- get toolbar(): Toolbar {
- return this._toolbar;
- }
- /**
- * Whether the content widget or an error is revealed.
- */
- get isRevealed(): boolean {
- return this._isRevealed;
- }
- /**
- * A promise that resolves when the widget is revealed.
- */
- get revealed(): Promise<void> {
- return this._revealed;
- }
- /**
- * Handle `'activate-request'` messages.
- */
- protected onActivateRequest(msg: Message): void {
- if (this._isRevealed) {
- if (this._content) {
- this._focusContent();
- }
- } else {
- this._spinner.node.focus();
- }
- }
- /**
- * Handle `'close-request'` messages.
- */
- protected onCloseRequest(msg: Message): void {
- this.dispose();
- }
- /**
- * Update the title based on the attributes of the child widget.
- */
- private _updateTitle(): void {
- if (this._changeGuard) {
- return;
- }
- this._changeGuard = true;
- const content = this.content;
- this.title.label = content.title.label;
- this.title.mnemonic = content.title.mnemonic;
- this.title.iconClass = content.title.iconClass;
- this.title.iconLabel = content.title.iconLabel;
- this.title.caption = content.title.caption;
- this.title.className = content.title.className;
- this.title.dataset = content.title.dataset;
- this._changeGuard = false;
- }
- /**
- * Update the content title based on attributes of the main widget.
- */
- private _updateContentTitle(): void {
- if (this._changeGuard) {
- return;
- }
- this._changeGuard = true;
- const content = this.content;
- content.title.label = this.title.label;
- content.title.mnemonic = this.title.mnemonic;
- content.title.iconClass = this.title.iconClass;
- content.title.iconLabel = this.title.iconLabel;
- content.title.caption = this.title.caption;
- content.title.className = this.title.className;
- content.title.dataset = this.title.dataset;
- this._changeGuard = false;
- }
- /**
- * Give focus to the content.
- */
- private _focusContent(): void {
- // Focus the content node if we aren't already focused on it or a
- // descendent.
- if (!this.content.node.contains(document.activeElement)) {
- this.content.node.focus();
- }
- // Activate the content asynchronously (which may change the focus).
- this.content.activate();
- }
- private _content: T;
- private _toolbar: Toolbar;
- private _changeGuard = false;
- private _spinner = new Spinner();
- private _isRevealed = false;
- private _revealed: Promise<void>;
- }
- /**
- * The namespace for the `MainAreaWidget` class statics.
- */
- export namespace MainAreaWidget {
- /**
- * An options object for creating a main area widget.
- */
- export interface IOptions<T extends Widget = Widget> extends Widget.IOptions {
- /**
- * The child widget to wrap.
- */
- content: T;
- /**
- * The toolbar to use for the widget. Defaults to an empty toolbar.
- */
- toolbar?: Toolbar;
- /**
- * An optional promise for when the content is ready to be revealed.
- */
- reveal?: Promise<any>;
- }
- /**
- * An options object for main area widget subclasses providing their own
- * default content.
- *
- * #### Notes
- * This makes it easier to have a subclass that provides its own default
- * content. This can go away once we upgrade to TypeScript 2.8 and have an
- * easy way to make a single property optional, ala
- * https://stackoverflow.com/a/46941824
- */
- export interface IOptionsOptionalContent<T extends Widget = Widget>
- extends Widget.IOptions {
- /**
- * The child widget to wrap.
- */
- content?: T;
- /**
- * The toolbar to use for the widget. Defaults to an empty toolbar.
- */
- toolbar?: Toolbar;
- /**
- * An optional promise for when the content is ready to be revealed.
- */
- reveal?: Promise<any>;
- }
- }
|