// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /*----------------------------------------------------------------------------- | Copyright (c) 2014-2017, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ import { Platform } from '@lumino/domutils'; import { MessageLoop } from '@lumino/messaging'; import { ISignal, Signal } from '@lumino/signaling'; import { BoxLayout } from './boxlayout'; import { StackedPanel } from './stackedpanel'; import { TabBar } from './tabbar'; import { Widget } from './widget'; /** * A widget which combines a `TabBar` and a `StackedPanel`. * * #### Notes * This is a simple panel which handles the common case of a tab bar * placed next to a content area. The selected tab controls the widget * which is shown in the content area. * * For use cases which require more control than is provided by this * panel, the `TabBar` widget may be used independently. */ export class TabPanel extends Widget { /** * Construct a new tab panel. * * @param options - The options for initializing the tab panel. */ constructor(options: TabPanel.IOptions = {}) { super(); this.addClass('lm-TabPanel'); /* */ this.addClass('p-TabPanel'); /* */ // Create the tab bar and stacked panel. this.tabBar = new TabBar(options); this.tabBar.addClass('lm-TabPanel-tabBar'); this.stackedPanel = new StackedPanel(); this.stackedPanel.addClass('lm-TabPanel-stackedPanel'); /* */ this.tabBar.addClass('p-TabPanel-tabBar'); this.stackedPanel.addClass('p-TabPanel-stackedPanel'); /* */ // Connect the tab bar signal handlers. this.tabBar.tabMoved.connect(this._onTabMoved, this); this.tabBar.currentChanged.connect(this._onCurrentChanged, this); this.tabBar.tabCloseRequested.connect(this._onTabCloseRequested, this); this.tabBar.tabActivateRequested.connect( this._onTabActivateRequested, this ); this.tabBar.addRequested.connect(this._onTabAddRequested, this); // Connect the stacked panel signal handlers. this.stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this); // Get the data related to the placement. this._tabPlacement = options.tabPlacement || 'top'; let direction = Private.directionFromPlacement(this._tabPlacement); let orientation = Private.orientationFromPlacement(this._tabPlacement); // Configure the tab bar for the placement. this.tabBar.orientation = orientation; this.tabBar.dataset['placement'] = this._tabPlacement; // Create the box layout. let layout = new BoxLayout({ direction, spacing: 0 }); // Set the stretch factors for the child widgets. BoxLayout.setStretch(this.tabBar, 0); BoxLayout.setStretch(this.stackedPanel, 1); // Add the child widgets to the layout. layout.addWidget(this.tabBar); layout.addWidget(this.stackedPanel); // Install the layout on the tab panel. this.layout = layout; } /** * A signal emitted when the current tab is changed. * * #### Notes * This signal is emitted when the currently selected tab is changed * either through user or programmatic interaction. * * Notably, this signal is not emitted when the index of the current * tab changes due to tabs being inserted, removed, or moved. It is * only emitted when the actual current tab node is changed. */ get currentChanged(): ISignal { return this._currentChanged; } /** * Get the index of the currently selected tab. * * #### Notes * This will be `-1` if no tab is selected. */ get currentIndex(): number { return this.tabBar.currentIndex; } /** * Set the index of the currently selected tab. * * #### Notes * If the index is out of range, it will be set to `-1`. */ set currentIndex(value: number) { this.tabBar.currentIndex = value; } /** * Get the currently selected widget. * * #### Notes * This will be `null` if there is no selected tab. */ get currentWidget(): Widget | null { let title = this.tabBar.currentTitle; return title ? title.owner : null; } /** * Set the currently selected widget. * * #### Notes * If the widget is not in the panel, it will be set to `null`. */ set currentWidget(value: Widget | null) { this.tabBar.currentTitle = value ? value.title : null; } /** * Get the whether the tabs are movable by the user. * * #### Notes * Tabs can always be moved programmatically. */ get tabsMovable(): boolean { return this.tabBar.tabsMovable; } /** * Set the whether the tabs are movable by the user. * * #### Notes * Tabs can always be moved programmatically. */ set tabsMovable(value: boolean) { this.tabBar.tabsMovable = value; } /** * Get the whether the add button is enabled. * */ get addButtonEnabled(): boolean { return this.tabBar.addButtonEnabled; } /** * Set the whether the add button is enabled. * */ set addButtonEnabled(value: boolean) { this.tabBar.addButtonEnabled = value; } /** * Get the tab placement for the tab panel. * * #### Notes * This controls the position of the tab bar relative to the content. */ get tabPlacement(): TabPanel.TabPlacement { return this._tabPlacement; } /** * Set the tab placement for the tab panel. * * #### Notes * This controls the position of the tab bar relative to the content. */ set tabPlacement(value: TabPanel.TabPlacement) { // Bail if the placement does not change. if (this._tabPlacement === value) { return; } // Update the internal value. this._tabPlacement = value; // Get the values related to the placement. let direction = Private.directionFromPlacement(value); let orientation = Private.orientationFromPlacement(value); // Configure the tab bar for the placement. this.tabBar.orientation = orientation; this.tabBar.dataset['placement'] = value; // Update the layout direction. (this.layout as BoxLayout).direction = direction; } /** * A signal emitted when the add button on a tab bar is clicked. * */ get addRequested(): ISignal> { return this._addRequested; } /** * The tab bar used by the tab panel. * * #### Notes * Modifying the tab bar directly can lead to undefined behavior. */ readonly tabBar: TabBar; /** * The stacked panel used by the tab panel. * * #### Notes * Modifying the panel directly can lead to undefined behavior. */ readonly stackedPanel: StackedPanel; /** * A read-only array of the widgets in the panel. */ get widgets(): ReadonlyArray { return this.stackedPanel.widgets; } /** * Add a widget to the end of the tab panel. * * @param widget - The widget to add to the tab panel. * * #### Notes * If the widget is already contained in the panel, it will be moved. * * The widget's `title` is used to populate the tab. */ addWidget(widget: Widget): void { this.insertWidget(this.widgets.length, widget); } /** * Insert a widget into the tab panel at a specified index. * * @param index - The index at which to insert the widget. * * @param widget - The widget to insert into to the tab panel. * * #### Notes * If the widget is already contained in the panel, it will be moved. * * The widget's `title` is used to populate the tab. */ insertWidget(index: number, widget: Widget): void { if (widget !== this.currentWidget) { widget.hide(); } this.stackedPanel.insertWidget(index, widget); this.tabBar.insertTab(index, widget.title); widget.node.setAttribute('role', 'tabpanel'); let renderer = this.tabBar.renderer; if (renderer instanceof TabBar.Renderer) { let tabId = renderer.createTabKey({ title: widget.title, current: false, zIndex: 0 }); widget.node.setAttribute('aria-labelledby', tabId); } } /** * Handle the `currentChanged` signal from the tab bar. */ private _onCurrentChanged( sender: TabBar, args: TabBar.ICurrentChangedArgs ): void { // Extract the previous and current title from the args. let { previousIndex, previousTitle, currentIndex, currentTitle } = args; // Extract the widgets from the titles. let previousWidget = previousTitle ? previousTitle.owner : null; let currentWidget = currentTitle ? currentTitle.owner : null; // Hide the previous widget. if (previousWidget) { previousWidget.hide(); } // Show the current widget. if (currentWidget) { currentWidget.show(); } // Emit the `currentChanged` signal for the tab panel. this._currentChanged.emit({ previousIndex, previousWidget, currentIndex, currentWidget }); // Flush the message loop on IE and Edge to prevent flicker. if (Platform.IS_EDGE || Platform.IS_IE) { MessageLoop.flush(); } } /** * Handle the `tabAddRequested` signal from the tab bar. */ private _onTabAddRequested(sender: TabBar, args: void): void { this._addRequested.emit(sender); } /** * Handle the `tabActivateRequested` signal from the tab bar. */ private _onTabActivateRequested( sender: TabBar, args: TabBar.ITabActivateRequestedArgs ): void { args.title.owner.activate(); } /** * Handle the `tabCloseRequested` signal from the tab bar. */ private _onTabCloseRequested( sender: TabBar, args: TabBar.ITabCloseRequestedArgs ): void { args.title.owner.close(); } /** * Handle the `tabMoved` signal from the tab bar. */ private _onTabMoved( sender: TabBar, args: TabBar.ITabMovedArgs ): void { this.stackedPanel.insertWidget(args.toIndex, args.title.owner); } /** * Handle the `widgetRemoved` signal from the stacked panel. */ private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void { widget.node.removeAttribute('role'); widget.node.removeAttribute('aria-labelledby'); this.tabBar.removeTab(widget.title); } private _tabPlacement: TabPanel.TabPlacement; private _currentChanged = new Signal( this ); private _addRequested = new Signal>(this); } /** * The namespace for the `TabPanel` class statics. */ export namespace TabPanel { /** * A type alias for tab placement in a tab bar. */ export type TabPlacement = | /** * The tabs are placed as a row above the content. */ 'top' /** * The tabs are placed as a column to the left of the content. */ | 'left' /** * The tabs are placed as a column to the right of the content. */ | 'right' /** * The tabs are placed as a row below the content. */ | 'bottom'; /** * An options object for initializing a tab panel. */ export interface IOptions { /** * The document to use with the tab panel. * * The default is the global `document` instance. */ document?: Document | ShadowRoot; /** * Whether the tabs are movable by the user. * * The default is `false`. */ tabsMovable?: boolean; /** * Whether the button to add new tabs is enabled. * * The default is `false`. */ addButtonEnabled?: boolean; /** * The placement of the tab bar relative to the content. * * The default is `'top'`. */ tabPlacement?: TabPlacement; /** * The renderer for the panel's tab bar. * * The default is a shared renderer instance. */ renderer?: TabBar.IRenderer; } /** * The arguments object for the `currentChanged` signal. */ export interface ICurrentChangedArgs { /** * The previously selected index. */ previousIndex: number; /** * The previously selected widget. */ previousWidget: Widget | null; /** * The currently selected index. */ currentIndex: number; /** * The currently selected widget. */ currentWidget: Widget | null; } } /** * The namespace for the module implementation details. */ namespace Private { /** * Convert a tab placement to tab bar orientation. */ export function orientationFromPlacement( plc: TabPanel.TabPlacement ): TabBar.Orientation { return placementToOrientationMap[plc]; } /** * Convert a tab placement to a box layout direction. */ export function directionFromPlacement( plc: TabPanel.TabPlacement ): BoxLayout.Direction { return placementToDirectionMap[plc]; } /** * A mapping of tab placement to tab bar orientation. */ const placementToOrientationMap: { [key: string]: TabBar.Orientation } = { top: 'horizontal', left: 'vertical', right: 'vertical', bottom: 'horizontal' }; /** * A mapping of tab placement to box layout direction. */ const placementToDirectionMap: { [key: string]: BoxLayout.Direction } = { top: 'top-to-bottom', left: 'left-to-right', right: 'right-to-left', bottom: 'bottom-to-top' }; }