| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'chrome://resources/cr_elements/cr_drawer/cr_drawer.js'; |
| import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render_lit.js'; |
| import 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.js'; |
| import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js'; |
| import 'chrome://resources/cr_elements/cr_view_manager/cr_view_manager.js'; |
| import './activity_log/activity_log.js'; |
| import './detail_view.js'; |
| import './drop_overlay.js'; |
| import './error_page.js'; |
| import './install_warnings_dialog.js'; |
| import './item_list.js'; |
| import './item_util.js'; |
| import './keyboard_shortcuts.js'; |
| import './load_error.js'; |
| import './options_dialog.js'; |
| import './sidebar.js'; |
| import './site_permissions/site_permissions.js'; |
| import './site_permissions/site_permissions_by_site.js'; |
| import './toolbar.js'; |
| |
| import {CrContainerShadowMixinLit} from 'chrome://resources/cr_elements/cr_container_shadow_mixin_lit.js'; |
| import {getToastManager} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.js'; |
| import type {CrViewManagerElement} from 'chrome://resources/cr_elements/cr_view_manager/cr_view_manager.js'; |
| import {I18nMixinLit} from 'chrome://resources/cr_elements/i18n_mixin_lit.js'; |
| import {assert, assertNotReached} from 'chrome://resources/js/assert.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; |
| import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js'; |
| import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| |
| import type {ActivityLogExtensionPlaceholder} from './activity_log/activity_log.js'; |
| import type {ExtensionsDetailViewElement} from './detail_view.js'; |
| import type {ExtensionsItemListElement} from './item_list.js'; |
| import {TOAST_DURATION_MS} from './item_util.js'; |
| import {getCss} from './manager.css.js'; |
| import {getHtml} from './manager.html.js'; |
| import type {PageState} from './navigation_helper.js'; |
| import {Dialog, navigation, Page} from './navigation_helper.js'; |
| import {Service} from './service.js'; |
| import type {ServiceInterface} from './service.js'; |
| import type {ExtensionsToolbarElement} from './toolbar.js'; |
| |
| /** |
| * Compares two extensions to determine which should come first in the list. |
| */ |
| function compareExtensions( |
| a: chrome.developerPrivate.ExtensionInfo, |
| b: chrome.developerPrivate.ExtensionInfo): number { |
| function compare(x: string, y: string): number { |
| return x < y ? -1 : (x > y ? 1 : 0); |
| } |
| function compareLocation( |
| x: chrome.developerPrivate.ExtensionInfo, |
| y: chrome.developerPrivate.ExtensionInfo): number { |
| if (x.location === y.location) { |
| return 0; |
| } |
| if (x.location === chrome.developerPrivate.Location.UNPACKED) { |
| return -1; |
| } |
| if (y.location === chrome.developerPrivate.Location.UNPACKED) { |
| return 1; |
| } |
| return 0; |
| } |
| return compareLocation(a, b) || |
| compare(a.name.toLowerCase(), b.name.toLowerCase()) || |
| compare(a.id, b.id); |
| } |
| |
| declare global { |
| interface HTMLElementEventMap { |
| 'load-error': CustomEvent<Error|chrome.developerPrivate.LoadError>; |
| } |
| } |
| |
| export interface ExtensionsManagerElement { |
| $: { |
| toolbar: ExtensionsToolbarElement, |
| viewManager: CrViewManagerElement, |
| 'items-list': ExtensionsItemListElement, |
| }; |
| } |
| |
| // TODO(crbug.com/40270029): Always show a top shadow for the DETAILS, ERRORS and |
| // SITE_PERMISSIONS_ALL_SITES pages. |
| const ExtensionsManagerElementBase = |
| I18nMixinLit(CrContainerShadowMixinLit(CrLitElement)); |
| |
| export class ExtensionsManagerElement extends ExtensionsManagerElementBase { |
| static get is() { |
| return 'extensions-manager'; |
| } |
| |
| static override get styles() { |
| return getCss(); |
| } |
| |
| override render() { |
| return getHtml.bind(this)(); |
| } |
| |
| static override get properties() { |
| return { |
| canLoadUnpacked: {type: Boolean}, |
| delegate: {type: Object}, |
| inDevMode: {type: Boolean}, |
| isMv2DeprecationNoticeDismissed: {type: Boolean}, |
| showActivityLog: {type: Boolean}, |
| enableEnhancedSiteControls: {type: Boolean}, |
| devModeControlledByPolicy: {type: Boolean}, |
| isChildAccount_: {type: Boolean}, |
| incognitoAvailable_: {type: Boolean}, |
| filter: {type: String}, |
| |
| /** |
| * The item currently displayed in the error subpage. We use a separate |
| * item for different pages (rather than a single subpageItem_ property) |
| * so that hidden subpages don't update when an item updates. That is, we |
| * don't want the details view subpage to update when the item shown in |
| * the errors page updates, and vice versa. |
| */ |
| errorPageItem_: {type: Object}, |
| |
| /** |
| * The item currently displayed in the details view subpage. See also |
| * errorPageItem_. |
| */ |
| detailViewItem_: {type: Object}, |
| |
| /** |
| * The item that provides some information about the current extension |
| * for the activity log view subpage. See also errorPageItem_. |
| */ |
| activityLogItem_: {type: Object}, |
| |
| extensions_: {type: Array}, |
| apps_: {type: Array}, |
| |
| /** |
| * Prevents page content from showing before data is first loaded. |
| */ |
| didInitPage_: {type: Boolean}, |
| |
| narrow_: {type: Boolean}, |
| |
| showDrawer_: {type: Boolean}, |
| showLoadErrorDialog_: {type: Boolean}, |
| showInstallWarningsDialog_: {type: Boolean}, |
| installWarnings_: {type: Array}, |
| showOptionsDialog_: {type: Boolean}, |
| |
| /** |
| * Whether the last page the user navigated from was the activity log |
| * page. |
| */ |
| fromActivityLog_: {type: Boolean}, |
| }; |
| } |
| |
| accessor canLoadUnpacked: boolean = false; |
| accessor delegate: ServiceInterface = Service.getInstance(); |
| accessor inDevMode: boolean = loadTimeData.getBoolean('inDevMode'); |
| accessor isMv2DeprecationNoticeDismissed: boolean = |
| loadTimeData.getBoolean('MV2DeprecationNoticeDismissed'); |
| accessor showActivityLog: boolean = |
| loadTimeData.getBoolean('showActivityLog'); |
| accessor enableEnhancedSiteControls: boolean = |
| loadTimeData.getBoolean('enableEnhancedSiteControls'); |
| accessor devModeControlledByPolicy: boolean = false; |
| protected accessor isChildAccount_: boolean = false; |
| protected accessor incognitoAvailable_: boolean = false; |
| accessor filter: string = ''; |
| protected accessor errorPageItem_: chrome.developerPrivate.ExtensionInfo| |
| undefined; |
| protected accessor detailViewItem_: chrome.developerPrivate.ExtensionInfo| |
| undefined; |
| protected accessor activityLogItem_: chrome.developerPrivate.ExtensionInfo| |
| ActivityLogExtensionPlaceholder|undefined; |
| protected accessor extensions_: chrome.developerPrivate.ExtensionInfo[] = []; |
| protected accessor apps_: chrome.developerPrivate.ExtensionInfo[] = []; |
| protected accessor didInitPage_: boolean = false; |
| protected accessor narrow_: boolean = false; |
| protected accessor showDrawer_: boolean = false; |
| protected accessor showLoadErrorDialog_: boolean = false; |
| protected accessor showInstallWarningsDialog_: boolean = false; |
| protected accessor installWarnings_: string[]|null = null; |
| protected accessor showOptionsDialog_: boolean = false; |
| protected accessor fromActivityLog_: boolean = false; |
| |
| /** |
| * A promise resolver for any external files waiting for initPage_ to be |
| * called after the extensions info has been fetched. |
| */ |
| private pageInitializedResolver_: PromiseResolver<void> = |
| new PromiseResolver<void>(); |
| |
| /** |
| * The current page being shown. Default to null, and initPage_ will figure |
| * out the initial page based on url. |
| */ |
| private currentPage_: PageState|null = null; |
| |
| /** |
| * The ID of the listener on |navigation|. Stored so that the |
| * listener can be removed when this element is detached (happens in tests). |
| */ |
| private navigationListener_: number|null = null; |
| |
| override firstUpdated(changedProperties: PropertyValues<this>) { |
| super.firstUpdated(changedProperties); |
| |
| this.addEventListener('load-error', this.onLoadError_); |
| this.addEventListener('view-enter-start', this.onViewEnterStart_); |
| this.addEventListener('view-exit-start', this.onViewExitStart_); |
| this.addEventListener('view-exit-finish', this.onViewExitFinish_); |
| |
| const service = Service.getInstance(); |
| |
| const onProfileStateChanged = |
| (profileInfo: chrome.developerPrivate.ProfileInfo) => { |
| this.isChildAccount_ = profileInfo.isChildAccount; |
| this.incognitoAvailable_ = profileInfo.isIncognitoAvailable; |
| this.devModeControlledByPolicy = |
| profileInfo.isDeveloperModeControlledByPolicy; |
| this.inDevMode = profileInfo.inDeveloperMode; |
| this.canLoadUnpacked = profileInfo.canLoadUnpacked; |
| this.isMv2DeprecationNoticeDismissed = |
| profileInfo.isMv2DeprecationNoticeDismissed; |
| }; |
| service.getProfileStateChangedTarget().addListener(onProfileStateChanged); |
| service.getProfileConfiguration().then(onProfileStateChanged); |
| |
| service.getExtensionsInfo().then(extensionsAndApps => { |
| this.initExtensionsAndApps_(extensionsAndApps); |
| this.initPage_(); |
| |
| service.getItemStateChangedTarget().addListener( |
| this.onItemStateChanged_.bind(this)); |
| }); |
| } |
| |
| override updated(changedProperties: PropertyValues<this>) { |
| super.updated(changedProperties); |
| |
| const changedPrivateProperties = |
| changedProperties as Map<PropertyKey, unknown>; |
| if (changedPrivateProperties.has('narrow_')) { |
| const drawer = this.shadowRoot.querySelector('cr-drawer'); |
| if (!this.narrow_ && drawer?.open) { |
| drawer.close(); |
| } |
| |
| // TODO(crbug.com/c/1451985): Handle changing focus if focus is on the |
| // sidebar or menu when it's about to disappear when `this.narrow_` |
| // changes. |
| } |
| } |
| |
| override connectedCallback() { |
| super.connectedCallback(); |
| |
| document.documentElement.classList.remove('loading'); |
| // https://github.com/microsoft/TypeScript/issues/13569 |
| (document as any).fonts.load('bold 12px Roboto'); |
| |
| this.navigationListener_ = navigation.addListener(newPage => { |
| this.changePage_(newPage); |
| }); |
| } |
| |
| override disconnectedCallback() { |
| super.disconnectedCallback(); |
| assert(this.navigationListener_); |
| assert(navigation.removeListener(this.navigationListener_)); |
| this.navigationListener_ = null; |
| } |
| |
| /** |
| * @return the promise of `pageInitializedResolver_` so tests can wait for the |
| * page to be initialized. |
| */ |
| whenPageInitializedForTest(): Promise<void> { |
| return this.pageInitializedResolver_.promise; |
| } |
| |
| /** |
| * Initializes the page to reflect what's specified in the url so that if |
| * the user visits chrome://extensions/?id=..., we land on the proper page. |
| */ |
| private initPage_() { |
| this.didInitPage_ = true; |
| this.changePage_(navigation.getCurrentPage()); |
| this.pageInitializedResolver_.resolve(); |
| } |
| |
| protected onNarrowChanged_(e: CustomEvent<{value: boolean}>) { |
| this.narrow_ = e.detail.value; |
| } |
| |
| private onItemStateChanged_(eventData: chrome.developerPrivate.EventData) { |
| const EventType = chrome.developerPrivate.EventType; |
| switch (eventData.event_type) { |
| case EventType.VIEW_REGISTERED: |
| case EventType.VIEW_UNREGISTERED: |
| case EventType.INSTALLED: |
| case EventType.LOADED: |
| case EventType.UNLOADED: |
| case EventType.ERROR_ADDED: |
| case EventType.ERRORS_REMOVED: |
| case EventType.PREFS_CHANGED: |
| case EventType.WARNINGS_CHANGED: |
| case EventType.COMMAND_ADDED: |
| case EventType.COMMAND_REMOVED: |
| case EventType.PERMISSIONS_CHANGED: |
| case EventType.SERVICE_WORKER_STARTED: |
| case EventType.SERVICE_WORKER_STOPPED: |
| case EventType.PINNED_ACTIONS_CHANGED: |
| // |extensionInfo| can be undefined in the case of an extension |
| // being unloaded right before uninstallation. There's nothing to do |
| // here. |
| if (!eventData.extensionInfo) { |
| break; |
| } |
| |
| if (this.delegate.shouldIgnoreUpdate( |
| eventData.extensionInfo.id, eventData.event_type)) { |
| break; |
| } |
| |
| const listId = this.getListId_(eventData.extensionInfo); |
| const currentIndex = this.getListFromId_(listId).findIndex( |
| (item: chrome.developerPrivate.ExtensionInfo) => |
| item.id === eventData.extensionInfo!.id); |
| |
| if (currentIndex >= 0) { |
| this.updateItem_(listId, currentIndex, eventData.extensionInfo); |
| } else { |
| this.addItem_(listId, eventData.extensionInfo); |
| } |
| |
| // This is likely to trigger multiple times (one for each extension |
| // that's disabled. That's fine; we'll only show the toast for the first |
| // one, since we check first if it's open. |
| const toastManager = getToastManager(); |
| if (this.showUnsupportedDeveloperExtensionDisabledToast_( |
| eventData.event_type, eventData.extensionInfo) && |
| !toastManager.isToastOpen) { |
| toastManager.duration = TOAST_DURATION_MS; |
| toastManager.show(this.i18n('itemUnsupportedDeveloperModeToast')); |
| } |
| break; |
| case EventType.UNINSTALLED: |
| this.removeItem_(eventData.item_id); |
| break; |
| case EventType.CONFIGURATION_CHANGED: |
| const index = this.getIndexInList_('extensions_', eventData.item_id); |
| this.updateItem_( |
| 'extensions_', index, |
| Object.assign({}, this.getData_(eventData.item_id), { |
| didAcknowledgeMV2DeprecationNotice: |
| eventData.extensionInfo?.didAcknowledgeMV2DeprecationNotice, |
| safetyCheckText: eventData.extensionInfo?.safetyCheckText, |
| })); |
| break; |
| default: |
| assertNotReached(); |
| } |
| } |
| |
| protected onFilterChanged_(event: CustomEvent<string>) { |
| if (this.currentPage_!.page !== Page.LIST) { |
| navigation.navigateTo({page: Page.LIST}); |
| } |
| this.filter = event.detail; |
| } |
| |
| protected onMenuButtonClick_() { |
| this.showDrawer_ = true; |
| setTimeout(() => { |
| this.shadowRoot.querySelector('cr-drawer')!.openDrawer(); |
| }, 0); |
| } |
| |
| /** |
| * @return The ID of the list that the item belongs in. |
| */ |
| private getListId_(item: chrome.developerPrivate.ExtensionInfo): string { |
| const ExtensionType = chrome.developerPrivate.ExtensionType; |
| switch (item.type) { |
| case ExtensionType.HOSTED_APP: |
| case ExtensionType.LEGACY_PACKAGED_APP: |
| case ExtensionType.PLATFORM_APP: |
| return 'apps_'; |
| case ExtensionType.EXTENSION: |
| case ExtensionType.SHARED_MODULE: |
| return 'extensions_'; |
| case ExtensionType.THEME: |
| assertNotReached('Don\'t send themes to the chrome://extensions page'); |
| default: |
| assertNotReached(); |
| } |
| } |
| |
| /** |
| * @param listId The list to look for the item in. |
| * @param itemId The id of the item to look for. |
| * @return The index of the item in the list, or -1 if not found. |
| */ |
| private getIndexInList_(listId: string, itemId: string): number { |
| return this.getListFromId_(listId).findIndex(function( |
| item: chrome.developerPrivate.ExtensionInfo) { |
| return item.id === itemId; |
| }); |
| } |
| |
| private getData_(id: string): chrome.developerPrivate.ExtensionInfo |
| |undefined { |
| return this.extensions_[this.getIndexInList_('extensions_', id)] || |
| this.apps_[this.getIndexInList_('apps_', id)]; |
| } |
| |
| /** |
| * Categorizes |extensionsAndApps| to apps and extensions and initializes |
| * those lists. |
| */ |
| private initExtensionsAndApps_(extensionsAndApps: |
| chrome.developerPrivate.ExtensionInfo[]) { |
| extensionsAndApps.sort(compareExtensions); |
| const apps: chrome.developerPrivate.ExtensionInfo[] = []; |
| const extensions: chrome.developerPrivate.ExtensionInfo[] = []; |
| for (const i of extensionsAndApps) { |
| const list = this.getListId_(i) === 'apps_' ? apps : extensions; |
| list.push(i); |
| } |
| |
| this.apps_ = apps; |
| this.extensions_ = extensions; |
| } |
| |
| /** |
| * Creates and adds a new extensions-item element to the list, inserting it |
| * into its sorted position in the relevant section. |
| * @param item The extension the new element is representing. |
| */ |
| private addItem_( |
| listId: string, item: chrome.developerPrivate.ExtensionInfo) { |
| // We should never try and add an existing item. |
| assert(this.getIndexInList_(listId, item.id) === -1); |
| const list = this.getListFromId_(listId); |
| let insertBeforeChild = |
| list.findIndex(function(listEl: chrome.developerPrivate.ExtensionInfo) { |
| return compareExtensions(listEl, item) > 0; |
| }); |
| if (insertBeforeChild === -1) { |
| insertBeforeChild = list.length; |
| } |
| this.updateList_(listId, insertBeforeChild, 0, item); |
| } |
| |
| private getListFromId_(listId: string): |
| chrome.developerPrivate.ExtensionInfo[] { |
| assert(listId === 'extensions_' || listId === 'apps_'); |
| return listId === 'extensions_' ? this.extensions_ : this.apps_; |
| } |
| |
| // Intentionally creating a new array reference to notify the Lit item-list |
| // child via data bindings. |
| private updateList_( |
| listId: string, index: number, removeCount: number, |
| newItem?: chrome.developerPrivate.ExtensionInfo) { |
| const list = this.getListFromId_(listId); |
| if (newItem) { |
| list.splice(index, removeCount, newItem); |
| } else { |
| list.splice(index, removeCount); |
| } |
| listId === 'extensions_' ? this.extensions_ = list.slice() : |
| this.apps_ = list.slice(); |
| } |
| |
| /** |
| * @param item The data for the item to update. |
| */ |
| private updateItem_( |
| listId: string, index: number, |
| item: chrome.developerPrivate.ExtensionInfo) { |
| // We should never try and update a non-existent item. |
| assert(index >= 0); |
| this.updateList_(listId, index, 1, item); |
| |
| // Update the subpage if it is open and displaying the item. If it's not |
| // open, we don't update the data even if it's displaying that item. We'll |
| // set the item correctly before opening the page. It's a little weird |
| // that the DOM will have stale data, but there's no point in causing the |
| // extra work. |
| if (this.detailViewItem_ && this.detailViewItem_.id === item.id && |
| this.currentPage_!.page === Page.DETAILS) { |
| this.detailViewItem_ = item; |
| } else if ( |
| this.errorPageItem_ && this.errorPageItem_.id === item.id && |
| this.currentPage_!.page === Page.ERRORS) { |
| this.errorPageItem_ = item; |
| } else if ( |
| this.activityLogItem_ && this.activityLogItem_.id === item.id && |
| this.currentPage_!.page === Page.ACTIVITY_LOG) { |
| this.activityLogItem_ = item; |
| } |
| } |
| |
| // When an item is removed while on the 'item list' page, move focus to the |
| // next item in the list with `listId` if available. If no items are in that |
| // list, focus to the search bar as a fallback. |
| // This is a fix for crbug.com/1416324 which causes focus to linger on a |
| // deleted element, which is then read by the screen reader. |
| private focusAfterItemRemoved_(listId: string, index: number) { |
| // A timeout is used so elements are focused after the DOM is updated. |
| setTimeout(() => { |
| const list = this.getListFromId_(listId); |
| if (list.length) { |
| const focusIndex = Math.min(list.length - 1, index); |
| const itemToFocusId = list[focusIndex]!.id; |
| |
| // In the rare case where the item cannot be focused despite existing, |
| // focus the search bar. |
| if (!this.$['items-list'].focusItemButton(itemToFocusId)) { |
| this.$.toolbar.focusSearchInput(); |
| } |
| } else { |
| this.$.toolbar.focusSearchInput(); |
| } |
| }, 0); |
| } |
| |
| /** |
| * @param itemId The id of item to remove. |
| */ |
| private removeItem_(itemId: string) { |
| // Search for the item to be deleted in `extensions_`. |
| let listId = 'extensions_'; |
| let index = this.getIndexInList_(listId, itemId); |
| if (index === -1) { |
| // If not in `extensions_` it must be in `apps_`. |
| listId = 'apps_'; |
| index = this.getIndexInList_(listId, itemId); |
| } |
| |
| // We should never try and remove a non-existent item. |
| assert(index >= 0); |
| this.updateList_(listId, index, 1); |
| if (this.currentPage_!.page === Page.LIST) { |
| // Wait for the items list to be updated with the new value before trying |
| // to focus an item. |
| this.$['items-list'].updateComplete.then(() => { |
| this.focusAfterItemRemoved_(listId, index); |
| }); |
| } else if ( |
| (this.currentPage_!.page === Page.ACTIVITY_LOG || |
| this.currentPage_!.page === Page.DETAILS || |
| this.currentPage_!.page === Page.ERRORS) && |
| this.currentPage_!.extensionId === itemId) { |
| // Leave the details page (the 'item list' page is a fine choice). |
| navigation.replaceWith({page: Page.LIST}); |
| } |
| } |
| |
| private onLoadError_( |
| e: CustomEvent<Error|chrome.developerPrivate.LoadError>) { |
| this.showLoadErrorDialog_ = true; |
| setTimeout(() => { |
| const dialog = this.shadowRoot.querySelector('extensions-load-error')!; |
| dialog.loadError = e.detail; |
| dialog.show(); |
| }, 0); |
| } |
| |
| /** |
| * Changes the active page selection. |
| */ |
| private changePage_(newPage: PageState) { |
| this.onCloseDrawer_(); |
| |
| const optionsDialog = |
| this.shadowRoot.querySelector('extensions-options-dialog'); |
| if (optionsDialog && optionsDialog.open) { |
| this.showOptionsDialog_ = false; |
| } |
| |
| const fromPage = this.currentPage_ ? this.currentPage_.page : null; |
| const toPage = newPage.page; |
| let data: chrome.developerPrivate.ExtensionInfo|undefined; |
| let activityLogPlaceholder; |
| if (toPage === Page.LIST) { |
| // Dismiss menu notifications for extensions module of Safety Hub. |
| this.delegate.dismissSafetyHubExtensionsMenuNotification(); |
| } |
| if (newPage.extensionId) { |
| data = this.getData_(newPage.extensionId); |
| if (!data) { |
| // Allow the user to navigate to the activity log page even if the |
| // extension ID is not valid. This enables the use case of seeing an |
| // extension's install-time activities by navigating to an extension's |
| // activity log page, then installing the extension. |
| if (this.showActivityLog && toPage === Page.ACTIVITY_LOG) { |
| activityLogPlaceholder = { |
| id: newPage.extensionId, |
| isPlaceholder: true, |
| }; |
| } else { |
| // Attempting to view an invalid (removed?) app or extension ID. |
| navigation.replaceWith({page: Page.LIST}); |
| return; |
| } |
| } |
| } |
| |
| if (toPage === Page.DETAILS) { |
| this.detailViewItem_ = data; |
| } else if (toPage === Page.ERRORS) { |
| this.errorPageItem_ = data; |
| } else if (toPage === Page.ACTIVITY_LOG) { |
| if (!this.showActivityLog) { |
| // Redirect back to the details page if we try to view the |
| // activity log of an extension but the flag is not set. |
| navigation.replaceWith( |
| {page: Page.DETAILS, extensionId: newPage.extensionId}); |
| return; |
| } |
| |
| this.activityLogItem_ = data || activityLogPlaceholder; |
| } else if ( |
| (toPage === Page.SITE_PERMISSIONS || |
| toPage === Page.SITE_PERMISSIONS_ALL_SITES) && |
| !this.enableEnhancedSiteControls) { |
| // Redirect back to the main page if we try to view the new site |
| // permissions page but the flag is not set. |
| navigation.replaceWith({page: Page.LIST}); |
| return; |
| } |
| |
| if (fromPage !== toPage) { |
| this.$.viewManager.switchView(toPage, 'no-animation', 'no-animation'); |
| } |
| |
| if (newPage.subpage) { |
| assert(newPage.subpage === Dialog.OPTIONS); |
| assert(newPage.extensionId); |
| this.showOptionsDialog_ = true; |
| setTimeout(() => { |
| this.shadowRoot.querySelector('extensions-options-dialog')!.show( |
| data!, |
| ); |
| }, 0); |
| } |
| |
| document.title = toPage === Page.DETAILS ? |
| `${loadTimeData.getString('title')} - ${this.detailViewItem_!.name}` : |
| loadTimeData.getString('title'); |
| this.currentPage_ = newPage; |
| } |
| |
| /** |
| * This method detaches the drawer dialog completely. Should only be |
| * triggered by the dialog's 'close' event. |
| */ |
| protected onDrawerClose_() { |
| this.showDrawer_ = false; |
| } |
| |
| /** |
| * This method animates the closing of the drawer. |
| */ |
| protected onCloseDrawer_() { |
| const drawer = this.shadowRoot.querySelector('cr-drawer'); |
| if (drawer && drawer.open) { |
| drawer.close(); |
| } |
| } |
| |
| protected onLoadErrorDialogClose_() { |
| this.showLoadErrorDialog_ = false; |
| } |
| |
| protected onOptionsDialogClose_() { |
| this.showOptionsDialog_ = false; |
| this.shadowRoot.querySelector( |
| 'extensions-detail-view')!.focusOptionsButton(); |
| } |
| |
| private onViewEnterStart_() { |
| this.fromActivityLog_ = false; |
| } |
| |
| private onViewExitStart_(e: Event) { |
| const viewType = (e.composedPath()[0] as HTMLElement).tagName; |
| this.fromActivityLog_ = viewType === 'EXTENSIONS-ACTIVITY-LOG'; |
| } |
| |
| private onViewExitFinish_(e: Event) { |
| const viewType = (e.composedPath()[0] as HTMLElement).tagName; |
| if (viewType === 'EXTENSIONS-ITEM-LIST' || |
| viewType === 'EXTENSIONS-KEYBOARD-SHORTCUTS' || |
| viewType === 'EXTENSIONS-ACTIVITY-LOG' || |
| viewType === 'EXTENSIONS-SITE-PERMISSIONS' || |
| viewType === 'EXTENSIONS-SITE-PERMISSIONS-BY-SITE') { |
| return; |
| } |
| |
| const extensionId = |
| (e.composedPath()[0] as ExtensionsDetailViewElement).data.id; |
| const list = this.shadowRoot.querySelector('extensions-item-list')!; |
| const button = viewType === 'EXTENSIONS-DETAIL-VIEW' ? |
| list.getDetailsButton(extensionId) : |
| list.getErrorsButton(extensionId); |
| |
| // The button will not exist, when returning from a details page |
| // because the corresponding extension/app was deleted. |
| if (button) { |
| button.focus(); |
| } |
| } |
| |
| protected onShowInstallWarnings_(e: CustomEvent<string[]>) { |
| // Leverage Polymer data bindings instead of just assigning the |
| // installWarnings on the dialog since the dialog hasn't been stamped |
| // in the DOM yet. |
| this.installWarnings_ = e.detail; |
| this.showInstallWarningsDialog_ = true; |
| } |
| |
| protected onInstallWarningsDialogClose_() { |
| this.installWarnings_ = null; |
| this.showInstallWarningsDialog_ = false; |
| } |
| |
| /** |
| * Show a toast when an unpacked extension becomes disabled when the user is |
| * not in developer mode. |
| */ |
| private showUnsupportedDeveloperExtensionDisabledToast_( |
| eventType: chrome.developerPrivate.EventType, |
| extensionInfo: chrome.developerPrivate.ExtensionInfo): boolean { |
| if (eventType !== chrome.developerPrivate.EventType.UNLOADED) { |
| return false; |
| } |
| |
| return !this.inDevMode && |
| extensionInfo.state === |
| chrome.developerPrivate.ExtensionState.DISABLED && |
| extensionInfo.location === chrome.developerPrivate.Location.UNPACKED && |
| extensionInfo.disableReasons.unsupportedDeveloperExtension; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'extensions-manager': ExtensionsManagerElement; |
| } |
| } |
| |
| customElements.define(ExtensionsManagerElement.is, ExtensionsManagerElement); |