chromium/chrome/browser/resources/history/synced_device_card.ts

// Copyright 2016 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_collapse/cr_collapse.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/icons_lit.html.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import './searched_label.js';
import './shared_style.css.js';
import './shared_vars.css.js';
import './strings.m.js';

import type {CrCollapseElement} from 'chrome://resources/cr_elements/cr_collapse/cr_collapse.js';
import {FocusRow} from 'chrome://resources/js/focus_row.js';
import {getFaviconForPageURL} from 'chrome://resources/js/icon.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {BrowserServiceImpl} from './browser_service.js';
import {SYNCED_TABS_HISTOGRAM_NAME, SyncedTabsHistogram} from './constants.js';
import type {ForeignSessionTab} from './externs.js';
import {getTemplate} from './synced_device_card.html.js';

interface OpenTabEvent {
  model: {tab: ForeignSessionTab};
}

declare global {
  interface HTMLElementTagNameMap {
    'history-synced-device-card': HistorySyncedDeviceCardElement;
  }
}

export interface HistorySyncedDeviceCardElement {
  $: {
    'card-heading': HTMLDivElement,
    'collapse': CrCollapseElement,
    'collapse-button': HTMLElement,
    'menu-button': HTMLElement,
  };
}

export class HistorySyncedDeviceCardElement extends PolymerElement {
  static get is() {
    return 'history-synced-device-card';
  }

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

  static get properties() {
    return {
      /**
       * The list of tabs open for this device.
       */
      tabs: {type: Array, observer: 'updateIcons_'},

      // Name of the synced device.
      device: String,

      // When the device information was last updated.
      lastUpdateTime: String,

      // Whether the card is open.
      opened: Boolean,

      searchTerm: String,

      /**
       * The indexes where a window separator should be shown. The use of a
       * separate array here is necessary for window separators to appear
       * correctly in search. See http://crrev.com/2022003002 for more details.
       */
      separatorIndexes: Array,

      // Internal identifier for the device.
      sessionTag: String,
    };
  }

  tabs: ForeignSessionTab[] = [];
  opened: boolean;
  separatorIndexes: number[];
  sessionTag: string;

  override ready() {
    super.ready();
    this.addEventListener('dom-change', this.notifyFocusUpdate_);
  }

  private fire_(eventName: string, detail?: any) {
    this.dispatchEvent(
        new CustomEvent(eventName, {bubbles: true, composed: true, detail}));
  }

  /**
   * Create FocusRows for this card. One is always made for the card heading and
   * one for each result if the card is open.
   */
  createFocusRows(): FocusRow[] {
    const titleRow = new FocusRow(this.$['card-heading'], null);
    titleRow.addItem('menu', '#menu-button');
    titleRow.addItem('collapse', '#collapse-button');
    const rows = [titleRow];
    if (this.opened) {
      this.shadowRoot!.querySelectorAll<HTMLElement>('.item-container')
          .forEach(function(el) {
            const row = new FocusRow(el, null);
            row.addItem('link', '.website-link');
            rows.push(row);
          });
    }
    return rows;
  }

  /** Open a single synced tab. */
  private openTab_(e: MouseEvent) {
    const model = (e as unknown as OpenTabEvent).model;
    const tab = model.tab;
    const browserService = BrowserServiceImpl.getInstance();
    browserService.recordHistogram(
        SYNCED_TABS_HISTOGRAM_NAME, SyncedTabsHistogram.LINK_CLICKED,
        SyncedTabsHistogram.LIMIT);
    browserService.openForeignSessionTab(this.sessionTag, tab.sessionId, e);
    e.preventDefault();
  }

  /**
   * Toggles the dropdown display of synced tabs for each device card.
   */
  toggleTabCard() {
    const histogramValue = this.$.collapse.opened ?
        SyncedTabsHistogram.COLLAPSE_SESSION :
        SyncedTabsHistogram.EXPAND_SESSION;

    BrowserServiceImpl.getInstance().recordHistogram(
        SYNCED_TABS_HISTOGRAM_NAME, histogramValue, SyncedTabsHistogram.LIMIT);

    this.$.collapse.toggle();

    this.fire_('update-focus-grid');
  }

  private notifyFocusUpdate_() {
    // Refresh focus after all rows are rendered.
    this.fire_('update-focus-grid');
  }

  /**
   * When the synced tab information is set, the icon associated with the tab
   * website is also set.
   */
  private updateIcons_() {
    setTimeout(() => {
      const icons =
          this.shadowRoot!.querySelectorAll<HTMLDivElement>('.website-icon');

      for (let i = 0; i < this.tabs.length; i++) {
        // Entries on this UI are coming strictly from sync, so we can set
        // |isSyncedUrlForHistoryUi| to true on the getFavicon call below.
        icons[i].style.backgroundImage = getFaviconForPageURL(
            this.tabs[i].url, true, this.tabs[i].remoteIconUrlForUma);
      }
    }, 0);
  }

  private isWindowSeparatorIndex_(index: number): boolean {
    return this.separatorIndexes.indexOf(index) !== -1;
  }

  private getCollapseIcon_(opened: boolean): string {
    return opened ? 'cr:expand-less' : 'cr:expand-more';
  }

  private getCollapseTitle_(opened: boolean): string {
    return opened ? loadTimeData.getString('collapseSessionButton') :
                    loadTimeData.getString('expandSessionButton');
  }

  private onMenuButtonClick_(e: Event) {
    this.fire_('synced-device-card-open-menu', {
      target: e.target,
      tag: this.sessionTag,
    });
    e.stopPropagation();  // Prevent cr-collapse.
  }

  private onLinkRightClick_() {
    BrowserServiceImpl.getInstance().recordHistogram(
        SYNCED_TABS_HISTOGRAM_NAME, SyncedTabsHistogram.LINK_RIGHT_CLICKED,
        SyncedTabsHistogram.LIMIT);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'history-synced-device-card': HistorySyncedDeviceCardElement;
  }
}

customElements.define(
    HistorySyncedDeviceCardElement.is, HistorySyncedDeviceCardElement);