mainareawidget.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Message } from '@phosphor/messaging';
  4. import { BoxLayout, Widget } from '@phosphor/widgets';
  5. import { Spinner } from './spinner';
  6. import { Toolbar } from './toolbar';
  7. import { DOMUtils } from './domutils';
  8. import { printSymbol, deferPrinting } from './printing';
  9. /**
  10. * A widget meant to be contained in the JupyterLab main area.
  11. *
  12. * #### Notes
  13. * Mirrors all of the `title` attributes of the content.
  14. * This widget is `closable` by default.
  15. * This widget is automatically disposed when closed.
  16. * This widget ensures its own focus when activated.
  17. */
  18. export class MainAreaWidget<T extends Widget = Widget> extends Widget {
  19. /**
  20. * Construct a new main area widget.
  21. *
  22. * @param options - The options for initializing the widget.
  23. */
  24. constructor(options: MainAreaWidget.IOptions<T>) {
  25. super(options);
  26. this.addClass('jp-MainAreaWidget');
  27. this.id = DOMUtils.createDomID();
  28. const content = (this._content = options.content);
  29. const toolbar = (this._toolbar = options.toolbar || new Toolbar());
  30. const spinner = this._spinner;
  31. const layout = (this.layout = new BoxLayout({ spacing: 0 }));
  32. layout.direction = 'top-to-bottom';
  33. BoxLayout.setStretch(toolbar, 0);
  34. BoxLayout.setStretch(content, 1);
  35. layout.addWidget(toolbar);
  36. layout.addWidget(content);
  37. deferPrinting(this, content);
  38. if (!content.id) {
  39. content.id = DOMUtils.createDomID();
  40. }
  41. content.node.tabIndex = -1;
  42. this._updateTitle();
  43. content.title.changed.connect(
  44. this._updateTitle,
  45. this
  46. );
  47. this.title.closable = true;
  48. this.title.changed.connect(
  49. this._updateContentTitle,
  50. this
  51. );
  52. if (options.reveal) {
  53. this.node.appendChild(spinner.node);
  54. this._revealed = options.reveal
  55. .then(() => {
  56. if (content.isDisposed) {
  57. this.dispose();
  58. return;
  59. }
  60. content.disposed.connect(() => this.dispose());
  61. const active = document.activeElement === spinner.node;
  62. this.node.removeChild(spinner.node);
  63. spinner.dispose();
  64. this._isRevealed = true;
  65. if (active) {
  66. this._focusContent();
  67. }
  68. })
  69. .catch(e => {
  70. // Show a revealed promise error.
  71. const error = new Widget();
  72. // Show the error to the user.
  73. const pre = document.createElement('pre');
  74. pre.textContent = String(e);
  75. error.node.appendChild(pre);
  76. BoxLayout.setStretch(error, 1);
  77. this.node.removeChild(spinner.node);
  78. spinner.dispose();
  79. content.dispose();
  80. this._content = null;
  81. toolbar.dispose();
  82. this._toolbar = null;
  83. layout.addWidget(error);
  84. this._isRevealed = true;
  85. throw error;
  86. });
  87. } else {
  88. // Handle no reveal promise.
  89. spinner.dispose();
  90. content.disposed.connect(() => this.dispose());
  91. this._isRevealed = true;
  92. this._revealed = Promise.resolve(undefined);
  93. }
  94. }
  95. /**
  96. * Print method. Defered to content.
  97. */
  98. [printSymbol]: () => void;
  99. /**
  100. * The content hosted by the widget.
  101. */
  102. get content(): T {
  103. return this._content;
  104. }
  105. /**
  106. * The toolbar hosted by the widget.
  107. */
  108. get toolbar(): Toolbar {
  109. return this._toolbar;
  110. }
  111. /**
  112. * Whether the content widget or an error is revealed.
  113. */
  114. get isRevealed(): boolean {
  115. return this._isRevealed;
  116. }
  117. /**
  118. * A promise that resolves when the widget is revealed.
  119. */
  120. get revealed(): Promise<void> {
  121. return this._revealed;
  122. }
  123. /**
  124. * Handle `'activate-request'` messages.
  125. */
  126. protected onActivateRequest(msg: Message): void {
  127. if (this._isRevealed) {
  128. if (this._content) {
  129. this._focusContent();
  130. }
  131. } else {
  132. this._spinner.node.focus();
  133. }
  134. }
  135. /**
  136. * Handle `'close-request'` messages.
  137. */
  138. protected onCloseRequest(msg: Message): void {
  139. this.dispose();
  140. }
  141. /**
  142. * Update the title based on the attributes of the child widget.
  143. */
  144. private _updateTitle(): void {
  145. if (this._changeGuard) {
  146. return;
  147. }
  148. this._changeGuard = true;
  149. const content = this.content;
  150. this.title.label = content.title.label;
  151. this.title.mnemonic = content.title.mnemonic;
  152. this.title.iconClass = content.title.iconClass;
  153. this.title.iconLabel = content.title.iconLabel;
  154. this.title.caption = content.title.caption;
  155. this.title.className = content.title.className;
  156. this.title.dataset = content.title.dataset;
  157. this._changeGuard = false;
  158. }
  159. /**
  160. * Update the content title based on attributes of the main widget.
  161. */
  162. private _updateContentTitle(): void {
  163. if (this._changeGuard) {
  164. return;
  165. }
  166. this._changeGuard = true;
  167. const content = this.content;
  168. content.title.label = this.title.label;
  169. content.title.mnemonic = this.title.mnemonic;
  170. content.title.iconClass = this.title.iconClass;
  171. content.title.iconLabel = this.title.iconLabel;
  172. content.title.caption = this.title.caption;
  173. content.title.className = this.title.className;
  174. content.title.dataset = this.title.dataset;
  175. this._changeGuard = false;
  176. }
  177. /**
  178. * Give focus to the content.
  179. */
  180. private _focusContent(): void {
  181. // Focus the content node if we aren't already focused on it or a
  182. // descendent.
  183. if (!this.content.node.contains(document.activeElement)) {
  184. this.content.node.focus();
  185. }
  186. // Activate the content asynchronously (which may change the focus).
  187. this.content.activate();
  188. }
  189. private _content: T;
  190. private _toolbar: Toolbar;
  191. private _changeGuard = false;
  192. private _spinner = new Spinner();
  193. private _isRevealed = false;
  194. private _revealed: Promise<void>;
  195. }
  196. /**
  197. * The namespace for the `MainAreaWidget` class statics.
  198. */
  199. export namespace MainAreaWidget {
  200. /**
  201. * An options object for creating a main area widget.
  202. */
  203. export interface IOptions<T extends Widget = Widget> extends Widget.IOptions {
  204. /**
  205. * The child widget to wrap.
  206. */
  207. content: T;
  208. /**
  209. * The toolbar to use for the widget. Defaults to an empty toolbar.
  210. */
  211. toolbar?: Toolbar;
  212. /**
  213. * An optional promise for when the content is ready to be revealed.
  214. */
  215. reveal?: Promise<any>;
  216. }
  217. /**
  218. * An options object for main area widget subclasses providing their own
  219. * default content.
  220. *
  221. * #### Notes
  222. * This makes it easier to have a subclass that provides its own default
  223. * content. This can go away once we upgrade to TypeScript 2.8 and have an
  224. * easy way to make a single property optional, ala
  225. * https://stackoverflow.com/a/46941824
  226. */
  227. export interface IOptionsOptionalContent<T extends Widget = Widget>
  228. extends Widget.IOptions {
  229. /**
  230. * The child widget to wrap.
  231. */
  232. content?: T;
  233. /**
  234. * The toolbar to use for the widget. Defaults to an empty toolbar.
  235. */
  236. toolbar?: Toolbar;
  237. /**
  238. * An optional promise for when the content is ready to be revealed.
  239. */
  240. reveal?: Promise<any>;
  241. }
  242. }