blob: 3ab745ef409c7c904c36916945477ed2df05a1a6 [file] [log] [blame]
// Copyright 2023 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://search-engine-choice/tangible_sync_style_shared.css.js';
import 'chrome://resources/cr_components/localized_link/localized_link.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/cr_elements/cr_radio_group/cr_radio_group.js';
import 'chrome://resources/cr_elements/cr_radio_button/cr_radio_button.js';
import 'chrome://resources/cr_elements/cr_icons.css.js';
import 'chrome://resources/polymer/v3_0/iron-collapse/iron-collapse.js';
import 'chrome://resources/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/cr_elements/cr_expand_button/cr_expand_button.js';
import './strings.m.js';
import type {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import type {CrRadioGroupElement} from 'chrome://resources/cr_elements/cr_radio_group/cr_radio_group.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {getFaviconForPageURL} from 'chrome://resources/js/icon.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {afterNextRender, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './app.html.js';
import type {SearchEngineChoice} from './browser_proxy.js';
import {SearchEngineChoiceBrowserProxy} from './browser_proxy.js';
import type {PageHandlerRemote} from './search_engine_choice.mojom-webui.js';
export interface SearchEngineChoiceAppElement {
$: {
dummyOmnibox: HTMLElement,
infoDialog: CrDialogElement,
searchEngineOmnibox: HTMLElement,
actionButton: CrButtonElement,
infoLink: HTMLElement,
choiceList: CrRadioGroupElement,
};
}
const SearchEngineChoiceAppElementBase = I18nMixin(PolymerElement);
export class SearchEngineChoiceAppElement extends
SearchEngineChoiceAppElementBase {
static get is() {
return 'search-engine-choice-app';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/**
* The choice list is passed as JSON because it doesn't change
* dynamically, so it would be better to have it available as loadtime
* data.
*/
choiceList_: {
type: Array,
value() {
return JSON.parse(loadTimeData.getString('choiceList'));
},
},
// The choice will always be > 0 when selected for prepopulated engines
// and == 0 for a custom search engine.
selectedChoice_: {
type: Number,
value: -1,
observer: 'onSelectedChoiceChanged_',
},
isActionButtonDisabled_: {
type: Boolean,
computed: 'computeActionButtonDisabled_(selectedChoice_, ' +
'hasUserScrolledToTheBottom_)',
},
fakeOmniboxText_: {
type: String,
value: '',
},
fakeOmniboxIconPath_: {
type: String,
value: '',
},
actionButtonText_: {
type: String,
computed: 'getActionButtonText_(hasUserScrolledToTheBottom_)',
},
hasUserScrolledToTheBottom_: {
type: Boolean,
value: false,
},
};
}
private choiceList_: SearchEngineChoice[];
private selectedChoice_: string;
private fakeOmniboxText_: string;
private fakeOmniboxIconPath_: string;
private pageHandler_: PageHandlerRemote;
private hasUserScrolledToTheBottom_: boolean;
private actionButtonText_: string;
constructor() {
super();
this.pageHandler_ = SearchEngineChoiceBrowserProxy.getInstance().handler;
}
override connectedCallback() {
super.connectedCallback();
// Change the `icon_path` format so that it can be used with the
// `background-image` property in HTML. The
// `background-image` property should be used because `getFaviconForPageURL`
// returns an `image-set` and not a url.
this.choiceList_.forEach((searchEngine: SearchEngineChoice) => {
if (searchEngine.prepopulateId === 0) {
// Fetch the favicon from the Favicon Service for custom search
// engines.
searchEngine.iconPath =
getFaviconForPageURL(searchEngine?.url!, false, '', 24);
} else {
searchEngine.iconPath = 'image-set(url(' + searchEngine.iconPath +
') 1x, url(' + searchEngine.iconPath + '@2x) 2x)';
}
});
afterNextRender(this, () => {
// If the choice list and the page don't contain a scrollbar then the
// user is already at the bottom.
this.hasUserScrolledToTheBottom_ =
!this.isChoiceListScrollable_() && !this.isPageScrollable_();
if (this.isChoiceListScrollable_()) {
this.$.choiceList.addEventListener(
'scroll', this.onChoiceListScroll_.bind(this));
}
if (this.isPageScrollable_()) {
document.addEventListener('scroll', this.onPageScroll_.bind(this));
}
this.pageHandler_.displayDialog();
});
}
private onLinkClicked_() {
this.$.infoDialog.showModal();
this.pageHandler_.handleLearnMoreLinkClicked();
}
private needsUserChoice_() {
return parseInt(this.selectedChoice_) === -1;
}
// The action button will be disabled if the user scrolls to the bottom of
// the list without making a search engine choice.
private computeActionButtonDisabled_() {
return this.hasUserScrolledToTheBottom_ && this.needsUserChoice_();
}
private onActionButtonClicked_() {
if (this.hasUserScrolledToTheBottom_) {
this.pageHandler_.handleSearchEngineChoiceSelected(
parseInt(this.selectedChoice_));
return;
}
if (this.isChoiceListScrollable_()) {
const choiceList = this.$.choiceList;
choiceList.scrollTo({top: choiceList.scrollHeight, behavior: 'smooth'});
} else if (this.isPageScrollable_()) {
window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'});
}
}
private onChevronClicked_(chevronExpanded: boolean) {
if (chevronExpanded) {
chrome.metricsPrivate.recordUserAction('ExpandSearchEngineDescription');
}
}
private onInfoDialogButtonClicked_() {
this.$.infoDialog.close();
}
private onSelectedChoiceChanged_(selectedChoice: string) {
// No search engine selected.
if (parseInt(selectedChoice) === -1) {
return;
}
// Get the selected engine.
const choice = this.choiceList_.find(
elem => elem.prepopulateId === parseInt(selectedChoice));
const searchEngineOmnibox = this.$.searchEngineOmnibox;
const dummyOmnibox = this.$.dummyOmnibox;
const fakeOmniboxText = this.i18n('fakeOmniboxText', choice?.name!);
const fakeOmniboxIconPath = choice?.iconPath!;
// Change the previous engine name to the new one and then start
// the fade-in-animation when the fade-out-animation finishes running.
const handleFadeOutFinished = (event: AnimationEvent) => {
if (event.animationName === 'fade-out-animation') {
searchEngineOmnibox.classList.remove('fade-out-animation');
this.fakeOmniboxText_ = fakeOmniboxText;
this.fakeOmniboxIconPath_ = fakeOmniboxIconPath;
// `requestAnimationFrame` is called to make sure that the previous
// animation is fully removed so that the next one can be run.
window.requestAnimationFrame(function() {
searchEngineOmnibox.classList.add('fade-in-animation');
});
} else if (event.animationName === 'fade-in-animation') {
// Hide the dummy omnibox so that it is not shown behind the
// search engine omnibox.
if (!dummyOmnibox.classList.contains('hidden')) {
dummyOmnibox.classList.add('hidden');
}
}
};
// Show the dummy omnibox at fade-out start so that it can be seen while
// animating the search engine omnibox.
const handleAnimationStart = (event: AnimationEvent) => {
if (event.animationName === 'fade-out-animation') {
dummyOmnibox.classList.remove('hidden');
}
};
searchEngineOmnibox.addEventListener('animationend', handleFadeOutFinished);
searchEngineOmnibox.addEventListener(
'animationstart', handleAnimationStart);
if (searchEngineOmnibox.classList.contains('fade-in-animation')) {
searchEngineOmnibox.classList.remove('fade-in-animation');
window.requestAnimationFrame(function() {
searchEngineOmnibox.classList.add('fade-out-animation');
});
} else {
this.fakeOmniboxText_ = fakeOmniboxText;
this.fakeOmniboxIconPath_ = fakeOmniboxIconPath;
searchEngineOmnibox.classList.add('fade-in-animation');
}
}
private onChoiceListScroll_() {
this.processScroll_(
/*contentHeight=*/ this.$.choiceList.scrollHeight,
/*viewportHeight=*/ this.$.choiceList.clientHeight,
/*scrollPosition=*/ this.$.choiceList.scrollTop,
);
}
private onPageScroll_() {
this.processScroll_(
/*contentHeight=*/ document.body.scrollHeight,
/*viewportHeight=*/ window.innerHeight,
/*scrollPosition=*/ window.scrollY,
);
}
private processScroll_(
contentHeight: number, viewportHeight: number, scrollPosition: number) {
// The value is checked against `< 1` instead of `=== 0` to keep a margin of
// error.
if (contentHeight - viewportHeight - scrollPosition < 1) {
this.hasUserScrolledToTheBottom_ = true;
document.removeEventListener('scroll', this.onPageScroll_.bind(this));
this.$.choiceList.removeEventListener(
'scroll', this.onChoiceListScroll_.bind(this));
}
}
// The choice list is scrollable at the dialog's full height.
private isChoiceListScrollable_() {
const choiceListOverflow = getComputedStyle(this.$.choiceList).overflow;
return choiceListOverflow === 'auto' &&
this.$.choiceList.scrollHeight > this.$.choiceList.clientHeight;
}
// The page becomes scrollable instead of the choice lists at specific
// heights.
private isPageScrollable_() {
const choiceListOverflow = getComputedStyle(this.$.choiceList).overflow;
return choiceListOverflow === 'visible' &&
document.body.scrollHeight > document.body.clientHeight;
}
private getActionButtonText_() {
return this.i18n(
this.hasUserScrolledToTheBottom_ ? 'submitButtonText' :
'moreButtonText');
}
}
declare global {
interface HTMLElementTagNameMap {
'search-engine-choice-app': SearchEngineChoiceAppElement;
}
}
customElements.define(
SearchEngineChoiceAppElement.is, SearchEngineChoiceAppElement);