| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as Host from '../../core/host/host.js'; |
| import * as i18n from '../../core/i18n/i18n.js'; |
| |
| import * as ARIAUtils from './ARIAUtils.js'; |
| import {type ContextMenu} from './ContextMenu.js'; |
| import {Icon} from './Icon.js'; |
| |
| import {Events as TabbedPaneEvents, TabbedPane, type EventData} from './TabbedPane.js'; |
| |
| import {Toolbar, ToolbarMenuButton, type ToolbarItem} from './Toolbar.js'; |
| import {createTextChild} from './UIUtils.js'; |
| import {type TabbedViewLocation, type View, type ViewLocation, type ViewLocationResolver} from './View.js'; |
| import { |
| getRegisteredLocationResolvers, |
| getRegisteredViewExtensions, |
| maybeRemoveViewExtension, |
| registerLocationResolver, |
| registerViewExtension, |
| ViewLocationCategoryValues, |
| ViewLocationValues, |
| ViewPersistence, |
| type ViewRegistration, |
| resetViewRegistration, |
| } from './ViewRegistration.js'; |
| |
| import {VBox, type Widget, type WidgetElement} from './Widget.js'; |
| import viewContainersStyles from './viewContainers.css.legacy.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Aria label for the tab panel view container |
| *@example {Sensors} PH1 |
| */ |
| sPanel: '{PH1} panel', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('ui/legacy/ViewManager.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export const defaultOptionsForTabs = { |
| security: true, |
| }; |
| |
| export class PreRegisteredView implements View { |
| private readonly viewRegistration: ViewRegistration; |
| private widgetRequested: boolean; |
| |
| constructor(viewRegistration: ViewRegistration) { |
| this.viewRegistration = viewRegistration; |
| this.widgetRequested = false; |
| } |
| |
| title(): Common.UIString.LocalizedString { |
| return this.viewRegistration.title(); |
| } |
| |
| commandPrompt(): Common.UIString.LocalizedString { |
| return this.viewRegistration.commandPrompt(); |
| } |
| isCloseable(): boolean { |
| return this.viewRegistration.persistence === ViewPersistence.CLOSEABLE; |
| } |
| |
| isPreviewFeature(): boolean { |
| return Boolean(this.viewRegistration.isPreviewFeature); |
| } |
| |
| isTransient(): boolean { |
| return this.viewRegistration.persistence === ViewPersistence.TRANSIENT; |
| } |
| |
| viewId(): string { |
| return this.viewRegistration.id; |
| } |
| |
| location(): ViewLocationValues|undefined { |
| return this.viewRegistration.location; |
| } |
| |
| order(): number|undefined { |
| return this.viewRegistration.order; |
| } |
| |
| settings(): string[]|undefined { |
| return this.viewRegistration.settings; |
| } |
| |
| tags(): string|undefined { |
| if (this.viewRegistration.tags) { |
| // Get localized keys and separate by null character to prevent fuzzy matching from matching across them. |
| return this.viewRegistration.tags.map(tag => tag()).join('\0'); |
| } |
| return undefined; |
| } |
| |
| persistence(): ViewPersistence|undefined { |
| return this.viewRegistration.persistence; |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| async toolbarItems(): Promise<any> { |
| if (this.viewRegistration.hasToolbar) { |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| return this.widget().then(widget => (widget as any).toolbarItems()); |
| } |
| return []; |
| } |
| |
| async widget(): Promise<Widget> { |
| this.widgetRequested = true; |
| return this.viewRegistration.loadView(); |
| } |
| |
| async disposeView(): Promise<void> { |
| if (!this.widgetRequested) { |
| return; |
| } |
| |
| const widget = await this.widget(); |
| await widget.ownerViewDisposed(); |
| } |
| |
| experiment(): string|undefined { |
| return this.viewRegistration.experiment; |
| } |
| |
| condition(): string|undefined { |
| return this.viewRegistration.condition; |
| } |
| } |
| |
| let viewManagerInstance: ViewManager|undefined; |
| |
| export class ViewManager { |
| readonly views: Map<string, View>; |
| private readonly locationNameByViewId: Map<string, string>; |
| private readonly locationOverrideSetting: Common.Settings.Setting<{[key: string]: string}>; |
| |
| private constructor() { |
| this.views = new Map(); |
| this.locationNameByViewId = new Map(); |
| |
| // Read override setting for location |
| this.locationOverrideSetting = Common.Settings.Settings.instance().createSetting('viewsLocationOverride', {}); |
| const preferredExtensionLocations = this.locationOverrideSetting.get(); |
| |
| // Views may define their initial ordering within a location. When the user has not reordered, we use the |
| // default ordering as defined by the views themselves. |
| |
| const viewsByLocation = new Map<ViewLocationValues|'none', PreRegisteredView[]>(); |
| for (const view of getRegisteredViewExtensions()) { |
| const location = view.location() || 'none'; |
| const views = viewsByLocation.get(location) || []; |
| views.push(view); |
| viewsByLocation.set(location, views); |
| } |
| |
| let sortedViewExtensions: PreRegisteredView[] = []; |
| for (const views of viewsByLocation.values()) { |
| views.sort((firstView, secondView) => { |
| const firstViewOrder = firstView.order(); |
| const secondViewOrder = secondView.order(); |
| if (firstViewOrder !== undefined && secondViewOrder !== undefined) { |
| return firstViewOrder - secondViewOrder; |
| } |
| return 0; |
| }); |
| sortedViewExtensions = sortedViewExtensions.concat(views); |
| } |
| |
| for (const view of sortedViewExtensions) { |
| const viewId = view.viewId(); |
| const location = view.location(); |
| if (this.views.has(viewId)) { |
| throw new Error(`Duplicate view id '${viewId}'`); |
| } |
| this.views.set(viewId, view); |
| // Use the preferred user location if available |
| const locationName = preferredExtensionLocations[viewId] || location; |
| this.locationNameByViewId.set(viewId, locationName as string); |
| } |
| } |
| |
| static instance(opts: { |
| forceNew: boolean|null, |
| } = {forceNew: null}): ViewManager { |
| const {forceNew} = opts; |
| if (!viewManagerInstance || forceNew) { |
| viewManagerInstance = new ViewManager(); |
| } |
| |
| return viewManagerInstance; |
| } |
| |
| static removeInstance(): void { |
| viewManagerInstance = undefined; |
| } |
| |
| static createToolbar(toolbarItems: ToolbarItem[]): Element|null { |
| if (!toolbarItems.length) { |
| return null; |
| } |
| const toolbar = new Toolbar(''); |
| for (const item of toolbarItems) { |
| toolbar.appendToolbarItem(item); |
| } |
| return toolbar.element; |
| } |
| |
| locationNameForViewId(viewId: string): string { |
| const locationName = this.locationNameByViewId.get(viewId); |
| if (!locationName) { |
| throw new Error(`No location name for view with id ${viewId}`); |
| } |
| return locationName; |
| } |
| |
| /** |
| * Moves a view to a new location |
| */ |
| moveView(viewId: string, locationName: string, options?: { |
| shouldSelectTab: (boolean), |
| overrideSaving: (boolean), |
| }): void { |
| const defaultOptions = {shouldSelectTab: true, overrideSaving: false}; |
| const {shouldSelectTab, overrideSaving} = options || defaultOptions; |
| if (!viewId || !locationName) { |
| return; |
| } |
| |
| const view = this.view(viewId); |
| if (!view) { |
| return; |
| } |
| |
| if (!overrideSaving) { |
| // Update the inner map of locations |
| this.locationNameByViewId.set(viewId, locationName); |
| |
| // Update the settings of location overwrites |
| const locations = this.locationOverrideSetting.get(); |
| locations[viewId] = locationName; |
| this.locationOverrideSetting.set(locations); |
| } |
| |
| // Find new location and show view there |
| void this.resolveLocation(locationName).then(location => { |
| if (!location) { |
| throw new Error('Move view: Could not resolve location for view: ' + viewId); |
| } |
| location.reveal(); |
| return location.showView(view, undefined, /* userGesture*/ true, /* omitFocus*/ false, shouldSelectTab); |
| }); |
| } |
| |
| revealView(view: View): Promise<void> { |
| const location = locationForView.get(view); |
| if (!location) { |
| return Promise.resolve(); |
| } |
| location.reveal(); |
| return location.showView(view); |
| } |
| |
| /** |
| * Show view in location |
| */ |
| showViewInLocation(viewId: string, locationName: string, shouldSelectTab: boolean|undefined = true): void { |
| this.moveView(viewId, locationName, { |
| shouldSelectTab, |
| overrideSaving: true, |
| }); |
| } |
| |
| view(viewId: string): View { |
| const view = this.views.get(viewId); |
| if (!view) { |
| throw new Error(`No view with id ${viewId} found!`); |
| } |
| return view; |
| } |
| |
| materializedWidget(viewId: string): Widget|null { |
| const view = this.view(viewId); |
| if (!view) { |
| return null; |
| } |
| return widgetForView.get(view) || null; |
| } |
| |
| showView(viewId: string, userGesture?: boolean, omitFocus?: boolean): Promise<void> { |
| const view = this.views.get(viewId); |
| if (!view) { |
| console.error('Could not find view for id: \'' + viewId + '\' ' + new Error().stack); |
| return Promise.resolve(); |
| } |
| |
| const locationName = this.locationNameByViewId.get(viewId); |
| |
| const location = locationForView.get(view); |
| if (location) { |
| location.reveal(); |
| return location.showView(view, undefined, userGesture, omitFocus); |
| } |
| |
| return this.resolveLocation(locationName).then(location => { |
| if (!location) { |
| throw new Error('Could not resolve location for view: ' + viewId); |
| } |
| location.reveal(); |
| return location.showView(view, undefined, userGesture, omitFocus); |
| }); |
| } |
| |
| async resolveLocation(location?: string): Promise<Location|null> { |
| if (!location) { |
| return Promise.resolve(null) as Promise<Location|null>; |
| } |
| const registeredResolvers = getRegisteredLocationResolvers().filter(resolver => resolver.name === location); |
| |
| if (registeredResolvers.length > 1) { |
| throw new Error('Duplicate resolver for location: ' + location); |
| } |
| if (registeredResolvers.length) { |
| const resolver = (await registeredResolvers[0].loadResolver() as ViewLocationResolver); |
| return resolver.resolveLocation(location) as Location | null; |
| } |
| throw new Error('Unresolved location: ' + location); |
| } |
| |
| createTabbedLocation( |
| revealCallback?: (() => void), location?: string, restoreSelection?: boolean, allowReorder?: boolean, |
| defaultTab?: string|null): TabbedViewLocation { |
| return new _TabbedLocation(this, revealCallback, location, restoreSelection, allowReorder, defaultTab); |
| } |
| |
| createStackLocation(revealCallback?: (() => void), location?: string): ViewLocation { |
| return new _StackLocation(this, revealCallback, location); |
| } |
| |
| hasViewsForLocation(location: string): boolean { |
| return Boolean(this.viewsForLocation(location).length); |
| } |
| |
| viewsForLocation(location: string): View[] { |
| const result = []; |
| for (const [id, view] of this.views.entries()) { |
| if (this.locationNameByViewId.get(id) === location) { |
| result.push(view); |
| } |
| } |
| return result; |
| } |
| } |
| |
| const widgetForView = new WeakMap<View, Widget>(); |
| |
| export class ContainerWidget extends VBox { |
| private readonly view: View; |
| private materializePromise?: Promise<void[]>; |
| |
| constructor(view: View) { |
| super(); |
| this.element.classList.add('flex-auto', 'view-container', 'overflow-auto'); |
| this.view = view; |
| this.element.tabIndex = -1; |
| ARIAUtils.markAsTabpanel(this.element); |
| ARIAUtils.setAccessibleName(this.element, i18nString(UIStrings.sPanel, {PH1: view.title()})); |
| this.setDefaultFocusedElement(this.element); |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| materialize(): Promise<any> { |
| if (this.materializePromise) { |
| return this.materializePromise; |
| } |
| const promises = []; |
| // TODO(crbug.com/1006759): Transform to async-await |
| promises.push(this.view.toolbarItems().then(toolbarItems => { |
| const toolbarElement = ViewManager.createToolbar(toolbarItems); |
| if (toolbarElement) { |
| this.element.insertBefore(toolbarElement, this.element.firstChild); |
| } |
| })); |
| promises.push(this.view.widget().then(widget => { |
| // Move focus from |this| to loaded |widget| if any. |
| const shouldFocus = this.element.hasFocus(); |
| this.setDefaultFocusedElement(null); |
| widgetForView.set(this.view, widget); |
| widget.show(this.element); |
| if (shouldFocus) { |
| widget.focus(); |
| } |
| })); |
| this.materializePromise = Promise.all(promises); |
| return this.materializePromise; |
| } |
| |
| wasShown(): void { |
| void this.materialize().then(() => { |
| const widget = widgetForView.get(this.view); |
| if (widget) { |
| widget.show(this.element); |
| this.wasShownForTest(); |
| } |
| }); |
| } |
| |
| private wasShownForTest(): void { |
| // This method is sniffed in tests. |
| } |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| export class _ExpandableContainerWidget extends VBox { |
| private titleElement: HTMLDivElement; |
| private readonly titleExpandIcon: Icon; |
| private readonly view: View; |
| private widget?: Widget; |
| private materializePromise?: Promise<void[]>; |
| |
| constructor(view: View) { |
| super(true); |
| this.element.classList.add('flex-none'); |
| this.registerRequiredCSS(viewContainersStyles); |
| |
| this.titleElement = document.createElement('div'); |
| this.titleElement.classList.add('expandable-view-title'); |
| ARIAUtils.markAsTreeitem(this.titleElement); |
| this.titleExpandIcon = Icon.create('smallicon-triangle-right', 'title-expand-icon'); |
| this.titleElement.appendChild(this.titleExpandIcon); |
| const titleText = view.title(); |
| createTextChild(this.titleElement, titleText); |
| ARIAUtils.setAccessibleName(this.titleElement, titleText); |
| ARIAUtils.setExpanded(this.titleElement, false); |
| this.titleElement.tabIndex = 0; |
| self.onInvokeElement(this.titleElement, this.toggleExpanded.bind(this)); |
| this.titleElement.addEventListener('keydown', this.onTitleKeyDown.bind(this), false); |
| this.contentElement.insertBefore(this.titleElement, this.contentElement.firstChild); |
| |
| ARIAUtils.setControls(this.titleElement, this.contentElement.createChild('slot')); |
| this.view = view; |
| expandableContainerForView.set(view, this); |
| } |
| |
| wasShown(): void { |
| if (this.widget && this.materializePromise) { |
| void this.materializePromise.then(() => { |
| if (this.titleElement.classList.contains('expanded') && this.widget) { |
| this.widget.show(this.element); |
| } |
| }); |
| } |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| private materialize(): Promise<any> { |
| if (this.materializePromise) { |
| return this.materializePromise; |
| } |
| // TODO(crbug.com/1006759): Transform to async-await |
| const promises = []; |
| promises.push(this.view.toolbarItems().then(toolbarItems => { |
| const toolbarElement = ViewManager.createToolbar(toolbarItems); |
| if (toolbarElement) { |
| this.titleElement.appendChild(toolbarElement); |
| } |
| })); |
| promises.push(this.view.widget().then(widget => { |
| this.widget = widget; |
| widgetForView.set(this.view, widget); |
| widget.show(this.element); |
| })); |
| this.materializePromise = Promise.all(promises); |
| return this.materializePromise; |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| expand(): Promise<any> { |
| if (this.titleElement.classList.contains('expanded')) { |
| return this.materialize(); |
| } |
| this.titleElement.classList.add('expanded'); |
| ARIAUtils.setExpanded(this.titleElement, true); |
| this.titleExpandIcon.setIconType('smallicon-triangle-down'); |
| return this.materialize().then(() => { |
| if (this.widget) { |
| this.widget.show(this.element); |
| } |
| }); |
| } |
| |
| private collapse(): void { |
| if (!this.titleElement.classList.contains('expanded')) { |
| return; |
| } |
| this.titleElement.classList.remove('expanded'); |
| ARIAUtils.setExpanded(this.titleElement, false); |
| this.titleExpandIcon.setIconType('smallicon-triangle-right'); |
| void this.materialize().then(() => { |
| if (this.widget) { |
| this.widget.detach(); |
| } |
| }); |
| } |
| |
| private toggleExpanded(event: Event): void { |
| if (event.type === 'keydown' && event.target !== this.titleElement) { |
| return; |
| } |
| if (this.titleElement.classList.contains('expanded')) { |
| this.collapse(); |
| } else { |
| void this.expand(); |
| } |
| } |
| |
| private onTitleKeyDown(event: Event): void { |
| if (event.target !== this.titleElement) { |
| return; |
| } |
| const keyEvent = (event as KeyboardEvent); |
| if (keyEvent.key === 'ArrowLeft') { |
| this.collapse(); |
| } else if (keyEvent.key === 'ArrowRight') { |
| if (!this.titleElement.classList.contains('expanded')) { |
| void this.expand(); |
| } else if (this.widget) { |
| this.widget.focus(); |
| } |
| } |
| } |
| } |
| |
| const expandableContainerForView = new WeakMap<View, _ExpandableContainerWidget>(); |
| |
| class Location { |
| protected readonly manager: ViewManager; |
| private readonly revealCallback: (() => void)|undefined; |
| private readonly widgetInternal: Widget; |
| |
| constructor(manager: ViewManager, widget: Widget, revealCallback?: (() => void)) { |
| this.manager = manager; |
| this.revealCallback = revealCallback; |
| this.widgetInternal = widget; |
| } |
| |
| widget(): Widget { |
| return this.widgetInternal; |
| } |
| |
| reveal(): void { |
| if (this.revealCallback) { |
| this.revealCallback(); |
| } |
| } |
| |
| showView( |
| _view: View, _insertBefore?: View|null, _userGesture?: boolean, _omitFocus?: boolean, |
| _shouldSelectTab?: boolean): Promise<void> { |
| throw new Error('not implemented'); |
| } |
| |
| removeView(_view: View): void { |
| throw new Error('not implemented'); |
| } |
| } |
| |
| const locationForView = new WeakMap<View, Location>(); |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| export class _TabbedLocation extends Location implements TabbedViewLocation { |
| private tabbedPaneInternal: TabbedPane; |
| private readonly allowReorder: boolean|undefined; |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| private readonly closeableTabSetting: Common.Settings.Setting<any>; |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| private readonly tabOrderSetting: Common.Settings.Setting<any>; |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| private readonly lastSelectedTabSetting: Common.Settings.Setting<any>|undefined; |
| private readonly defaultTab: string|null|undefined; |
| private readonly views: Map<string, View>; |
| |
| constructor( |
| manager: ViewManager, revealCallback?: (() => void), location?: string, restoreSelection?: boolean, |
| allowReorder?: boolean, defaultTab?: string|null) { |
| const tabbedPane = new TabbedPane(); |
| if (allowReorder) { |
| tabbedPane.setAllowTabReorder(true); |
| } |
| |
| super(manager, tabbedPane, revealCallback); |
| this.tabbedPaneInternal = tabbedPane; |
| this.allowReorder = allowReorder; |
| |
| this.tabbedPaneInternal.addEventListener(TabbedPaneEvents.TabSelected, this.tabSelected, this); |
| this.tabbedPaneInternal.addEventListener(TabbedPaneEvents.TabClosed, this.tabClosed, this); |
| |
| this.closeableTabSetting = Common.Settings.Settings.instance().createSetting('closeableTabs', {}); |
| // As we give tabs the capability to be closed we also need to add them to the setting so they are still open |
| // until the user decide to close them |
| this.setOrUpdateCloseableTabsSetting(); |
| |
| this.tabOrderSetting = Common.Settings.Settings.instance().createSetting(location + '-tabOrder', {}); |
| this.tabbedPaneInternal.addEventListener(TabbedPaneEvents.TabOrderChanged, this.persistTabOrder, this); |
| if (restoreSelection) { |
| this.lastSelectedTabSetting = Common.Settings.Settings.instance().createSetting(location + '-selectedTab', ''); |
| } |
| this.defaultTab = defaultTab; |
| |
| this.views = new Map(); |
| |
| if (location) { |
| this.appendApplicableItems(location); |
| } |
| } |
| |
| private setOrUpdateCloseableTabsSetting(): void { |
| // Update the setting value, we respect the closed state decided by the user |
| // and append the new tabs with value of true so they are shown open |
| const tabs = this.closeableTabSetting.get(); |
| const newClosable = Object.assign( |
| { |
| ...defaultOptionsForTabs, |
| }, |
| tabs); |
| this.closeableTabSetting.set(newClosable); |
| } |
| |
| widget(): Widget { |
| return this.tabbedPaneInternal; |
| } |
| |
| tabbedPane(): TabbedPane { |
| return this.tabbedPaneInternal; |
| } |
| |
| enableMoreTabsButton(): ToolbarMenuButton { |
| const moreTabsButton = new ToolbarMenuButton(this.appendTabsToMenu.bind(this)); |
| this.tabbedPaneInternal.leftToolbar().appendToolbarItem(moreTabsButton); |
| this.tabbedPaneInternal.disableOverflowMenu(); |
| return moreTabsButton; |
| } |
| |
| appendApplicableItems(locationName: string): void { |
| const views = this.manager.viewsForLocation(locationName); |
| if (this.allowReorder) { |
| let i = 0; |
| const persistedOrders = this.tabOrderSetting.get(); |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| const orders = new Map<string, any>(); |
| for (const view of views) { |
| orders.set(view.viewId(), persistedOrders[view.viewId()] || (++i) * _TabbedLocation.orderStep); |
| } |
| views.sort((a, b) => orders.get(a.viewId()) - orders.get(b.viewId())); |
| } |
| |
| for (const view of views) { |
| const id = view.viewId(); |
| this.views.set(id, view); |
| locationForView.set(view, this); |
| if (view.isTransient()) { |
| continue; |
| } |
| if (!view.isCloseable()) { |
| this.appendTab(view); |
| } else if (this.closeableTabSetting.get()[id]) { |
| this.appendTab(view); |
| } |
| } |
| |
| // If a default tab was provided we open or select it |
| if (this.defaultTab) { |
| if (this.tabbedPaneInternal.hasTab(this.defaultTab)) { |
| // If the tabbed pane already has the tab we just have to select it |
| this.tabbedPaneInternal.selectTab(this.defaultTab); |
| } else { |
| // If the tab is not present already it can be because: |
| // it doesn't correspond to this tabbed location |
| // or because it is closed |
| const view = Array.from(this.views.values()).find(view => view.viewId() === this.defaultTab); |
| if (view) { |
| // defaultTab is indeed part of the views for this tabbed location |
| void this.showView(view); |
| } |
| } |
| } else if (this.lastSelectedTabSetting && this.tabbedPaneInternal.hasTab(this.lastSelectedTabSetting.get())) { |
| this.tabbedPaneInternal.selectTab(this.lastSelectedTabSetting.get()); |
| } |
| } |
| |
| private appendTabsToMenu(contextMenu: ContextMenu): void { |
| const views = Array.from(this.views.values()); |
| views.sort((viewa, viewb) => viewa.title().localeCompare(viewb.title())); |
| for (const view of views) { |
| const title = view.title(); |
| |
| if (view.viewId() === 'issues-pane') { |
| contextMenu.defaultSection().appendItem(title, () => { |
| Host.userMetrics.issuesPanelOpenedFrom(Host.UserMetrics.IssueOpener.HamburgerMenu); |
| void this.showView(view, undefined, true); |
| }); |
| continue; |
| } |
| |
| contextMenu.defaultSection().appendItem(title, this.showView.bind(this, view, undefined, true)); |
| } |
| } |
| |
| private appendTab(view: View, index?: number): void { |
| this.tabbedPaneInternal.appendTab( |
| view.viewId(), view.title(), new ContainerWidget(view), undefined, false, |
| view.isCloseable() || view.isTransient(), view.isPreviewFeature(), index); |
| } |
| |
| appendView(view: View, insertBefore?: View|null): void { |
| if (this.tabbedPaneInternal.hasTab(view.viewId())) { |
| return; |
| } |
| const oldLocation = locationForView.get(view); |
| if (oldLocation && oldLocation !== this) { |
| oldLocation.removeView(view); |
| } |
| locationForView.set(view, this); |
| this.manager.views.set(view.viewId(), view); |
| this.views.set(view.viewId(), view); |
| let index: number|undefined = undefined; |
| const tabIds = this.tabbedPaneInternal.tabIds(); |
| if (this.allowReorder) { |
| const orderSetting = this.tabOrderSetting.get(); |
| const order = orderSetting[view.viewId()]; |
| for (let i = 0; order && i < tabIds.length; ++i) { |
| if (orderSetting[tabIds[i]] && orderSetting[tabIds[i]] > order) { |
| index = i; |
| break; |
| } |
| } |
| } else if (insertBefore) { |
| for (let i = 0; i < tabIds.length; ++i) { |
| if (tabIds[i] === insertBefore.viewId()) { |
| index = i; |
| break; |
| } |
| } |
| } |
| this.appendTab(view, index); |
| |
| if (view.isCloseable()) { |
| const tabs = this.closeableTabSetting.get(); |
| const tabId = view.viewId(); |
| if (!tabs[tabId]) { |
| tabs[tabId] = true; |
| this.closeableTabSetting.set(tabs); |
| } |
| } |
| this.persistTabOrder(); |
| } |
| |
| async showView( |
| view: View, insertBefore?: View|null, userGesture?: boolean, omitFocus?: boolean, |
| shouldSelectTab: boolean|undefined = true): Promise<void> { |
| this.appendView(view, insertBefore); |
| if (shouldSelectTab) { |
| this.tabbedPaneInternal.selectTab(view.viewId(), userGesture); |
| } |
| if (!omitFocus) { |
| this.tabbedPaneInternal.focus(); |
| } |
| const widget = (this.tabbedPaneInternal.tabView(view.viewId()) as ContainerWidget); |
| await widget.materialize(); |
| } |
| |
| removeView(view: View): void { |
| if (!this.tabbedPaneInternal.hasTab(view.viewId())) { |
| return; |
| } |
| |
| locationForView.delete(view); |
| this.manager.views.delete(view.viewId()); |
| this.tabbedPaneInternal.closeTab(view.viewId()); |
| this.views.delete(view.viewId()); |
| } |
| |
| private tabSelected(event: Common.EventTarget.EventTargetEvent<EventData>): void { |
| const {tabId} = event.data; |
| if (this.lastSelectedTabSetting && event.data['isUserGesture']) { |
| this.lastSelectedTabSetting.set(tabId); |
| } |
| } |
| |
| private tabClosed(event: Common.EventTarget.EventTargetEvent<EventData>): void { |
| const {tabId} = event.data; |
| const tabs = this.closeableTabSetting.get(); |
| if (tabs[tabId]) { |
| tabs[tabId] = false; |
| this.closeableTabSetting.set(tabs); |
| } |
| const view = this.views.get(tabId); |
| if (view) { |
| void view.disposeView(); |
| } |
| } |
| |
| private persistTabOrder(): void { |
| const tabIds = this.tabbedPaneInternal.tabIds(); |
| const tabOrders: { |
| [x: string]: number, |
| } = {}; |
| for (let i = 0; i < tabIds.length; i++) { |
| tabOrders[tabIds[i]] = (i + 1) * _TabbedLocation.orderStep; |
| } |
| |
| const oldTabOrder = this.tabOrderSetting.get(); |
| const oldTabArray = Object.keys(oldTabOrder); |
| oldTabArray.sort((a, b) => oldTabOrder[a] - oldTabOrder[b]); |
| let lastOrder = 0; |
| for (const key of oldTabArray) { |
| if (key in tabOrders) { |
| lastOrder = tabOrders[key]; |
| continue; |
| } |
| tabOrders[key] = ++lastOrder; |
| } |
| this.tabOrderSetting.set(tabOrders); |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| getCloseableTabSetting(): Common.Settings.Setting<any> { |
| return this.closeableTabSetting.get(); |
| } |
| |
| static orderStep = 10; // Keep in sync with descriptors. |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| class _StackLocation extends Location implements ViewLocation { |
| private readonly vbox: VBox; |
| private readonly expandableContainers: Map<string, _ExpandableContainerWidget>; |
| |
| constructor(manager: ViewManager, revealCallback?: (() => void), location?: string) { |
| const vbox = new VBox(); |
| super(manager, vbox, revealCallback); |
| this.vbox = vbox; |
| ARIAUtils.markAsTree(vbox.element); |
| |
| this.expandableContainers = new Map(); |
| |
| if (location) { |
| this.appendApplicableItems(location); |
| } |
| } |
| |
| appendView(view: View, insertBefore?: View|null): void { |
| const oldLocation = locationForView.get(view); |
| if (oldLocation && oldLocation !== this) { |
| oldLocation.removeView(view); |
| } |
| |
| let container = this.expandableContainers.get(view.viewId()); |
| if (!container) { |
| locationForView.set(view, this); |
| this.manager.views.set(view.viewId(), view); |
| container = new _ExpandableContainerWidget(view); |
| let beforeElement: (WidgetElement|null)|null = null; |
| if (insertBefore) { |
| const beforeContainer = expandableContainerForView.get(insertBefore); |
| beforeElement = beforeContainer ? beforeContainer.element : null; |
| } |
| container.show(this.vbox.contentElement, beforeElement); |
| this.expandableContainers.set(view.viewId(), container); |
| } |
| } |
| |
| async showView(view: View, insertBefore?: View|null): Promise<void> { |
| this.appendView(view, insertBefore); |
| const container = this.expandableContainers.get(view.viewId()); |
| if (container) { |
| await container.expand(); |
| } |
| } |
| |
| removeView(view: View): void { |
| const container = this.expandableContainers.get(view.viewId()); |
| if (!container) { |
| return; |
| } |
| |
| container.detach(); |
| this.expandableContainers.delete(view.viewId()); |
| locationForView.delete(view); |
| this.manager.views.delete(view.viewId()); |
| } |
| |
| appendApplicableItems(locationName: string): void { |
| for (const view of this.manager.viewsForLocation(locationName)) { |
| this.appendView(view); |
| } |
| } |
| } |
| |
| export { |
| ViewRegistration, |
| ViewPersistence, |
| getRegisteredViewExtensions, |
| maybeRemoveViewExtension, |
| registerViewExtension, |
| ViewLocationValues, |
| getRegisteredLocationResolvers, |
| registerLocationResolver, |
| ViewLocationCategoryValues, |
| resetViewRegistration, |
| }; |