chromium/chrome/browser/resources/settings/search_engines_page/search_engines_page.ts

// 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.

/**
 * @fileoverview 'settings-search-engines-page' is the settings page
 * containing search engines settings.
 */
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_shared_style.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/js/cr.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import '../controls/controlled_radio_button.js';
import '../controls/settings_radio_group.js';
import '../simple_confirmation_dialog.js';
import './search_engine_edit_dialog.js';
import './search_engines_list.js';
import './omnibox_extension_entry.js';
import '../settings_shared.css.js';
import '../settings_vars.css.js';

import type {WebUiListenerMixinInterface} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {focusWithoutInk} from 'chrome://resources/js/focus_without_ink.js';
import type {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import type {SettingsRadioGroupElement} from '../controls/settings_radio_group.js';
import {GlobalScrollTargetMixin} from '../global_scroll_target_mixin.js';
import {routes} from '../route.js';

import type {SearchEngine, SearchEnginesBrowserProxy, SearchEnginesInfo} from './search_engines_browser_proxy.js';
import {SearchEnginesBrowserProxyImpl, SearchEnginesInteractions} from './search_engines_browser_proxy.js';
import {getTemplate} from './search_engines_page.html.js';

type SearchEngineEditEvent = CustomEvent<{
  engine: SearchEngine,
  anchorElement: HTMLElement,
}>;

type SearchEngineDeleteEvent = CustomEvent<{
  engine: SearchEngine,
  anchorElement: HTMLElement,
}>;

export interface SettingsSearchEnginesPageElement {
  $: {
    extensions: IronListElement,
    keyboardShortcutSettingGroup: SettingsRadioGroupElement,
  };
}

const SettingsSearchEnginesPageElementBase =
    GlobalScrollTargetMixin(WebUiListenerMixin(PolymerElement)) as
    {new (): PolymerElement & WebUiListenerMixinInterface};

export class SettingsSearchEnginesPageElement extends
    SettingsSearchEnginesPageElementBase {
  static get is() {
    return 'settings-search-engines-page';
  }

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

  static get properties() {
    return {
      /**
       * Preferences state.
       */
      prefs: {
        type: Object,
        notify: true,
      },

      defaultEngines: Array,
      activeEngines: Array,
      otherEngines: Array,
      extensions: Array,

      /**
       * Needed by GlobalScrollTargetMixin.
       */
      subpageRoute: {
        type: Object,
        value: routes.SEARCH_ENGINES,
      },

      showExtensionsList_: {
        type: Boolean,
        computed: 'computeShowExtensionsList_(extensions)',
      },

      /** Filters out all search engines that do not match. */
      filter: {
        type: String,
        value: '',
      },

      matchingDefaultEngines_: {
        type: Array,
        computed: 'computeMatchingEngines_(defaultEngines, filter)',
      },

      matchingActiveEngines_: {
        type: Array,
        computed: 'computeMatchingEngines_(activeEngines, filter)',
      },

      matchingOtherEngines_: {
        type: Array,
        computed: 'computeMatchingEngines_(otherEngines, filter)',
      },

      matchingExtensions_: {
        type: Array,
        computed: 'computeMatchingEngines_(extensions, filter)',
      },

      omniboxExtensionlastFocused_: Object,
      omniboxExtensionListBlurred_: Boolean,

      dialogModel_: {
        type: Object,
        value: null,
      },

      dialogAnchorElement_: {
        type: Object,
        value: null,
      },

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

      showDeleteConfirmationDialog_: {
        type: Boolean,
        value: false,
      },
    };
  }

  static get observers() {
    return ['extensionsChanged_(extensions, showExtensionsList_)'];
  }

  defaultEngines: SearchEngine[];
  activeEngines: SearchEngine[];
  otherEngines: SearchEngine[];
  extensions: SearchEngine[];
  private showExtensionsList_: boolean;
  filter: string;
  private matchingDefaultEngines_: SearchEngine[];
  private matchingActiveEngines_: SearchEngine[];
  private matchingOtherEngines_: SearchEngine[];
  private matchingExtensions_: SearchEngine[];
  private omniboxExtensionlastFocused_: HTMLElement;
  private omniboxExtensionListBlurred_: boolean;
  private dialogModel_: SearchEngine|null;
  private dialogAnchorElement_: HTMLElement|null;
  private showEditDialog_: boolean;
  private showDeleteConfirmationDialog_: boolean;
  private browserProxy_: SearchEnginesBrowserProxy =
      SearchEnginesBrowserProxyImpl.getInstance();

  override ready() {
    super.ready();

    this.browserProxy_.getSearchEnginesList().then(
        this.enginesChanged_.bind(this));
    this.addWebUiListener(
        'search-engines-changed', this.enginesChanged_.bind(this));

    this.addEventListener(
        'view-or-edit-search-engine',
        e => this.onEditSearchEngine_(e as SearchEngineEditEvent));

    this.addEventListener(
        'delete-search-engine',
        e => this.onDeleteSearchEngine_(e as SearchEngineDeleteEvent));
  }

  private openEditDialog_(
      searchEngine: SearchEngine|null, anchorElement: HTMLElement) {
    this.dialogModel_ = searchEngine;
    this.dialogAnchorElement_ = anchorElement;
    this.showEditDialog_ = true;
  }

  private openDeleteConfirmationDialog_(
      searchEngine: SearchEngine|null, anchorElement: HTMLElement) {
    this.dialogModel_ = searchEngine;
    this.dialogAnchorElement_ = anchorElement;
    this.showDeleteConfirmationDialog_ = true;
  }

  private onCloseEditDialog_() {
    this.showEditDialog_ = false;
    focusWithoutInk(this.dialogAnchorElement_ as HTMLElement);
    this.dialogModel_ = null;
    this.dialogAnchorElement_ = null;
  }

  private onCloseDeleteConfirmationDialog_() {
    const dialog =
        this.shadowRoot!.querySelector('settings-simple-confirmation-dialog');
    assert(dialog);
    const confirmed = dialog.wasConfirmed();
    this.showDeleteConfirmationDialog_ = false;

    if (confirmed) {
      assert(this.dialogModel_);
      this.browserProxy_.removeSearchEngine(this.dialogModel_.modelIndex);
      this.dialogAnchorElement_ = null;
    }

    this.dialogModel_ = null;
  }

  private onEditSearchEngine_(e: SearchEngineEditEvent) {
    this.openEditDialog_(e.detail.engine, e.detail.anchorElement);
  }

  private onDeleteSearchEngine_(e: SearchEngineDeleteEvent) {
    this.openDeleteConfirmationDialog_(e.detail.engine, e.detail.anchorElement);
  }

  private extensionsChanged_() {
    if (this.showExtensionsList_ && this.$.extensions) {
      this.$.extensions.notifyResize();
    }
  }

  private enginesChanged_(searchEnginesInfo: SearchEnginesInfo) {
    this.defaultEngines = searchEnginesInfo.defaults;
    this.activeEngines = searchEnginesInfo.actives;
    this.otherEngines = searchEnginesInfo.others;
    this.extensions = searchEnginesInfo.extensions;
  }

  private onAddSearchEngineClick_(e: Event) {
    e.preventDefault();
    this.openEditDialog_(
        null, this.shadowRoot!.querySelector('#addSearchEngine')!);
  }

  private computeShowExtensionsList_(): boolean {
    return this.extensions.length > 0;
  }

  /**
   * Filters the given list based on the currently existing filter string.
   */
  private computeMatchingEngines_(list: SearchEngine[]): SearchEngine[] {
    if (this.filter === '') {
      return list;
    }

    const filter = this.filter.toLowerCase();
    return list.filter(e => {
      return [e.displayName, e.name, e.keyword, e.url].some(
          term => term.toLowerCase().includes(filter));
    });
  }

  /**
   * @param list The original list.
   * @param filteredList The filtered list.
   * @return Whether to show the "no results" message.
   */
  private showNoResultsMessage_(
      list: SearchEngine[], filteredList: SearchEngine[]): boolean {
    return list.length > 0 && filteredList.length === 0;
  }

  private onKeyboardShortcutSettingChange_() {
    const spaceEnabled =
        this.$.keyboardShortcutSettingGroup.selected === 'true';

    this.browserProxy_.recordSearchEnginesPageHistogram(
        spaceEnabled ?
            SearchEnginesInteractions.KEYBOARD_SHORTCUT_SPACE_OR_TAB :
            SearchEnginesInteractions.KEYBOARD_SHORTCUT_TAB);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'settings-search-engines-page': SettingsSearchEnginesPageElement;
  }
}

customElements.define(
    SettingsSearchEnginesPageElement.is, SettingsSearchEnginesPageElement);