UNPKG

13.7 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3/*-----------------------------------------------------------------------------
4| Copyright (c) 2014-2017, PhosphorJS Contributors
5|
6| Distributed under the terms of the BSD 3-Clause License.
7|
8| The full license is in the file LICENSE, distributed with this software.
9|----------------------------------------------------------------------------*/
10import { Platform } from '@lumino/domutils';
11
12import { MessageLoop } from '@lumino/messaging';
13
14import { ISignal, Signal } from '@lumino/signaling';
15
16import { BoxLayout } from './boxlayout';
17
18import { StackedPanel } from './stackedpanel';
19
20import { TabBar } from './tabbar';
21
22import { Widget } from './widget';
23
24/**
25 * A widget which combines a `TabBar` and a `StackedPanel`.
26 *
27 * #### Notes
28 * This is a simple panel which handles the common case of a tab bar
29 * placed next to a content area. The selected tab controls the widget
30 * which is shown in the content area.
31 *
32 * For use cases which require more control than is provided by this
33 * panel, the `TabBar` widget may be used independently.
34 */
35export class TabPanel extends Widget {
36 /**
37 * Construct a new tab panel.
38 *
39 * @param options - The options for initializing the tab panel.
40 */
41 constructor(options: TabPanel.IOptions = {}) {
42 super();
43 this.addClass('lm-TabPanel');
44 /* <DEPRECATED> */
45 this.addClass('p-TabPanel');
46 /* </DEPRECATED> */
47
48 // Create the tab bar and stacked panel.
49 this.tabBar = new TabBar<Widget>(options);
50 this.tabBar.addClass('lm-TabPanel-tabBar');
51 this.stackedPanel = new StackedPanel();
52 this.stackedPanel.addClass('lm-TabPanel-stackedPanel');
53 /* <DEPRECATED> */
54 this.tabBar.addClass('p-TabPanel-tabBar');
55 this.stackedPanel.addClass('p-TabPanel-stackedPanel');
56 /* </DEPRECATED> */
57
58 // Connect the tab bar signal handlers.
59 this.tabBar.tabMoved.connect(this._onTabMoved, this);
60 this.tabBar.currentChanged.connect(this._onCurrentChanged, this);
61 this.tabBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
62 this.tabBar.tabActivateRequested.connect(
63 this._onTabActivateRequested,
64 this
65 );
66 this.tabBar.addRequested.connect(this._onTabAddRequested, this);
67
68 // Connect the stacked panel signal handlers.
69 this.stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
70
71 // Get the data related to the placement.
72 this._tabPlacement = options.tabPlacement || 'top';
73 let direction = Private.directionFromPlacement(this._tabPlacement);
74 let orientation = Private.orientationFromPlacement(this._tabPlacement);
75
76 // Configure the tab bar for the placement.
77 this.tabBar.orientation = orientation;
78 this.tabBar.dataset['placement'] = this._tabPlacement;
79
80 // Create the box layout.
81 let layout = new BoxLayout({ direction, spacing: 0 });
82
83 // Set the stretch factors for the child widgets.
84 BoxLayout.setStretch(this.tabBar, 0);
85 BoxLayout.setStretch(this.stackedPanel, 1);
86
87 // Add the child widgets to the layout.
88 layout.addWidget(this.tabBar);
89 layout.addWidget(this.stackedPanel);
90
91 // Install the layout on the tab panel.
92 this.layout = layout;
93 }
94
95 /**
96 * A signal emitted when the current tab is changed.
97 *
98 * #### Notes
99 * This signal is emitted when the currently selected tab is changed
100 * either through user or programmatic interaction.
101 *
102 * Notably, this signal is not emitted when the index of the current
103 * tab changes due to tabs being inserted, removed, or moved. It is
104 * only emitted when the actual current tab node is changed.
105 */
106 get currentChanged(): ISignal<this, TabPanel.ICurrentChangedArgs> {
107 return this._currentChanged;
108 }
109
110 /**
111 * Get the index of the currently selected tab.
112 *
113 * #### Notes
114 * This will be `-1` if no tab is selected.
115 */
116 get currentIndex(): number {
117 return this.tabBar.currentIndex;
118 }
119
120 /**
121 * Set the index of the currently selected tab.
122 *
123 * #### Notes
124 * If the index is out of range, it will be set to `-1`.
125 */
126 set currentIndex(value: number) {
127 this.tabBar.currentIndex = value;
128 }
129
130 /**
131 * Get the currently selected widget.
132 *
133 * #### Notes
134 * This will be `null` if there is no selected tab.
135 */
136 get currentWidget(): Widget | null {
137 let title = this.tabBar.currentTitle;
138 return title ? title.owner : null;
139 }
140
141 /**
142 * Set the currently selected widget.
143 *
144 * #### Notes
145 * If the widget is not in the panel, it will be set to `null`.
146 */
147 set currentWidget(value: Widget | null) {
148 this.tabBar.currentTitle = value ? value.title : null;
149 }
150
151 /**
152 * Get the whether the tabs are movable by the user.
153 *
154 * #### Notes
155 * Tabs can always be moved programmatically.
156 */
157 get tabsMovable(): boolean {
158 return this.tabBar.tabsMovable;
159 }
160
161 /**
162 * Set the whether the tabs are movable by the user.
163 *
164 * #### Notes
165 * Tabs can always be moved programmatically.
166 */
167 set tabsMovable(value: boolean) {
168 this.tabBar.tabsMovable = value;
169 }
170
171 /**
172 * Get the whether the add button is enabled.
173 *
174 */
175 get addButtonEnabled(): boolean {
176 return this.tabBar.addButtonEnabled;
177 }
178
179 /**
180 * Set the whether the add button is enabled.
181 *
182 */
183 set addButtonEnabled(value: boolean) {
184 this.tabBar.addButtonEnabled = value;
185 }
186
187 /**
188 * Get the tab placement for the tab panel.
189 *
190 * #### Notes
191 * This controls the position of the tab bar relative to the content.
192 */
193 get tabPlacement(): TabPanel.TabPlacement {
194 return this._tabPlacement;
195 }
196
197 /**
198 * Set the tab placement for the tab panel.
199 *
200 * #### Notes
201 * This controls the position of the tab bar relative to the content.
202 */
203 set tabPlacement(value: TabPanel.TabPlacement) {
204 // Bail if the placement does not change.
205 if (this._tabPlacement === value) {
206 return;
207 }
208
209 // Update the internal value.
210 this._tabPlacement = value;
211
212 // Get the values related to the placement.
213 let direction = Private.directionFromPlacement(value);
214 let orientation = Private.orientationFromPlacement(value);
215
216 // Configure the tab bar for the placement.
217 this.tabBar.orientation = orientation;
218 this.tabBar.dataset['placement'] = value;
219
220 // Update the layout direction.
221 (this.layout as BoxLayout).direction = direction;
222 }
223
224 /**
225 * A signal emitted when the add button on a tab bar is clicked.
226 *
227 */
228 get addRequested(): ISignal<this, TabBar<Widget>> {
229 return this._addRequested;
230 }
231
232 /**
233 * The tab bar used by the tab panel.
234 *
235 * #### Notes
236 * Modifying the tab bar directly can lead to undefined behavior.
237 */
238 readonly tabBar: TabBar<Widget>;
239
240 /**
241 * The stacked panel used by the tab panel.
242 *
243 * #### Notes
244 * Modifying the panel directly can lead to undefined behavior.
245 */
246 readonly stackedPanel: StackedPanel;
247
248 /**
249 * A read-only array of the widgets in the panel.
250 */
251 get widgets(): ReadonlyArray<Widget> {
252 return this.stackedPanel.widgets;
253 }
254
255 /**
256 * Add a widget to the end of the tab panel.
257 *
258 * @param widget - The widget to add to the tab panel.
259 *
260 * #### Notes
261 * If the widget is already contained in the panel, it will be moved.
262 *
263 * The widget's `title` is used to populate the tab.
264 */
265 addWidget(widget: Widget): void {
266 this.insertWidget(this.widgets.length, widget);
267 }
268
269 /**
270 * Insert a widget into the tab panel at a specified index.
271 *
272 * @param index - The index at which to insert the widget.
273 *
274 * @param widget - The widget to insert into to the tab panel.
275 *
276 * #### Notes
277 * If the widget is already contained in the panel, it will be moved.
278 *
279 * The widget's `title` is used to populate the tab.
280 */
281 insertWidget(index: number, widget: Widget): void {
282 if (widget !== this.currentWidget) {
283 widget.hide();
284 }
285 this.stackedPanel.insertWidget(index, widget);
286 this.tabBar.insertTab(index, widget.title);
287
288 widget.node.setAttribute('role', 'tabpanel');
289
290 let renderer = this.tabBar.renderer;
291 if (renderer instanceof TabBar.Renderer) {
292 let tabId = renderer.createTabKey({
293 title: widget.title,
294 current: false,
295 zIndex: 0
296 });
297 widget.node.setAttribute('aria-labelledby', tabId);
298 }
299 }
300
301 /**
302 * Handle the `currentChanged` signal from the tab bar.
303 */
304 private _onCurrentChanged(
305 sender: TabBar<Widget>,
306 args: TabBar.ICurrentChangedArgs<Widget>
307 ): void {
308 // Extract the previous and current title from the args.
309 let { previousIndex, previousTitle, currentIndex, currentTitle } = args;
310
311 // Extract the widgets from the titles.
312 let previousWidget = previousTitle ? previousTitle.owner : null;
313 let currentWidget = currentTitle ? currentTitle.owner : null;
314
315 // Hide the previous widget.
316 if (previousWidget) {
317 previousWidget.hide();
318 }
319
320 // Show the current widget.
321 if (currentWidget) {
322 currentWidget.show();
323 }
324
325 // Emit the `currentChanged` signal for the tab panel.
326 this._currentChanged.emit({
327 previousIndex,
328 previousWidget,
329 currentIndex,
330 currentWidget
331 });
332
333 // Flush the message loop on IE and Edge to prevent flicker.
334 if (Platform.IS_EDGE || Platform.IS_IE) {
335 MessageLoop.flush();
336 }
337 }
338
339 /**
340 * Handle the `tabAddRequested` signal from the tab bar.
341 */
342 private _onTabAddRequested(sender: TabBar<Widget>, args: void): void {
343 this._addRequested.emit(sender);
344 }
345
346 /**
347 * Handle the `tabActivateRequested` signal from the tab bar.
348 */
349 private _onTabActivateRequested(
350 sender: TabBar<Widget>,
351 args: TabBar.ITabActivateRequestedArgs<Widget>
352 ): void {
353 args.title.owner.activate();
354 }
355
356 /**
357 * Handle the `tabCloseRequested` signal from the tab bar.
358 */
359 private _onTabCloseRequested(
360 sender: TabBar<Widget>,
361 args: TabBar.ITabCloseRequestedArgs<Widget>
362 ): void {
363 args.title.owner.close();
364 }
365
366 /**
367 * Handle the `tabMoved` signal from the tab bar.
368 */
369 private _onTabMoved(
370 sender: TabBar<Widget>,
371 args: TabBar.ITabMovedArgs<Widget>
372 ): void {
373 this.stackedPanel.insertWidget(args.toIndex, args.title.owner);
374 }
375
376 /**
377 * Handle the `widgetRemoved` signal from the stacked panel.
378 */
379 private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void {
380 widget.node.removeAttribute('role');
381 widget.node.removeAttribute('aria-labelledby');
382 this.tabBar.removeTab(widget.title);
383 }
384
385 private _tabPlacement: TabPanel.TabPlacement;
386 private _currentChanged = new Signal<this, TabPanel.ICurrentChangedArgs>(
387 this
388 );
389
390 private _addRequested = new Signal<this, TabBar<Widget>>(this);
391}
392
393/**
394 * The namespace for the `TabPanel` class statics.
395 */
396export namespace TabPanel {
397 /**
398 * A type alias for tab placement in a tab bar.
399 */
400 export type TabPlacement =
401 | /**
402 * The tabs are placed as a row above the content.
403 */
404 'top'
405
406 /**
407 * The tabs are placed as a column to the left of the content.
408 */
409 | 'left'
410
411 /**
412 * The tabs are placed as a column to the right of the content.
413 */
414 | 'right'
415
416 /**
417 * The tabs are placed as a row below the content.
418 */
419 | 'bottom';
420
421 /**
422 * An options object for initializing a tab panel.
423 */
424 export interface IOptions {
425 /**
426 * The document to use with the tab panel.
427 *
428 * The default is the global `document` instance.
429 */
430 document?: Document | ShadowRoot;
431
432 /**
433 * Whether the tabs are movable by the user.
434 *
435 * The default is `false`.
436 */
437 tabsMovable?: boolean;
438
439 /**
440 * Whether the button to add new tabs is enabled.
441 *
442 * The default is `false`.
443 */
444 addButtonEnabled?: boolean;
445
446 /**
447 * The placement of the tab bar relative to the content.
448 *
449 * The default is `'top'`.
450 */
451 tabPlacement?: TabPlacement;
452
453 /**
454 * The renderer for the panel's tab bar.
455 *
456 * The default is a shared renderer instance.
457 */
458 renderer?: TabBar.IRenderer<Widget>;
459 }
460
461 /**
462 * The arguments object for the `currentChanged` signal.
463 */
464 export interface ICurrentChangedArgs {
465 /**
466 * The previously selected index.
467 */
468 previousIndex: number;
469
470 /**
471 * The previously selected widget.
472 */
473 previousWidget: Widget | null;
474
475 /**
476 * The currently selected index.
477 */
478 currentIndex: number;
479
480 /**
481 * The currently selected widget.
482 */
483 currentWidget: Widget | null;
484 }
485}
486
487/**
488 * The namespace for the module implementation details.
489 */
490namespace Private {
491 /**
492 * Convert a tab placement to tab bar orientation.
493 */
494 export function orientationFromPlacement(
495 plc: TabPanel.TabPlacement
496 ): TabBar.Orientation {
497 return placementToOrientationMap[plc];
498 }
499
500 /**
501 * Convert a tab placement to a box layout direction.
502 */
503 export function directionFromPlacement(
504 plc: TabPanel.TabPlacement
505 ): BoxLayout.Direction {
506 return placementToDirectionMap[plc];
507 }
508
509 /**
510 * A mapping of tab placement to tab bar orientation.
511 */
512 const placementToOrientationMap: { [key: string]: TabBar.Orientation } = {
513 top: 'horizontal',
514 left: 'vertical',
515 right: 'vertical',
516 bottom: 'horizontal'
517 };
518
519 /**
520 * A mapping of tab placement to box layout direction.
521 */
522 const placementToDirectionMap: { [key: string]: BoxLayout.Direction } = {
523 top: 'top-to-bottom',
524 left: 'left-to-right',
525 right: 'right-to-left',
526 bottom: 'bottom-to-top'
527 };
528}