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 | |----------------------------------------------------------------------------*/
|
10 | import { Platform } from '@lumino/domutils';
|
11 |
|
12 | import { MessageLoop } from '@lumino/messaging';
|
13 |
|
14 | import { ISignal, Signal } from '@lumino/signaling';
|
15 |
|
16 | import { BoxLayout } from './boxlayout';
|
17 |
|
18 | import { StackedPanel } from './stackedpanel';
|
19 |
|
20 | import { TabBar } from './tabbar';
|
21 |
|
22 | import { 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 | */
|
35 | export 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 | */
|
396 | export 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 | */
|
490 | namespace 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 | }
|