chromium/chrome/browser/resources/settings/performance_page/tab_discard/exception_list.ts

// Copyright 2022 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_action_menu/cr_action_menu.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_collapse/cr_collapse.js';
import 'chrome://resources/cr_elements/cr_expand_button/cr_expand_button.js';
import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import 'chrome://resources/cr_elements/cr_tooltip/cr_tooltip.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import '../../settings_shared.css.js';
import './exception_edit_dialog.js';
import './exception_entry.js';
import './exception_tabbed_add_dialog.js';

import type {PrefsMixinInterface} from '/shared/settings/prefs/prefs_mixin.js';
import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
import type {CrActionMenuElement} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
import type {CrCollapseElement} from 'chrome://resources/cr_elements/cr_collapse/cr_collapse.js';
import type {CrExpandButtonElement} from 'chrome://resources/cr_elements/cr_expand_button/cr_expand_button.js';
import type {CrLazyRenderElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import type {CrTooltipElement} from 'chrome://resources/cr_elements/cr_tooltip/cr_tooltip.js';
import type {ListPropertyUpdateMixinInterface} from 'chrome://resources/cr_elements/list_property_update_mixin.js';
import {ListPropertyUpdateMixin} from 'chrome://resources/cr_elements/list_property_update_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import type {DomRepeat} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import type {TooltipMixinInterface} from '../../tooltip_mixin.js';
import {TooltipMixin} from '../../tooltip_mixin.js';
import type {PerformanceMetricsProxy} from '../performance_metrics_proxy.js';
import {MemorySaverModeExceptionListAction, PerformanceMetricsProxyImpl} from '../performance_metrics_proxy.js';

import type {ExceptionEntry} from './exception_entry.js';
import {getTemplate} from './exception_list.html.js';
import {TAB_DISCARD_EXCEPTIONS_MANAGED_PREF, TAB_DISCARD_EXCEPTIONS_PREF} from './exception_validation_mixin.js';

export const TAB_DISCARD_EXCEPTIONS_OVERFLOW_SIZE: number = 5;

export interface ExceptionListElement {
  $: {
    addButton: CrButtonElement,
    collapse: CrCollapseElement,
    expandButton: CrExpandButtonElement,
    list: DomRepeat,
    overflowList: DomRepeat,
    menu: CrLazyRenderElement<CrActionMenuElement>,
    noSitesAdded: HTMLElement,
    tooltip: CrTooltipElement,
  };
}

type Constructor<T> = new (...args: any[]) => T;
const ExceptionListElementBase =
    TooltipMixin(ListPropertyUpdateMixin(PrefsMixin(PolymerElement))) as
    Constructor<TooltipMixinInterface&ListPropertyUpdateMixinInterface&
                PrefsMixinInterface&PolymerElement>;

export class ExceptionListElement extends
    ExceptionListElementBase {
  static get is() {
    return 'tab-discard-exception-list';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      siteList_: {
        type: Array,
        value: [],
      },

      overflowSiteListExpanded: {type: Boolean, value: false},

      /**
       * Rule corresponding to the last more actions menu opened. Indicates to
       * this element and its dialog which rule to edit or if a new one should
       * be added.
       */
      selectedRule_: {
        type: String,
        value: '',
      },

      showTabbedAddDialog_: {
        type: Boolean,
        value: false,
      },

      showEditDialog_: {
        type: Boolean,
        value: false,
      },

      tooltipText_: String,
    };
  }

  static get observers() {
    return [
      `onPrefsChanged_(prefs.${TAB_DISCARD_EXCEPTIONS_PREF}.value.*,` +
          `prefs.${TAB_DISCARD_EXCEPTIONS_MANAGED_PREF}.value.*)`,
    ];
  }

  private siteList_: ExceptionEntry[];
  private overflowSiteListExpanded: boolean;
  private selectedRule_: string;
  private showTabbedAddDialog_: boolean;
  private showEditDialog_: boolean;
  private tooltipText_: string;

  private metricsProxy_: PerformanceMetricsProxy =
      PerformanceMetricsProxyImpl.getInstance();

  private hasSites_(): boolean {
    return this.siteList_.length > 0;
  }

  private hasOverflowSites_() {
    return this.siteList_.length > TAB_DISCARD_EXCEPTIONS_OVERFLOW_SIZE;
  }

  private getSiteList_() {
    return this.siteList_.slice(-TAB_DISCARD_EXCEPTIONS_OVERFLOW_SIZE)
        .reverse();
  }

  private getOverflowSiteList_() {
    return this.siteList_.slice(0, -TAB_DISCARD_EXCEPTIONS_OVERFLOW_SIZE)
        .reverse();
  }

  private onAddClick_() {
    assert(!this.showEditDialog_);
    this.showTabbedAddDialog_ = true;
  }

  private onMenuClick_(e: CustomEvent<{target: HTMLElement, site: string}>) {
    e.stopPropagation();
    this.selectedRule_ = e.detail.site;
    this.$.menu.get().showAt(e.detail.target);
  }

  private onEditClick_() {
    assert(this.selectedRule_);
    assert(!this.showTabbedAddDialog_);
    this.showEditDialog_ = true;
    this.$.menu.get().close();
  }

  private onDeleteClick_() {
    this.deletePrefDictEntry(TAB_DISCARD_EXCEPTIONS_PREF, this.selectedRule_);
    this.metricsProxy_.recordExceptionListAction(
        MemorySaverModeExceptionListAction.REMOVE);
    this.$.menu.get().close();
  }

  private onTabbedAddDialogClose_() {
    this.showTabbedAddDialog_ = false;
  }

  private onEditDialogClose_() {
    this.showEditDialog_ = false;
  }

  private onPrefsChanged_() {
    const newSites: ExceptionEntry[] = [];
    for (const pref
             of [TAB_DISCARD_EXCEPTIONS_MANAGED_PREF,
                 TAB_DISCARD_EXCEPTIONS_PREF]) {
      // Annotate sites with their managed status and append them to newSites
      // with managed sites first.
      const prefObject = this.getPref(pref);
      let sites = prefObject.value;

      if (sites.constructor.name === 'Object') {
        sites = Object.keys(sites);
      }
      const siteToExceptionEntry = (site: string) => ({
        site,
        managed: prefObject.enforcement ===
            chrome.settingsPrivate.Enforcement.ENFORCED,
      });
      newSites.push(...sites.map(siteToExceptionEntry));
    }

    // Optimizes updates by keeping existing references and minimizes splices
    this.updateList(
        'siteList_', (entry: ExceptionEntry) => entry.site, newSites);
  }

  /**
   * Need to use common tooltip since the tooltip in the entry is cut off from
   * the iron-list.
   */
  private onShowTooltip_(e: CustomEvent<{target: HTMLElement, text: string}>) {
    this.tooltipText_ = e.detail.text;
    this.showTooltipAtTarget(this.$.tooltip, e.detail.target);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'tab-discard-exception-list': ExceptionListElement;
  }
}

customElements.define(
    ExceptionListElement.is, ExceptionListElement);