UNPKG

9.18 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { nullTranslator } from '@jupyterlab/translation';
4import { ReactiveToolbar, Spinner } from '@jupyterlab/ui-components';
5import { MessageLoop } from '@lumino/messaging';
6import { BoxLayout, BoxPanel, Widget } from '@lumino/widgets';
7import { DOMUtils } from './domutils';
8import { Printing } from './printing';
9/**
10 * A flag to indicate that event handlers are caught in the capture phase.
11 */
12const USE_CAPTURE = true;
13/**
14 * A widget meant to be contained in the JupyterLab main area.
15 *
16 * #### Notes
17 * Mirrors all of the `title` attributes of the content.
18 * This widget is `closable` by default.
19 * This widget is automatically disposed when closed.
20 * This widget ensures its own focus when activated.
21 */
22export class MainAreaWidget extends Widget {
23 /**
24 * Construct a new main area widget.
25 *
26 * @param options - The options for initializing the widget.
27 */
28 constructor(options) {
29 super(options);
30 this._changeGuard = false;
31 this._spinner = new Spinner();
32 this._isRevealed = false;
33 this._evtMouseDown = () => {
34 if (!this.node.contains(document.activeElement)) {
35 this._focusContent();
36 }
37 };
38 this.addClass('jp-MainAreaWidget');
39 // Set contain=strict to avoid many forced layout rendering while adding cells.
40 // Don't forget to remove the CSS class when your remove the spinner to allow
41 // the content to be rendered.
42 // @see https://github.com/jupyterlab/jupyterlab/issues/9381
43 this.addClass('jp-MainAreaWidget-ContainStrict');
44 this.id = DOMUtils.createDomID();
45 const trans = (options.translator || nullTranslator).load('jupyterlab');
46 const content = (this._content = options.content);
47 content.node.setAttribute('role', 'region');
48 content.node.setAttribute('aria-label', trans.__('notebook content'));
49 const toolbar = (this._toolbar = options.toolbar || new ReactiveToolbar());
50 toolbar.node.setAttribute('role', 'toolbar');
51 toolbar.node.setAttribute('aria-label', trans.__('notebook actions'));
52 const contentHeader = (this._contentHeader =
53 options.contentHeader ||
54 new BoxPanel({
55 direction: 'top-to-bottom',
56 spacing: 0
57 }));
58 const layout = (this.layout = new BoxLayout({ spacing: 0 }));
59 layout.direction = 'top-to-bottom';
60 BoxLayout.setStretch(toolbar, 0);
61 BoxLayout.setStretch(contentHeader, 0);
62 BoxLayout.setStretch(content, 1);
63 layout.addWidget(toolbar);
64 layout.addWidget(contentHeader);
65 layout.addWidget(content);
66 if (!content.id) {
67 content.id = DOMUtils.createDomID();
68 }
69 content.node.tabIndex = -1;
70 this._updateTitle();
71 content.title.changed.connect(this._updateTitle, this);
72 this.title.closable = true;
73 this.title.changed.connect(this._updateContentTitle, this);
74 if (options.reveal) {
75 this.node.appendChild(this._spinner.node);
76 this._revealed = options.reveal
77 .then(() => {
78 if (content.isDisposed) {
79 this.dispose();
80 return;
81 }
82 content.disposed.connect(() => this.dispose());
83 const active = document.activeElement === this._spinner.node;
84 this._disposeSpinner();
85 this._isRevealed = true;
86 if (active) {
87 this._focusContent();
88 }
89 })
90 .catch(e => {
91 // Show a revealed promise error.
92 const error = new Widget();
93 error.addClass('jp-MainAreaWidget-error');
94 // Show the error to the user.
95 const pre = document.createElement('pre');
96 pre.textContent = String(e);
97 error.node.appendChild(pre);
98 BoxLayout.setStretch(error, 1);
99 this._disposeSpinner();
100 content.dispose();
101 this._content = null;
102 toolbar.dispose();
103 this._toolbar = null;
104 layout.addWidget(error);
105 this._isRevealed = true;
106 throw error;
107 });
108 }
109 else {
110 // Handle no reveal promise.
111 this._spinner.dispose();
112 this.removeClass('jp-MainAreaWidget-ContainStrict');
113 content.disposed.connect(() => this.dispose());
114 this._isRevealed = true;
115 this._revealed = Promise.resolve(undefined);
116 }
117 }
118 /**
119 * Print method. Deferred to content.
120 */
121 [Printing.symbol]() {
122 if (!this._content) {
123 return null;
124 }
125 return Printing.getPrintFunction(this._content);
126 }
127 /**
128 * The content hosted by the widget.
129 */
130 get content() {
131 return this._content;
132 }
133 /**
134 * The toolbar hosted by the widget.
135 */
136 get toolbar() {
137 return this._toolbar;
138 }
139 /**
140 * A panel for widgets that sit between the toolbar and the content.
141 * Imagine a formatting toolbar, notification headers, etc.
142 */
143 get contentHeader() {
144 return this._contentHeader;
145 }
146 /**
147 * Whether the content widget or an error is revealed.
148 */
149 get isRevealed() {
150 return this._isRevealed;
151 }
152 /**
153 * A promise that resolves when the widget is revealed.
154 */
155 get revealed() {
156 return this._revealed;
157 }
158 /**
159 * Handle `'activate-request'` messages.
160 */
161 onActivateRequest(msg) {
162 if (this._isRevealed) {
163 this._focusContent();
164 }
165 else {
166 this._spinner.node.focus();
167 }
168 }
169 /**
170 * Handle `after-attach` messages for the widget.
171 */
172 onAfterAttach(msg) {
173 super.onAfterAttach(msg);
174 // Focus content in capture phase to ensure relevant commands operate on the
175 // current main area widget.
176 // Add the event listener directly instead of using `handleEvent` in order
177 // to save sub-classes from needing to reason about calling it as well.
178 this.node.addEventListener('mousedown', this._evtMouseDown, USE_CAPTURE);
179 }
180 /**
181 * Handle `before-detach` messages for the widget.
182 */
183 onBeforeDetach(msg) {
184 this.node.removeEventListener('mousedown', this._evtMouseDown, USE_CAPTURE);
185 super.onBeforeDetach(msg);
186 }
187 /**
188 * Handle `'close-request'` messages.
189 */
190 onCloseRequest(msg) {
191 this.dispose();
192 }
193 /**
194 * Handle `'update-request'` messages by forwarding them to the content.
195 */
196 onUpdateRequest(msg) {
197 if (this._content) {
198 MessageLoop.sendMessage(this._content, msg);
199 }
200 }
201 _disposeSpinner() {
202 this.node.removeChild(this._spinner.node);
203 this._spinner.dispose();
204 this.removeClass('jp-MainAreaWidget-ContainStrict');
205 }
206 /**
207 * Update the title based on the attributes of the child widget.
208 */
209 _updateTitle() {
210 if (this._changeGuard || !this.content) {
211 return;
212 }
213 this._changeGuard = true;
214 const content = this.content;
215 this.title.label = content.title.label;
216 this.title.mnemonic = content.title.mnemonic;
217 this.title.icon = content.title.icon;
218 this.title.iconClass = content.title.iconClass;
219 this.title.iconLabel = content.title.iconLabel;
220 this.title.caption = content.title.caption;
221 this.title.className = content.title.className;
222 this.title.dataset = content.title.dataset;
223 this._changeGuard = false;
224 }
225 /**
226 * Update the content title based on attributes of the main widget.
227 */
228 _updateContentTitle() {
229 if (this._changeGuard || !this.content) {
230 return;
231 }
232 this._changeGuard = true;
233 const content = this.content;
234 content.title.label = this.title.label;
235 content.title.mnemonic = this.title.mnemonic;
236 content.title.icon = this.title.icon;
237 content.title.iconClass = this.title.iconClass;
238 content.title.iconLabel = this.title.iconLabel;
239 content.title.caption = this.title.caption;
240 content.title.className = this.title.className;
241 content.title.dataset = this.title.dataset;
242 this._changeGuard = false;
243 }
244 /**
245 * Give focus to the content.
246 */
247 _focusContent() {
248 if (!this.content) {
249 return;
250 }
251 // Focus the content node if we aren't already focused on it or a
252 // descendent.
253 if (!this.content.node.contains(document.activeElement)) {
254 this.content.node.focus();
255 }
256 // Activate the content asynchronously (which may change the focus).
257 this.content.activate();
258 }
259}
260//# sourceMappingURL=mainareawidget.js.map
\No newline at end of file