/********************************************************************************
 * Copyright (C) 2018 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/ import PerfectScrollbar from 'perfect-scrollbar' ; import { TabBar , Title , Widget } from '@phosphor/widgets' ; import { VirtualElement , h , VirtualDOM , ElementInlineStyle } from '@phosphor/virtualdom' ; import { Disposable , DisposableCollection , MenuPath , notEmpty , SelectionService } from '../../common' ; import { ContextMenuRenderer } from '../context-menu-renderer' ; import { Signal , Slot } from '@phosphor/signaling' ; import { Message , MessageLoop } from '@phosphor/messaging' ; import { ArrayExt } from '@phosphor/algorithm' ; import { ElementExt } from '@phosphor/domutils' ; import { TabBarToolbarRegistry , TabBarToolbar } from './tab-bar-toolbar' ; import { TheiaDockPanel , MAIN_AREA_ID , BOTTOM_AREA_ID } from './theia-dock-panel' ; import { WidgetDecoration } from '../widget-decoration' ; import { TabBarDecoratorService } from './tab-bar-decorator' ; import { IconThemeService } from '../icon-theme-service' ; import { BreadcrumbsRenderer , BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer' ; import { NavigatableWidget } from '../navigatable-types' ; import { IDragEvent } from '@phosphor/dragdrop' ; /** The class name added to hidden content nodes, which are required to render vertical side bars. */ const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content' ; /** Menu path for tab bars used throughout the application shell. */ export const SHELL_TABBAR_CONTEXT_MENU = [ 'shell-tabbar-context-menu' ] ; export const SHELL_TABBAR_CONTEXT_CLOSE = [ ... SHELL_TABBAR_CONTEXT_MENU , '0_close' ] ; export const SHELL_TABBAR_CONTEXT_COPY = [ ... SHELL_TABBAR_CONTEXT_MENU , '1_copy' ] ; export const SHELL_TABBAR_CONTEXT_PIN = [ ... SHELL_TABBAR_CONTEXT_MENU , '4_pin' ] ; export const SHELL_TABBAR_CONTEXT_SPLIT = [ ... SHELL_TABBAR_CONTEXT_MENU , '5_split' ] ; export const TabBarRendererFactory = Symbol ( 'TabBarRendererFactory' ) ; export type TabBarRendererFactory = ( ) => TabBarRenderer ; /**
 * Size information of DOM elements used for rendering tabs in side bars.
 */ export interface SizeData { width : number ; height : number ; } /**
 * Extension of the rendering data used for tabs in side bars of the application shell.
 */ export interface SideBarRenderData extends TabBar . IRenderData < Widget > { labelSize ? : SizeData ; iconSize ? : SizeData ; paddingTop ? : number ; paddingBottom ? : number ; } /**
 * A tab bar renderer that offers a context menu. In addition, this renderer is able to
 * set an explicit position and size on the icon and label of each tab in a side bar.
 * This is necessary because the elements of side bar tabs are rotated using the CSS
 * `transform` property, disrupting the browser's ability to arrange those elements
 * automatically.
 */ export class TabBarRenderer extends TabBar . Renderer { /**
     * The menu path used to render the context menu.
     */ contextMenuPath ? : MenuPath ; protected readonly toDispose = new DisposableCollection ( ) ; constructor ( protected readonly contextMenuRenderer ? , protected readonly decoratorService ? , protected readonly iconThemeService ? , protected readonly selectionService ? ) { super ( ) ; if ( this . decoratorService ) { this . toDispose . push ( Disposable . create ( ( ) => this . resetDecorations ( ) ) ) ; this . toDispose . push ( this . decoratorService . onDidChangeDecorations ( ( ) => this . resetDecorations ( ) ) ) ; } if ( this . iconThemeService ) { this . toDispose . push ( this . iconThemeService . onDidChangeCurrent ( ( ) => { if ( this . _tabBar ) { this . _tabBar . update ( ) ; } } ) ) ; } } dispose ( ) { this . toDispose . dispose ( ) ; } protected _tabBar ? : TabBar < Widget > ; protected readonly toDisposeOnTabBar = new DisposableCollection ( ) ; /**
     * A reference to the tab bar is required in order to activate it when a context menu
     * is requested.
     */ set tabBar ( tabBar ) { if ( this . toDispose . disposed ) { throw new Error ( 'disposed' ) ; } if ( this . _tabBar === tabBar ) { return ; } this . toDisposeOnTabBar . dispose ( ) ; this . toDispose . push ( this . toDisposeOnTabBar ) ; this . _tabBar = tabBar ; if ( tabBar ) { const listener = ( _ , { title } ) => this . resetDecorations ( title ) ; tabBar . tabCloseRequested . connect ( listener ) ; this . toDisposeOnTabBar . push ( Disposable . create ( ( ) => tabBar . tabCloseRequested . disconnect ( listener ) ) ) ; } this . resetDecorations ( ) ; } get tabBar ( ) : TabBar < Widget > | undefined { return this . _tabBar ; } param { SideBarRenderData } data param { boolean } isInSidePanel returns { VirtualElement } renderTab ( data , isInSidePanel ? ) { const title = data . title ; const id = this . createTabId ( data . title ) ; const key = this . createTabKey ( data ) ; const style = this . createTabStyle ( data ) ; const className = this . createTabClass ( data ) ; const dataset = this . createTabDataset ( data ) ; return h . li ( { key , className , id , title : title . caption , style , dataset , oncontextmenu : this . handleContextMenuEvent , ondblclick : this . handleDblClickEvent , onauxclick : ( e ) => { e . preventDefault ( ) ; } } , h . div ( { className : 'theia-tab-icon-label' } , this . renderIcon ( data , isInSidePanel ) , this . renderLabel ( data , isInSidePanel ) , this . renderBadge ( data , isInSidePanel ) ) , this . renderCloseIcon ( data ) ) ; } createTabId ( title ) { return 'shell-tab-' + title . owner . id ; } /**
     * If size information is available for the label and icon, set an explicit height on the tab.
     * The height value also considers padding, which should be derived from CSS settings.
     */ createTabStyle ( data ) { const zIndex = `${ data . zIndex }` ; const labelSize = data . labelSize ; const iconSize = data . iconSize ; let height ; if ( labelSize || iconSize ) { const labelHeight = labelSize ? ( this . tabBar && this . tabBar . orientation === 'horizontal' ? labelSize . height : labelSize . width ) : 0 ; const iconHeight = iconSize ? iconSize . height : 0 ; let paddingTop = data . paddingTop || 0 ; if ( labelHeight > 0 && iconHeight > 0 ) { paddingTop = paddingTop * 1.5 ; } const paddingBottom = data . paddingBottom || 0 ; height = `${ labelHeight + iconHeight + paddingTop + paddingBottom }px` ; } return { zIndex , height } ; } param { SideBarRenderData } data param { boolean } isInSidePanel returns { VirtualElement } renderLabel ( data , isInSidePanel ? ) { const labelSize = data . labelSize ; const iconSize = data . iconSize ; let width ; let height ; let top ; if ( labelSize ) { width = `${ labelSize . width }px` ; height = `${ labelSize . height }px` ; } if ( data . paddingTop || iconSize ) { const iconHeight = iconSize ? iconSize . height : 0 ; let paddingTop = data . paddingTop || 0 ; if ( iconHeight > 0 ) { paddingTop = paddingTop * 1.5 ; } top = `${ paddingTop + iconHeight }px` ; } const style = { width , height , top } ; if ( isInSidePanel || ( this . tabBar && this . tabBar . titles . length < 2 ) ) { return h . div ( { className : 'p-TabBar-tabLabel' , style } , data . title . label ) ; } const originalToDisplayedMap = this . findDuplicateLabels ( [ ... this . tabBar ! . titles ] ) ; const labelDetails = originalToDisplayedMap . get ( data . title . caption ) ; if ( labelDetails ) { return h . div ( { className : 'p-TabBar-tabLabelWrapper' } , h . div ( { className : 'p-TabBar-tabLabel' , style } , data . title . label ) , h . div ( { className : 'p-TabBar-tabLabelDetails' , style } , labelDetails ) ) ; } return h . div ( { className : 'p-TabBar-tabLabel' , style } , data . title . label ) ; } renderBadge ( data , isInSidePanel ? ) { const totalBadge = this . getDecorationData ( data . title , 'badge' ) . reduce ( ( sum , badge ) => sum ! + badge ! , 0 ) ; if ( ! totalBadge ) { return h . div ( { } ) ; } const limitedBadge = totalBadge >= 100 ? '99+' : totalBadge ; return isInSidePanel ? h . div ( { className : 'theia-badge-decorator-sidebar' } , `${ limitedBadge }` ) : h . div ( { className : 'theia-badge-decorator-horizontal' } , `${ limitedBadge }` ) ; } protected readonly decorations = new Map < Title < Widget > , WidgetDecoration . Data [ ] > ( ) ; protected resetDecorations ( title ? ) { if ( title ) { this . decorations . delete ( title ) ; } else { this . decorations . clear ( ) ; } if ( this . tabBar ) { this . tabBar . update ( ) ; } } param { string } title protected getDecorations ( title ) { if ( this . tabBar && this . decoratorService ) { const owner = title . owner ; if ( ! owner . resetTabBarDecorations ) { owner . resetTabBarDecorations = ( ) => this . decorations . delete ( title ) ; title . owner . disposed . connect ( owner . resetTabBarDecorations ) ; } const decorations = this . decorations . get ( title ) || this . decoratorService . getDecorations ( title ) ; this . decorations . set ( title , decorations ) ; return decorations ; } return [ ] ; } param { string } title param { K } key protected getDecorationData < K extends keyof WidgetDecoration . Data > ( title , key ) { return this . getDecorations ( title ) . filter ( data => data [ key ] !== undefined ) . map ( data => data [ key ] ) ; } param { string | string [ ] } iconName param { string [ ] } additionalClasses private getIconClass ( iconName , additionalClasses = [ ] ) { const iconClass = ( typeof iconName === 'string' ) ? [ 'a' , 'fa' , `fa-${ iconName }` ] : [ 'a' ] . concat ( iconName ) ; return iconClass . concat ( additionalClasses ) . join ( ' ' ) ; } param { Title < Widget > [ ] } titles returns { Map < string , string > } findDuplicateLabels ( titles ) { const labelGroups = new Map < string , Map < number , string > > ( ) ; titles . forEach ( ( title , index ) => { if ( ! labelGroups . has ( title . label ) ) { labelGroups . set ( title . label , new Map < number , string > ( ) ) ; } labelGroups . get ( title . label ) ! . set ( index , title . caption ) ; } ) ; const originalToDisplayedMap = new Map < string , string > ( ) ; labelGroups . forEach ( labelGroup => { if ( labelGroup . size > 1 ) { const paths = [ ] ; let maxPathLength = 0 ; labelGroup . forEach ( ( pathStr , index ) => { const steps = pathStr . split ( '/' ) ; maxPathLength = Math . max ( maxPathLength , steps . length ) ; paths [ index ] = ( steps . slice ( 0 , steps . length - 1 ) ) ; let defaultDisplayedPath = steps . slice ( - 4 , - 1 ) . join ( '/' ) ; if ( steps . length > 4 ) { defaultDisplayedPath = '.../' + defaultDisplayedPath ; } originalToDisplayedMap . set ( pathStr , defaultDisplayedPath ) ; } ) ; let i = 0 ; while ( i < maxPathLength - 1 ) { const stepOccurrences = new Map < string , number [ ] > ( ) ; paths . forEach ( ( path , index ) => { const step = path [ i ] ; if ( path . length > 0 ) { if ( i > path . length - 1 ) { paths [ index ] = [ ] ; } else if ( ! stepOccurrences . has ( step ) ) { stepOccurrences . set ( step , [ index ] ) ; } else { stepOccurrences . get ( step ) ! . push ( index ) ; } } } ) ; stepOccurrences . forEach ( ( indexArr , displayedPath ) => { if ( indexArr . length === 1 ) { const originalPath = labelGroup . get ( indexArr [ 0 ] ) ; if ( originalPath ) { const originalElements = originalPath . split ( '/' ) ; const displayedElements = displayedPath . split ( '/' ) ; if ( originalElements . slice ( - 2 ) [ 0 ] !== displayedElements . slice ( - 1 ) [ 0 ] ) { displayedPath += '/...' ; } if ( originalElements [ 0 ] !== displayedElements [ 0 ] ) { displayedPath = '.../' + displayedPath ; } originalToDisplayedMap . set ( originalPath , displayedPath ) ; paths [ indexArr [ 0 ] ] = [ ] ; } } } ) ; i ++ ; } } } ) ; return originalToDisplayedMap ; } param { SideBarRenderData } data param { boolean } isInSidePanel renderIcon ( data , isInSidePanel ? ) { if ( ! isInSidePanel && this . iconThemeService && this . iconThemeService . current === 'none' ) { return h . div ( ) ; } let top ; if ( data . paddingTop ) { top = `${ data . paddingTop || 0 }px` ; } const style = { top } ; const baseClassName = this . createIconClass ( data ) ; const overlayIcons = [ ] ; const decorationData = this . getDecorationData ( data . title , 'iconOverlay' ) ; if ( decorationData . length > 0 ) { const baseIcon = h . div ( { className : baseClassName , style } , data . title . iconLabel ) ; const wrapperClassName = WidgetDecoration . Styles . ICON_WRAPPER_CLASS ; const decoratorSizeClassName = isInSidePanel ? WidgetDecoration . Styles . DECORATOR_SIDEBAR_SIZE_CLASS : WidgetDecoration . Styles . DECORATOR_SIZE_CLASS ; decorationData . filter ( notEmpty ) . map ( overlay => [ overlay . position , overlay ] as [ WidgetDecoration . IconOverlayPosition , WidgetDecoration . IconOverlay | WidgetDecoration . IconClassOverlay ] ) . forEach ( ( [ position , overlay ] ) => { const iconAdditionalClasses = [ decoratorSizeClassName , WidgetDecoration . IconOverlayPosition . getStyle ( position , isInSidePanel ) ] ; const overlayIconStyle = ( color ? ) => { if ( color === undefined ) { return { } ; } return { color } ; } ; if ( overlay . background ) { const backgroundIconClassName = this . getIconClass ( overlay . background . shape , iconAdditionalClasses ) ; overlayIcons . push ( h . div ( { key : data . title . label + '-background' , className : backgroundIconClassName , style : overlayIconStyle ( overlay . background . color ) } ) ) ; } const overlayIcon = ( overlay as WidgetDecoration . IconOverlay ) . icon || ( overlay as WidgetDecoration . IconClassOverlay ) . iconClass ; const overlayIconClassName = this . getIconClass ( overlayIcon , iconAdditionalClasses ) ; overlayIcons . push ( h . span ( { key : data . title . label , className : overlayIconClassName , style : overlayIconStyle ( overlay . color ) } ) ) ; } ) ; return h . div ( { className : wrapperClassName , style } , [ baseIcon , ... overlayIcons ] ) ; } return h . div ( { className : baseClassName , style } , data . title . iconLabel ) ; } protected handleContextMenuEvent = ( event ) => { if ( this . contextMenuRenderer && this . contextMenuPath && event . currentTarget instanceof HTMLElement ) { event . stopPropagation ( ) ; event . preventDefault ( ) ; let widget = undefined ; if ( this . tabBar ) { const titleIndex = Array . from ( this . tabBar . contentNode . getElementsByClassName ( 'p-TabBar-tab' ) ) . findIndex ( node => node . contains ( event . currentTarget as HTMLElement ) ) ; if ( titleIndex !== - 1 ) { widget = this . tabBar . titles [ titleIndex ] . owner ; } } const oldSelection = this . selectionService ?. selection ; if ( widget && this . selectionService ) { this . selectionService . selection = NavigatableWidget . is ( widget ) ? { uri : widget . getResourceUri ( ) } : widget ; } this . contextMenuRenderer . render ( { menuPath : this . contextMenuPath ! , anchor : event , args : [ event ] , onHide : ( ) => setTimeout ( ( ) => { if ( this . selectionService ) { this . selectionService . selection = oldSelection ; } } ) } ) ; } } ; protected handleDblClickEvent = ( event ) => { if ( this . tabBar && event . currentTarget instanceof HTMLElement ) { const id = event . currentTarget . id ; const title = this . tabBar . titles . find ( t => this . createTabId ( t ) === id ) || null ; const area = title && title . owner . parent ; if ( area instanceof TheiaDockPanel && ( area . id === BOTTOM_AREA_ID || area . id === MAIN_AREA_ID ) ) { area . toggleMaximized ( ) ; } } } ; } /**
 * A specialized tab bar for the main and bottom areas.
 */ export class ScrollableTabBar extends TabBar < Widget > { protected scrollBar ? : PerfectScrollbar ; private scrollBarFactory : ( ) => PerfectScrollbar ; private pendingReveal ? : Promise < void > ; protected readonly toDispose = new DisposableCollection ( ) ; constructor ( options ? ) { super ( options ) ; this . scrollBarFactory = ( ) => new PerfectScrollbar ( this . scrollbarHost , options ) ; } dispose ( ) { if ( this . isDisposed ) { return ; } super . dispose ( ) ; this . toDispose . dispose ( ) ; } protected onAfterAttach ( msg ) { if ( ! this . scrollBar ) { this . scrollBar = this . scrollBarFactory ( ) ; } super . onAfterAttach ( msg ) ; } protected onBeforeDetach ( msg ) { super . onBeforeDetach ( msg ) ; if ( this . scrollBar ) { this . scrollBar . destroy ( ) ; this . scrollBar = undefined ; } } protected onUpdateRequest ( msg ) { super . onUpdateRequest ( msg ) ; if ( this . scrollBar ) { this . scrollBar . update ( ) ; } } protected onResize ( msg ) { super . onResize ( msg ) ; if ( this . scrollBar ) { if ( this . currentIndex >= 0 ) { this . revealTab ( this . currentIndex ) ; } this . scrollBar . update ( ) ; } } /**
     * Reveal the tab with the given index by moving the scroll bar if necessary.
     */ revealTab ( index ) { if ( this . pendingReveal ) { return this . pendingReveal ; } const result = new Promise < void > ( ( resolve , reject ) => { window . requestAnimationFrame ( ( ) => { const tab = this . contentNode . children [ index ] as HTMLElement ; if ( tab && this . isVisible ) { const parent = this . scrollbarHost ; if ( this . orientation === 'horizontal' ) { const scroll = parent . scrollLeft ; const left = tab . offsetLeft ; if ( scroll > left ) { parent . scrollLeft = left ; } else { const right = left + tab . clientWidth - parent . clientWidth ; if ( scroll < right && tab . clientWidth < parent . clientWidth ) { parent . scrollLeft = right ; } } } else { const scroll = parent . scrollTop ; const top = tab . offsetTop ; if ( scroll > top ) { parent . scrollTop = top ; } else { const bottom = top + tab . clientHeight - parent . clientHeight ; if ( scroll < bottom && tab . clientHeight < parent . clientHeight ) { parent . scrollTop = bottom ; } } } } if ( this . pendingReveal === result ) { this . pendingReveal = undefined ; } resolve ( ) ; } ) ; } ) ; this . pendingReveal = result ; return result ; } protected get scrollbarHost ( ) : HTMLElement { return this . node ; } } /**
 * Specialized scrollable tab-bar which comes with toolbar support.
 * Instead of the following DOM structure.
 *
 * +-------------------------+
 * |[TAB_0][TAB_1][TAB_2][TAB|
 * +-------------Scrollable--+
 *
 * There is a dedicated HTML element for toolbar which does **not** contained in the scrollable element.
 *
 * +-------------------------+-----------------+
 * |[TAB_0][TAB_1][TAB_2][TAB|         Toolbar |
 * +-------------Scrollable--+-Non-Scrollable-+
 *
 */ export class ToolbarAwareTabBar extends ScrollableTabBar { protected contentContainer : HTMLElement ; protected toolbar : TabBarToolbar | undefined ; protected breadcrumbsContainer : HTMLElement ; protected readonly breadcrumbsRenderer : BreadcrumbsRenderer ; protected topRow : HTMLElement ; constructor ( protected readonly tabBarToolbarRegistry , protected readonly tabBarToolbarFactory , protected readonly breadcrumbsRendererFactory , protected readonly options ? ) { super ( options ) ; this . breadcrumbsRenderer = this . breadcrumbsRendererFactory ( ) ; this . rewireDOM ( ) ; this . toDispose . push ( this . tabBarToolbarRegistry . onDidChange ( ( ) => this . update ( ) ) ) ; this . toDispose . push ( this . breadcrumbsRenderer ) ; this . toDispose . push ( this . breadcrumbsRenderer . onDidChangeActiveState ( active => { this . node . classList . toggle ( 'theia-tabBar-multirow' , active ) ; if ( this . parent ) { MessageLoop . sendMessage ( this . parent , new Message ( 'fit-request' ) ) ; } } ) ) ; this . node . classList . toggle ( 'theia-tabBar-multirow' , this . breadcrumbsRenderer . active ) ; const handler = ( ) => this . updateBreadcrumbs ( ) ; this . currentChanged . connect ( handler ) ; this . toDispose . push ( Disposable . create ( ( ) => this . currentChanged . disconnect ( handler ) ) ) ; } /**
     * Overrides the `contentNode` property getter in PhosphorJS' TabBar.
     */ get contentNode ( ) : HTMLUListElement { return this . tabBarContainer . getElementsByClassName ( ToolbarAwareTabBar . Styles . TAB_BAR_CONTENT ) [ 0 ] as HTMLUListElement ; } /**
     * Overrides the scrollable host from the parent class.
     */ protected get scrollbarHost ( ) : HTMLElement { return this . tabBarContainer ; } protected get tabBarContainer ( ) : HTMLElement { return this . node . getElementsByClassName ( ToolbarAwareTabBar . Styles . TAB_BAR_CONTENT_CONTAINER ) [ 0 ] as HTMLElement ; } protected async updateBreadcrumbs ( ) { const current = this . currentTitle ?. owner ; const uri = NavigatableWidget . is ( current ) ? current . getResourceUri ( ) : undefined ; await this . breadcrumbsRenderer . refresh ( uri ) ; } protected onAfterAttach ( msg ) { if ( this . toolbar ) { if ( this . toolbar . isAttached ) { Widget . detach ( this . toolbar ) ; } Widget . attach ( this . toolbar , this . topRow ) ; if ( this . breadcrumbsContainer ) { this . node . appendChild ( this . breadcrumbsContainer ) ; } this . breadcrumbsRenderer ?. refresh ( ) ; } super . onAfterAttach ( msg ) ; } protected onBeforeDetach ( msg ) { if ( this . toolbar && this . toolbar . isAttached ) { Widget . detach ( this . toolbar ) ; } super . onBeforeDetach ( msg ) ; } protected onUpdateRequest ( msg ) { super . onUpdateRequest ( msg ) ; this . updateToolbar ( ) ; } protected updateToolbar ( ) { if ( ! this . toolbar ) { return ; } const widget = this . currentTitle ?. owner ?? undefined ; this . toolbar . updateTarget ( widget ) ; } handleEvent ( event ) { if ( this . toolbar && event instanceof MouseEvent && this . toolbar . shouldHandleMouseEvent ( event ) ) { return ; } super . handleEvent ( event ) ; } /**
     * Restructures the DOM defined in PhosphorJS.
     *
     * By default the tabs (`li`) are contained in the `this.contentNode` (`ul`) which is wrapped in a `div` (`this.node`).
     * Instead of this structure, we add a container for the `this.contentNode` and for the toolbar.
     * The scrollbar will only work for the `ul` part but it does not affect the toolbar, so it can be on the right hand-side.
     */ protected rewireDOM ( ) { const contentNode = this . node . getElementsByClassName ( ToolbarAwareTabBar . Styles . TAB_BAR_CONTENT ) [ 0 ] ; if ( ! contentNode ) { throw new Error ( "'this.node' does not have the content as a direct child with class name 'p-TabBar-content'." ) ; } this . node . removeChild ( contentNode ) ; this . topRow = document . createElement ( 'div' ) ; this . topRow . classList . add ( 'theia-tabBar-tab-row' ) ; this . contentContainer = document . createElement ( 'div' ) ; this . contentContainer . classList . add ( ToolbarAwareTabBar . Styles . TAB_BAR_CONTENT_CONTAINER ) ; this . contentContainer . appendChild ( contentNode ) ; this . topRow . appendChild ( this . contentContainer ) ; this . node . appendChild ( this . topRow ) ; this . toolbar = this . tabBarToolbarFactory ( ) ; this . breadcrumbsContainer = document . createElement ( 'div' ) ; this . breadcrumbsContainer . classList . add ( 'theia-tabBar-breadcrumb-row' ) ; this . breadcrumbsContainer . appendChild ( this . breadcrumbsRenderer . host ) ; this . node . appendChild ( this . breadcrumbsContainer ) ; } } export namespace ToolbarAwareTabBar { export namespace Styles { export const TAB_BAR_CONTENT = 'p-TabBar-content' ; export const TAB_BAR_CONTENT_CONTAINER = 'p-TabBar-content-container' ; } } /**
 * A specialized tab bar for side areas.
 */ export class SideTabBar extends ScrollableTabBar { private static readonly DRAG_THRESHOLD = 5 ; /**
     * Emitted when a tab is added to the tab bar.
     */ readonly tabAdded = new Signal < this , { title : Title < Widget > ; } > ( this ) ; /**
     * Side panels can be collapsed by clicking on the currently selected tab. This signal is
     * emitted when the mouse is released on the selected tab without initiating a drag.
     */ readonly collapseRequested = new Signal < this , Title < Widget > > ( this ) ; private mouseData ? : { pressX : number ; pressY : number ; mouseDownTabIndex : number ; } ; constructor ( options ? ) { super ( options ) ; const hiddenContent = document . createElement ( 'ul' ) ; hiddenContent . className = HIDDEN_CONTENT_CLASS ; this . node . appendChild ( hiddenContent ) ; } /**
     * Tab bars of the left and right side panel are arranged vertically by rotating their labels.
     * Rotation is realized with the CSS `transform` property, which disrupts the browser's ability
     * to arrange the involved elements automatically. Therefore the elements are arranged explicitly
     * by the TabBarRenderer using inline `height` and `top` styles. However, the size of labels
     * must still be computed by the browser, so the rendering is performed in two steps: first the
     * tab bar is rendered horizontally inside a _hidden content node_, then it is rendered again
     * vertically inside the proper content node. After the first step, size information is gathered
     * from all labels so it can be applied during the second step.
     */ get hiddenContentNode ( ) : HTMLUListElement { return this . node . getElementsByClassName ( HIDDEN_CONTENT_CLASS ) [ 0 ] as HTMLUListElement ; } insertTab ( index , value ) { const result = super . insertTab ( index , value ) ; this . tabAdded . emit ( { title : result } ) ; return result ; } protected onAfterAttach ( msg ) { super . onAfterAttach ( msg ) ; this . renderTabBar ( ) ; this . node . addEventListener ( 'p-dragenter' , this ) ; this . node . addEventListener ( 'p-dragover' , this ) ; this . node . addEventListener ( 'p-dragleave' , this ) ; document . addEventListener ( 'p-drop' , this ) ; } protected onAfterDetach ( msg ) { super . onAfterDetach ( msg ) ; this . node . removeEventListener ( 'p-dragenter' , this ) ; this . node . removeEventListener ( 'p-dragover' , this ) ; this . node . removeEventListener ( 'p-dragleave' , this ) ; document . removeEventListener ( 'p-drop' , this ) ; } protected onUpdateRequest ( msg ) { this . renderTabBar ( ) ; if ( this . scrollBar ) { this . scrollBar . update ( ) ; } } /**
     * Render the tab bar in the _hidden content node_ (see `hiddenContentNode` for explanation),
     * then gather size information for labels and render it again in the proper content node.
     */ protected renderTabBar ( ) { if ( this . isAttached ) { this . renderTabs ( this . hiddenContentNode ) ; window . requestAnimationFrame ( ( ) => { const hiddenContent = this . hiddenContentNode ; const n = hiddenContent . children . length ; const renderData = new Array < Partial < SideBarRenderData > > ( n ) ; for ( let i = 0 ; i < n ; i ++ ) { const hiddenTab = hiddenContent . children [ i ] ; const tabStyle = window . getComputedStyle ( hiddenTab ) ; const rd = { paddingTop : parseFloat ( tabStyle . paddingTop ! ) , paddingBottom : parseFloat ( tabStyle . paddingBottom ! ) } ; const labelElements = hiddenTab . getElementsByClassName ( 'p-TabBar-tabLabel' ) ; if ( labelElements . length === 1 ) { const label = labelElements [ 0 ] ; rd . labelSize = { width : label . clientWidth , height : label . clientHeight } ; } const iconElements = hiddenTab . getElementsByClassName ( 'p-TabBar-tabIcon' ) ; if ( iconElements . length === 1 ) { const icon = iconElements [ 0 ] ; rd . iconSize = { width : icon . clientWidth , height : icon . clientHeight } ; } renderData [ i ] = rd ; } this . renderTabs ( this . contentNode , renderData ) ; } ) ; } } /**
     * Render the tab bar using the given DOM element as host. The optional `renderData` is forwarded
     * to the TabBarRenderer.
     */ protected renderTabs ( host , renderData ? ) { const titles = this . titles ; const n = titles . length ; const renderer = this . renderer as TabBarRenderer ; const currentTitle = this . currentTitle ; const content = new Array < VirtualElement > ( n ) ; for ( let i = 0 ; i < n ; i ++ ) { const title = titles [ i ] ; const current = title === currentTitle ; const zIndex = current ? n : n - i - 1 ; let rd ; if ( renderData && i < renderData . length ) { rd = { title , current , zIndex , ... renderData [ i ] } ; } else { rd = { title , current , zIndex } ; } content [ i ] = renderer . renderTab ( rd , true ) ; } VirtualDOM . render ( content , host ) ; } /**
     * The following event processing is used to generate `collapseRequested` signals
     * when the mouse goes up on the currently selected tab without too much movement
     * between `mousedown` and `mouseup`. The movement threshold is the same that
     * is used by the superclass to detect a drag event. The `allowDeselect` option
     * of the TabBar constructor cannot be used here because it is triggered when the
     * mouse goes down, and thus collides with dragging.
     */ handleEvent ( event ) { switch ( event . type ) { case 'mousedown' : this . onMouseDown ( event as MouseEvent ) ; super . handleEvent ( event ) ; break ; case 'mouseup' : super . handleEvent ( event ) ; this . onMouseUp ( event as MouseEvent ) ; break ; case 'mousemove' : this . onMouseMove ( event as MouseEvent ) ; super . handleEvent ( event ) ; break ; case 'p-dragenter' : this . onDragEnter ( event as IDragEvent ) ; break ; case 'p-dragover' : this . onDragOver ( event as IDragEvent ) ; break ; case 'p-dragleave' : case 'p-drop' : this . cancelViewContainerDND ( ) ; break ; default : super . handleEvent ( event ) ; } } private onMouseDown ( event ) { if ( event . button !== 0 || this . mouseData ) { return ; } const tabs = this . contentNode . children ; const index = ArrayExt . findFirstIndex ( tabs , tab => ElementExt . hitTest ( tab , event . clientX , event . clientY ) ) ; if ( index < 0 || index !== this . currentIndex ) { return ; } const icon = tabs [ index ] . querySelector ( this . renderer . closeIconSelector ) ; if ( icon && icon . contains ( event . target as HTMLElement ) ) { return ; } this . mouseData = { pressX : event . clientX , pressY : event . clientY , mouseDownTabIndex : index } ; } private onMouseUp ( event ) { if ( event . button !== 0 || ! this . mouseData ) { return ; } const mouseDownTabIndex = this . mouseData . mouseDownTabIndex ; this . mouseData = undefined ; const tabs = this . contentNode . children ; const index = ArrayExt . findFirstIndex ( tabs , tab => ElementExt . hitTest ( tab , event . clientX , event . clientY ) ) ; if ( index < 0 || index !== mouseDownTabIndex ) { return ; } this . collapseRequested . emit ( this . titles [ index ] ) ; } private onMouseMove ( event ) { if ( event . button !== 0 || ! this . mouseData ) { return ; } const data = this . mouseData ; const dx = Math . abs ( event . clientX - data . pressX ) ; const dy = Math . abs ( event . clientY - data . pressY ) ; const threshold = SideTabBar . DRAG_THRESHOLD ; if ( dx >= threshold || dy >= threshold ) { this . mouseData = undefined ; } } toCancelViewContainerDND = new DisposableCollection ( ) ; protected cancelViewContainerDND = ( ) => { this . toCancelViewContainerDND . dispose ( ) ; } ; /**
     * Handles `viewContainerPart` drag enter.
     */ protected onDragEnter = ( event ) => { this . cancelViewContainerDND ( ) ; if ( event . mimeData . getData ( 'application/vnd.phosphor.view-container-factory' ) ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; } } ; /**
     * Handle `viewContainerPart` drag over,
     * Defines the appropriate `drpAction` and opens the tab on which the mouse stands on for more than 800 ms.
     */ protected onDragOver = ( event ) => { const factory = event . mimeData . getData ( 'application/vnd.phosphor.view-container-factory' ) ; const widget = factory && factory ( ) ; if ( ! widget ) { event . dropAction = 'none' ; return ; } event . preventDefault ( ) ; event . stopPropagation ( ) ; if ( ! this . toCancelViewContainerDND . disposed ) { event . dropAction = event . proposedAction ; return ; } const { target , clientX , clientY } = event ; if ( target instanceof HTMLElement ) { if ( widget . options . disableDraggingToOtherContainers || widget . viewContainer . disableDNDBetweenContainers ) { event . dropAction = 'none' ; target . classList . add ( 'theia-cursor-no-drop' ) ; this . toCancelViewContainerDND . push ( Disposable . create ( ( ) => { target . classList . remove ( 'theia-cursor-no-drop' ) ; } ) ) ; } else { event . dropAction = event . proposedAction ; } const { top , bottom , left , right , height } = target . getBoundingClientRect ( ) ; const mouseOnTop = ( clientY - top ) < ( height / 2 ) ; const dropTargetClass = `drop-target-${ mouseOnTop ? 'top' : 'bottom' }` ; const tabs = this . contentNode . children ; const targetTab = ArrayExt . findFirstValue ( tabs , t => ElementExt . hitTest ( t , clientX , clientY ) ) ; if ( ! targetTab ) { return ; } targetTab . classList . add ( dropTargetClass ) ; this . toCancelViewContainerDND . push ( Disposable . create ( ( ) => { if ( targetTab ) { targetTab . classList . remove ( dropTargetClass ) ; } } ) ) ; const openTabTimer = setTimeout ( ( ) => { const title = this . titles . find ( t => ( this . renderer as TabBarRenderer ) . createTabId ( t ) === targetTab . id ) ; if ( title ) { const mouseStillOnTab = clientX >= left && clientX <= right && clientY >= top && clientY <= bottom ; if ( mouseStillOnTab ) { this . currentTitle = title ; } } } , 800 ) ; this . toCancelViewContainerDND . push ( Disposable . create ( ( ) => { clearTimeout ( openTabTimer ) ; } ) ) ; } } ; }