// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './iframe.js';
import './logo.js';
import './strings.m.js';
import 'chrome://resources/cr_components/searchbox/searchbox.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import {ColorChangeUpdater} from 'chrome://resources/cr_components/color_change_listener/colors_css_updater.js';
import {HelpBubbleMixinLit} from 'chrome://resources/cr_components/help_bubble/help_bubble_mixin_lit.js';
import type {CrToastElement} from 'chrome://resources/cr_elements/cr_toast/cr_toast.js';
import type {ClickInfo} from 'chrome://resources/js/browser_command.mojom-webui.js';
import {Command} from 'chrome://resources/js/browser_command.mojom-webui.js';
import {BrowserCommandProxy} from 'chrome://resources/js/browser_command/browser_command_proxy.js';
import {hexColorToSkColor, skColorToRgba} from 'chrome://resources/js/color_utils.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {FocusOutlineManager} from 'chrome://resources/js/focus_outline_manager.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {getTrustedScriptURL} from 'chrome://resources/js/static_types.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 {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
import {getCss} from './app.css.js';
import {getHtml} from './app.html.js';
import {BackgroundManager} from './background_manager.js';
import {CustomizeDialogPage} from './customize_dialog_types.js';
import type {IframeElement} from './iframe.js';
import type {LogoElement} from './logo.js';
import {recordDuration, recordLoadDuration} from './metrics_utils.js';
import type {PageCallbackRouter, PageHandlerRemote, Theme} from './new_tab_page.mojom-webui.js';
import {CustomizeChromeSection, IphFeature, NtpBackgroundImageSource} from './new_tab_page.mojom-webui.js';
import {NewTabPageProxy} from './new_tab_page_proxy.js';
import {$$} from './utils.js';
import {Action as VoiceAction, recordVoiceAction} from './voice_search_overlay.js';
import {WindowProxy} from './window_proxy.js';
interface ExecutePromoBrowserCommandData {
commandId: Command;
clickInfo: ClickInfo;
}
interface CanShowPromoWithBrowserCommandData {
frameType: string;
messageType: string;
commandId: Command;
}
/**
* Elements on the NTP. This enum must match the numbering for NTPElement in
* enums.xml. These values are persisted to logs. Entries should not be
* renumbered, removed or reused.
*/
export enum NtpElement {
OTHER = 0,
BACKGROUND = 1,
ONE_GOOGLE_BAR = 2,
LOGO = 3,
REALBOX = 4,
MOST_VISITED = 5,
MIDDLE_SLOT_PROMO = 6,
MODULE = 7,
CUSTOMIZE = 8, // Obsolete
CUSTOMIZE_BUTTON = 9,
CUSTOMIZE_DIALOG = 10, // Obsolete
WALLPAPER_SEARCH_BUTTON = 11,
MAX_VALUE = WALLPAPER_SEARCH_BUTTON,
}
/**
* Customize Chrome entry points. This enum must match the numbering for
* NtpCustomizeChromeEntryPoint in enums.xml. These values are persisted to
* logs. Entries should not be renumbered, removed or reused.
*/
export enum NtpCustomizeChromeEntryPoint {
CUSTOMIZE_BUTTON = 0,
MODULE = 1,
URL = 2,
WALLPAPER_SEARCH_BUTTON = 3,
MAX_VALUE = WALLPAPER_SEARCH_BUTTON,
}
const CUSTOMIZE_URL_PARAM: string = 'customize';
const OGB_IFRAME_ORIGIN = 'chrome-untrusted://new-tab-page';
export const CUSTOMIZE_CHROME_BUTTON_ELEMENT_ID =
'NewTabPageUI::kCustomizeChromeButtonElementId';
// 900px ~= 561px (max value for --ntp-search-box-width) * 1.5 + some margin.
const realboxCanShowSecondarySideMediaQueryList =
window.matchMedia('(min-width: 900px)');
function recordClick(element: NtpElement) {
chrome.metricsPrivate.recordEnumerationValue(
'NewTabPage.Click', element, NtpElement.MAX_VALUE + 1);
}
function recordCustomizeChromeOpen(element: NtpCustomizeChromeEntryPoint) {
chrome.metricsPrivate.recordEnumerationValue(
'NewTabPage.CustomizeChromeOpened', element,
NtpCustomizeChromeEntryPoint.MAX_VALUE + 1);
}
// Adds a <script> tag that holds the lazy loaded code.
function ensureLazyLoaded() {
const script = document.createElement('script');
script.type = 'module';
script.src = getTrustedScriptURL`./lazy_load.js`;
document.body.appendChild(script);
}
const AppElementBase = HelpBubbleMixinLit(CrLitElement);
export interface AppElement {
$: {
oneGoogleBarClipPath: HTMLElement,
logo: LogoElement,
};
}
export class AppElement extends AppElementBase {
static get is() {
return 'ntp-app';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
oneGoogleBarIframeOrigin_: {type: String},
oneGoogleBarIframePath_: {type: String},
oneGoogleBarLoaded_: {type: Boolean},
theme_: {type: Object},
showCustomize_: {type: Boolean},
showCustomizeChromeText_: {type: Boolean},
showWallpaperSearch_: {
type: Boolean,
reflect: true,
},
selectedCustomizeDialogPage_: {type: String},
showVoiceSearchOverlay_: {type: Boolean},
showBackgroundImage_: {
reflect: true,
type: Boolean,
},
backgroundImageAttribution1_: {type: String},
backgroundImageAttribution2_: {type: String},
backgroundImageAttributionUrl_: {type: String},
backgroundColor_: {type: Object},
// Used in cr-searchbox component via host-context.
colorSourceIsBaseline: {type: Boolean},
logoColor_: {type: String},
singleColoredLogo_: {type: Boolean},
/**
* Whether the secondary side can be shown based on the feature state and
* the width available to the dropdown for the ntp searchbox.
*/
realboxCanShowSecondarySide: {
type: Boolean,
reflect: true,
},
/**
* Whether the searchbox secondary side was at any point available to
* be shown.
*/
realboxHadSecondarySide: {
type: Boolean,
reflect: true,
notify: true,
},
realboxIsTall_: {
type: Boolean,
reflect: true,
},
realboxShown_: {type: Boolean},
/* Searchbox width behavior. */
searchboxWidthBehavior_: {
type: String,
reflect: true,
},
logoEnabled_: {type: Boolean},
oneGoogleBarEnabled_: {type: Boolean},
shortcutsEnabled_: {type: Boolean},
singleRowShortcutsEnabled_: {type: Boolean},
middleSlotPromoEnabled_: {type: Boolean},
modulesEnabled_: {type: Boolean},
modulesRedesignedEnabled_: {
type: Boolean,
reflect: true,
},
wideModulesEnabled_: {
type: Boolean,
reflect: true,
},
middleSlotPromoLoaded_: {type: Boolean},
modulesLoaded_: {type: Boolean},
modulesShownToUser: {
type: Boolean,
reflect: true,
},
/**
* In order to avoid flicker, the promo and modules are hidden until both
* are loaded. If modules are disabled, the promo is shown as soon as it
* is loaded.
*/
promoAndModulesLoaded_: {type: Boolean},
showLensUploadDialog_: {type: Boolean},
/**
* If true, renders additional elements that were not deemed crucial to
* to show up immediately on load.
*/
lazyRender_: {type: Boolean},
scrolledToTop_: {type: Boolean},
wallpaperSearchButtonAnimationEnabled_: {
type: Boolean,
reflect: true,
},
wallpaperSearchButtonEnabled_: {
type: Boolean,
reflect: true,
},
};
}
protected oneGoogleBarIframeOrigin_: string = OGB_IFRAME_ORIGIN;
protected oneGoogleBarIframePath_: string;
protected oneGoogleBarLoaded_: boolean;
protected theme_?: Theme;
protected showCustomize_: boolean;
protected showCustomizeChromeText_: boolean;
protected showWallpaperSearch_: boolean = false;
private selectedCustomizeDialogPage_: string|null;
protected showVoiceSearchOverlay_: boolean = false;
protected showBackgroundImage_: boolean;
protected backgroundImageAttribution1_: string;
protected backgroundImageAttribution2_: string;
protected backgroundImageAttributionUrl_: string;
protected backgroundColor_: SkColor|null;
protected colorSourceIsBaseline: boolean;
protected logoColor_: SkColor|null = null;
protected singleColoredLogo_: boolean;
realboxCanShowSecondarySide: boolean;
realboxHadSecondarySide: boolean;
protected realboxIsTall_ = loadTimeData.getBoolean('realboxIsTall');
protected realboxShown_: boolean;
protected searchboxWidthBehavior_: string =
loadTimeData.getString('searchboxWidthBehavior');
protected showLensUploadDialog_: boolean = false;
protected logoEnabled_: boolean = loadTimeData.getBoolean('logoEnabled');
protected oneGoogleBarEnabled_: boolean =
loadTimeData.getBoolean('oneGoogleBarEnabled');
protected shortcutsEnabled_: boolean =
loadTimeData.getBoolean('shortcutsEnabled');
protected singleRowShortcutsEnabled_: boolean =
loadTimeData.getBoolean('singleRowShortcutsEnabled');
private modulesFreShown: boolean;
protected middleSlotPromoEnabled_: boolean =
loadTimeData.getBoolean('middleSlotPromoEnabled');
protected modulesEnabled_: boolean =
loadTimeData.getBoolean('modulesEnabled');
protected modulesRedesignedEnabled_: boolean =
loadTimeData.getBoolean('modulesRedesignedEnabled');
protected wideModulesEnabled_ = loadTimeData.getBoolean('wideModulesEnabled');
private middleSlotPromoLoaded_: boolean = false;
private modulesLoaded_: boolean = false;
protected modulesShownToUser: boolean;
protected promoAndModulesLoaded_: boolean = false;
protected lazyRender_: boolean;
protected scrolledToTop_: boolean = document.documentElement.scrollTop <= 0;
private wallpaperSearchButtonAnimationEnabled_: boolean =
loadTimeData.getBoolean('wallpaperSearchButtonAnimationEnabled');
protected wallpaperSearchButtonEnabled_: boolean =
loadTimeData.getBoolean('wallpaperSearchButtonEnabled');
private callbackRouter_: PageCallbackRouter;
private pageHandler_: PageHandlerRemote;
private backgroundManager_: BackgroundManager;
private setThemeListenerId_: number|null = null;
private setCustomizeChromeSidePanelVisibilityListener_: number|null = null;
private setWallpaperSearchButtonVisibilityListener_: number|null = null;
private eventTracker_: EventTracker = new EventTracker();
private shouldPrintPerformance_: boolean;
private backgroundImageLoadStartEpoch_: number;
private backgroundImageLoadStart_: number = 0;
private showWebstoreToastListenerId_: number|null = null;
constructor() {
performance.mark('app-creation-start');
super();
this.callbackRouter_ = NewTabPageProxy.getInstance().callbackRouter;
this.pageHandler_ = NewTabPageProxy.getInstance().handler;
this.backgroundManager_ = BackgroundManager.getInstance();
this.shouldPrintPerformance_ =
new URLSearchParams(location.search).has('print_perf');
this.oneGoogleBarIframePath_ = (() => {
const params = new URLSearchParams();
params.set(
'paramsencoded', btoa(window.location.search.replace(/^[?]/, '&')));
return `${OGB_IFRAME_ORIGIN}/one-google-bar?${params}`;
})();
this.showCustomize_ =
WindowProxy.getInstance().url.searchParams.has(CUSTOMIZE_URL_PARAM);
this.selectedCustomizeDialogPage_ =
WindowProxy.getInstance().url.searchParams.get(CUSTOMIZE_URL_PARAM);
this.realboxCanShowSecondarySide =
realboxCanShowSecondarySideMediaQueryList.matches;
/**
* Initialized with the start of the performance timeline in case the
* background image load is not triggered by JS.
*/
this.backgroundImageLoadStartEpoch_ = performance.timeOrigin;
chrome.metricsPrivate.recordValue(
{
metricName: 'NewTabPage.Height',
type: chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LINEAR,
min: 1,
max: 1000,
buckets: 200,
},
Math.floor(window.innerHeight));
chrome.metricsPrivate.recordValue(
{
metricName: 'NewTabPage.Width',
type: chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LINEAR,
min: 1,
max: 1920,
buckets: 384,
},
Math.floor(window.innerWidth));
ColorChangeUpdater.forDocument().start();
}
override connectedCallback() {
super.connectedCallback();
realboxCanShowSecondarySideMediaQueryList.addEventListener(
'change', this.onRealboxCanShowSecondarySideChanged_.bind(this));
this.setThemeListenerId_ =
this.callbackRouter_.setTheme.addListener((theme: Theme) => {
if (!this.theme_) {
this.onThemeLoaded_(theme);
}
performance.measure('theme-set');
this.theme_ = theme;
});
this.setCustomizeChromeSidePanelVisibilityListener_ =
this.callbackRouter_.setCustomizeChromeSidePanelVisibility.addListener(
(visible: boolean) => {
this.showCustomize_ = visible;
if (!visible) {
this.showWallpaperSearch_ = false;
}
});
this.showWebstoreToastListenerId_ =
this.callbackRouter_.showWebstoreToast.addListener(() => {
if (this.showCustomize_) {
const toast = $$<CrToastElement>(this, '#webstoreToast');
if (toast) {
toast!.hidden = false;
toast!.show();
}
}
});
this.setWallpaperSearchButtonVisibilityListener_ =
this.callbackRouter_.setWallpaperSearchButtonVisibility.addListener(
(visible: boolean) => {
// We only show the button if wallpaper search is enabled when the
// NTP loads. This prevents the button from showing if Customize
// Chrome doesn't have the wallpaper search element yet.
if (!visible) {
this.wallpaperSearchButtonEnabled_ = visible;
}
});
// Open Customize Chrome if there are Customize Chrome URL params.
if (this.showCustomize_) {
this.setCustomizeChromeSidePanelVisible_(this.showCustomize_);
recordCustomizeChromeOpen(NtpCustomizeChromeEntryPoint.URL);
}
this.eventTracker_.add(window, 'message', (event: MessageEvent) => {
const data = event.data;
// Something in OneGoogleBar is sending a message that is received here.
// Need to ignore it.
if (typeof data !== 'object') {
return;
}
if ('frameType' in data && data.frameType === 'one-google-bar') {
this.handleOneGoogleBarMessage_(event);
}
});
this.eventTracker_.add(window, 'keydown', this.onWindowKeydown_.bind(this));
this.eventTracker_.add(
window, 'click', this.onWindowClick_.bind(this), /*capture=*/ true);
this.eventTracker_.add(document, 'scroll', () => {
this.scrolledToTop_ = document.documentElement.scrollTop <= 0;
});
if (loadTimeData.getString('backgroundImageUrl')) {
this.backgroundManager_.getBackgroundImageLoadTime().then(
time => {
const duration = time - this.backgroundImageLoadStartEpoch_;
recordDuration(
'NewTabPage.Images.ShownTime.BackgroundImage', duration);
if (this.shouldPrintPerformance_) {
this.printPerformanceDatum_(
'background-image-load', this.backgroundImageLoadStart_,
duration);
this.printPerformanceDatum_(
'background-image-loaded',
this.backgroundImageLoadStart_ + duration);
}
},
() => {
// Ignore. Failed to capture background image load time.
});
}
FocusOutlineManager.forDocument(document);
}
override disconnectedCallback() {
super.disconnectedCallback();
realboxCanShowSecondarySideMediaQueryList.removeEventListener(
'change', this.onRealboxCanShowSecondarySideChanged_.bind(this));
this.callbackRouter_.removeListener(this.setThemeListenerId_!);
this.callbackRouter_.removeListener(
this.setCustomizeChromeSidePanelVisibilityListener_!);
this.callbackRouter_.removeListener(this.showWebstoreToastListenerId_!);
this.callbackRouter_.removeListener(
this.setWallpaperSearchButtonVisibilityListener_!);
this.eventTracker_.removeAll();
}
override firstUpdated() {
this.pageHandler_.onAppRendered(WindowProxy.getInstance().now());
// Let the browser breathe and then render remaining elements.
WindowProxy.getInstance().waitForLazyRender().then(() => {
ensureLazyLoaded();
this.lazyRender_ = true;
});
this.printPerformance_();
performance.measure('app-creation', 'app-creation-start');
}
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
if (changedPrivateProperties.has('theme_')) {
this.showBackgroundImage_ = this.computeShowBackgroundImage_();
this.backgroundImageAttribution1_ =
this.computeBackgroundImageAttribution1_();
this.backgroundImageAttribution2_ =
this.computeBackgroundImageAttribution2_();
this.backgroundImageAttributionUrl_ =
this.computeBackgroundImageAttributionUrl_();
this.colorSourceIsBaseline = this.computeColorSourceIsBaseline();
this.logoColor_ = this.computeLogoColor_();
this.singleColoredLogo_ = this.computeSingleColoredLogo_();
}
// theme_, showBackgroundImage_
this.backgroundColor_ = this.computeBackgroundColor_();
// theme_, showLensUploadDialog_
this.realboxShown_ = this.computeRealboxShown_();
// middleSlotPromoLoaded_, modulesLoaded_
this.promoAndModulesLoaded_ = this.computePromoAndModulesLoaded_();
// wallpaperSearchButtonEnabled_, showBackgroundImage_
this.showCustomizeChromeText_ = this.computeShowCustomizeChromeText_();
}
override updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
if (changedPrivateProperties.has('lazyRender_') && this.lazyRender_) {
this.onLazyRendered_();
}
if (changedPrivateProperties.has('theme_')) {
this.onThemeChange_();
}
if (changedPrivateProperties.has('logoColor_')) {
this.style.setProperty(
'--ntp-logo-color', this.rgbaOrInherit_(this.logoColor_));
}
if (changedPrivateProperties.has('showBackgroundImage_')) {
this.onShowBackgroundImageChange_();
}
if (changedPrivateProperties.has('promoAndModulesLoaded_')) {
this.onPromoAndModulesLoadedChange_();
}
if (changedPrivateProperties.has('oneGoogleBarLoaded_') ||
changedPrivateProperties.has('theme_')) {
this.updateOneGoogleBarAppearance_();
}
}
// Called to update the OGB of relevant NTP state changes.
private updateOneGoogleBarAppearance_() {
if (this.oneGoogleBarLoaded_) {
const isNtpDarkTheme =
this.theme_ && (!!this.theme_.backgroundImage || this.theme_.isDark);
$$<IframeElement>(this, '#oneGoogleBar')!.postMessage({
type: 'updateAppearance',
// We should be using a light OGB for dark themes and vice versa.
applyLightTheme: isNtpDarkTheme,
});
}
}
private computeShowCustomizeChromeText_(): boolean {
if (this.wallpaperSearchButtonEnabled_) {
return false;
}
return !this.showBackgroundImage_;
}
private computeBackgroundImageAttribution1_(): string {
return this.theme_ && this.theme_.backgroundImageAttribution1 || '';
}
private computeBackgroundImageAttribution2_(): string {
return this.theme_ && this.theme_.backgroundImageAttribution2 || '';
}
private computeBackgroundImageAttributionUrl_(): string {
return this.theme_ && this.theme_.backgroundImageAttributionUrl ?
this.theme_.backgroundImageAttributionUrl.url :
'';
}
private computeRealboxShown_(): boolean {
// Do not show the realbox if the upload dialog is showing.
return !!this.theme_ && !this.showLensUploadDialog_;
}
private computePromoAndModulesLoaded_(): boolean {
return (!loadTimeData.getBoolean('middleSlotPromoEnabled') ||
this.middleSlotPromoLoaded_) &&
(!loadTimeData.getBoolean('modulesEnabled') || this.modulesLoaded_);
}
private onRealboxCanShowSecondarySideChanged_(e: MediaQueryListEvent) {
this.realboxCanShowSecondarySide = e.matches;
}
private async onLazyRendered_() {
// Integration tests use this attribute to determine when lazy load has
// completed.
document.documentElement.setAttribute('lazy-loaded', String(true));
this.registerHelpBubble(
CUSTOMIZE_CHROME_BUTTON_ELEMENT_ID, '#customizeButton', {fixed: true});
this.pageHandler_.maybeShowFeaturePromo(IphFeature.kCustomizeChrome);
if (this.wallpaperSearchButtonEnabled_) {
this.pageHandler_.incrementWallpaperSearchButtonShownCount();
}
}
protected onOpenVoiceSearch_() {
this.showVoiceSearchOverlay_ = true;
recordVoiceAction(VoiceAction.ACTIVATE_SEARCH_BOX);
}
protected onOpenLensSearch_() {
this.showLensUploadDialog_ = true;
}
protected onCloseLensSearch_() {
this.showLensUploadDialog_ = false;
}
protected onCustomizeClick_() {
// Let side panel decide what page or section to show.
this.selectedCustomizeDialogPage_ = null;
this.setCustomizeChromeSidePanelVisible_(!this.showCustomize_);
if (!this.showCustomize_) {
this.pageHandler_.incrementCustomizeChromeButtonOpenCount();
recordCustomizeChromeOpen(NtpCustomizeChromeEntryPoint.CUSTOMIZE_BUTTON);
}
}
protected onWallpaperSearchClick_() {
// Close the side panel if Wallpaper Search is open.
if (this.showCustomize_ && this.showWallpaperSearch_) {
this.selectedCustomizeDialogPage_ = null;
this.setCustomizeChromeSidePanelVisible_(!this.showCustomize_);
return;
}
// Open Wallpaper Search if the side panel is closed. Otherwise, navigate
// the side panel to Wallpaper Search.
this.selectedCustomizeDialogPage_ = CustomizeDialogPage.WALLPAPER_SEARCH;
this.showWallpaperSearch_ = true;
this.setCustomizeChromeSidePanelVisible_(this.showWallpaperSearch_);
if (!this.showCustomize_) {
this.pageHandler_.incrementCustomizeChromeButtonOpenCount();
recordCustomizeChromeOpen(
NtpCustomizeChromeEntryPoint.WALLPAPER_SEARCH_BUTTON);
}
}
protected onVoiceSearchOverlayClose_() {
this.showVoiceSearchOverlay_ = false;
}
/**
* Handles <CTRL> + <SHIFT> + <.> (also <CMD> + <SHIFT> + <.> on mac) to open
* voice search.
*/
private onWindowKeydown_(e: KeyboardEvent) {
let ctrlKeyPressed = e.ctrlKey;
// <if expr="is_macosx">
ctrlKeyPressed = ctrlKeyPressed || e.metaKey;
// </if>
if (ctrlKeyPressed && e.code === 'Period' && e.shiftKey) {
this.showVoiceSearchOverlay_ = true;
recordVoiceAction(VoiceAction.ACTIVATE_KEYBOARD);
}
}
private rgbaOrInherit_(skColor: SkColor|null): string {
return skColor ? skColorToRgba(skColor) : 'inherit';
}
private computeShowBackgroundImage_(): boolean {
return !!this.theme_ && !!this.theme_.backgroundImage;
}
private onShowBackgroundImageChange_() {
this.backgroundManager_.setShowBackgroundImage(this.showBackgroundImage_);
}
private onThemeChange_() {
if (this.theme_) {
this.backgroundManager_.setBackgroundColor(this.theme_.backgroundColor);
this.style.setProperty(
'--color-new-tab-page-attribution-foreground',
this.rgbaOrInherit_(this.theme_.textColor));
this.style.setProperty(
'--color-new-tab-page-most-visited-foreground',
this.rgbaOrInherit_(this.theme_.textColor));
}
this.updateBackgroundImagePath_();
}
private onThemeLoaded_(theme: Theme) {
chrome.metricsPrivate.recordSparseValueWithPersistentHash(
'NewTabPage.Collections.IdOnLoad',
theme.backgroundImageCollectionId ?? '');
if (!theme.backgroundImage || !theme.backgroundImage.imageSource) {
chrome.metricsPrivate.recordEnumerationValue(
'NewTabPage.BackgroundImageSource', NtpBackgroundImageSource.kNoImage,
NtpBackgroundImageSource.MAX_VALUE + 1);
return;
} else {
chrome.metricsPrivate.recordEnumerationValue(
'NewTabPage.BackgroundImageSource', theme.backgroundImage.imageSource,
NtpBackgroundImageSource.MAX_VALUE + 1);
}
if (theme.backgroundImage.imageSource ===
NtpBackgroundImageSource.kWallpaperSearch ||
theme.backgroundImage.imageSource ===
NtpBackgroundImageSource.kWallpaperSearchInspiration) {
this.wallpaperSearchButtonAnimationEnabled_ = false;
}
}
private onPromoAndModulesLoadedChange_() {
if (this.promoAndModulesLoaded_ &&
loadTimeData.getBoolean('modulesEnabled')) {
recordLoadDuration(
'NewTabPage.Modules.ShownTime', WindowProxy.getInstance().now());
}
}
/**
* Set the #backgroundImage |path| only when different and non-empty. Reset
* the customize dialog background selection if the dialog is closed.
*
* The ntp-untrusted-iframe |path| is set directly. When using a data binding
* instead, the quick updates to the |path| result in iframe loading an error
* page.
*/
private updateBackgroundImagePath_() {
const backgroundImage = this.theme_ && this.theme_.backgroundImage;
if (backgroundImage) {
this.backgroundManager_.setBackgroundImage(backgroundImage);
}
}
private computeBackgroundColor_(): SkColor|null {
if (this.showBackgroundImage_ || !this.theme_) {
return null;
}
return this.theme_.backgroundColor;
}
private computeColorSourceIsBaseline(): boolean {
return !!this.theme_ && this.theme_.isBaseline;
}
private computeLogoColor_(): SkColor|null {
if (!this.theme_) {
return null;
}
return this.theme_.logoColor ||
(this.theme_.isDark ? hexColorToSkColor('#ffffff') : null);
}
private computeSingleColoredLogo_(): boolean {
return !!this.theme_ && (!!this.theme_.logoColor || this.theme_.isDark);
}
/**
* Sends the command received from the given source and origin to the browser.
* Relays the browser response to whether or not a promo containing the given
* command can be shown back to the source promo frame. |commandSource| and
* |commandOrigin| are used only to send the response back to the source promo
* frame and should not be used for anything else.
* @param messageData Data received from the source promo frame.
* @param commandSource Source promo frame.
* @param commandOrigin Origin of the source promo frame.
*/
private canShowPromoWithBrowserCommand_(
messageData: CanShowPromoWithBrowserCommandData, commandSource: Window,
commandOrigin: string) {
// Make sure we don't send unsupported commands to the browser.
/** @type {!Command} */
const commandId = Object.values(Command).includes(messageData.commandId) ?
messageData.commandId :
Command.kUnknownCommand;
BrowserCommandProxy.getInstance().handler.canExecuteCommand(commandId).then(
({canExecute}) => {
const response = {
messageType: messageData.messageType,
[messageData.commandId]: canExecute,
};
commandSource.postMessage(response, commandOrigin);
});
}
/**
* Sends the command and the accompanying mouse click info received from the
* promo of the given source and origin to the browser. Relays the execution
* status response back to the source promo frame. |commandSource| and
* |commandOrigin| are used only to send the execution status response back to
* the source promo frame and should not be used for anything else.
* @param commandData Command and mouse click info.
* @param commandSource Source promo frame.
* @param commandOrigin Origin of the source promo frame.
*/
private executePromoBrowserCommand_(
commandData: ExecutePromoBrowserCommandData, commandSource: Window,
commandOrigin: string) {
// Make sure we don't send unsupported commands to the browser.
const commandId = Object.values(Command).includes(commandData.commandId) ?
commandData.commandId :
Command.kUnknownCommand;
BrowserCommandProxy.getInstance()
.handler.executeCommand(commandId, commandData.clickInfo)
.then(({commandExecuted}) => {
commandSource.postMessage(commandExecuted, commandOrigin);
});
}
/**
* Handles messages from the OneGoogleBar iframe. The messages that are
* handled include show bar on load and overlay updates.
*
* 'overlaysUpdated' message includes the updated array of overlay rects that
* are shown.
*/
private handleOneGoogleBarMessage_(event: MessageEvent) {
const data = event.data;
if (data.messageType === 'loaded') {
const oneGoogleBar = $$<IframeElement>(this, '#oneGoogleBar')!;
oneGoogleBar.style.clipPath = 'url(#oneGoogleBarClipPath)';
oneGoogleBar.style.zIndex = '1000';
this.oneGoogleBarLoaded_ = true;
this.pageHandler_.onOneGoogleBarRendered(WindowProxy.getInstance().now());
} else if (data.messageType === 'overlaysUpdated') {
this.$.oneGoogleBarClipPath.querySelectorAll('rect').forEach(el => {
el.remove();
});
const overlayRects = data.data as DOMRect[];
overlayRects.forEach(({x, y, width, height}) => {
const rectElement =
document.createElementNS('http://www.w3.org/2000/svg', 'rect');
// Add 8px around every rect to ensure shadows are not cutoff.
rectElement.setAttribute('x', `${x - 8}`);
rectElement.setAttribute('y', `${y - 8}`);
rectElement.setAttribute('width', `${width + 16}`);
rectElement.setAttribute('height', `${height + 16}`);
this.$.oneGoogleBarClipPath.appendChild(rectElement);
});
} else if (data.messageType === 'can-show-promo-with-browser-command') {
this.canShowPromoWithBrowserCommand_(
data, event.source as Window, event.origin);
} else if (data.messageType === 'execute-browser-command') {
this.executePromoBrowserCommand_(
data.data, event.source as Window, event.origin);
} else if (data.messageType === 'click') {
recordClick(NtpElement.ONE_GOOGLE_BAR);
}
}
protected onMiddleSlotPromoLoaded_() {
this.middleSlotPromoLoaded_ = true;
}
protected onModulesLoaded_() {
this.modulesLoaded_ = true;
}
protected onCustomizeModule_() {
this.showCustomize_ = true;
this.selectedCustomizeDialogPage_ = CustomizeDialogPage.MODULES;
recordCustomizeChromeOpen(NtpCustomizeChromeEntryPoint.MODULE);
this.setCustomizeChromeSidePanelVisible_(this.showCustomize_);
}
private setCustomizeChromeSidePanelVisible_(visible: boolean) {
let section: CustomizeChromeSection = CustomizeChromeSection.kUnspecified;
switch (this.selectedCustomizeDialogPage_) {
case CustomizeDialogPage.BACKGROUNDS:
case CustomizeDialogPage.THEMES:
section = CustomizeChromeSection.kAppearance;
break;
case CustomizeDialogPage.SHORTCUTS:
section = CustomizeChromeSection.kShortcuts;
break;
case CustomizeDialogPage.MODULES:
section = CustomizeChromeSection.kModules;
break;
case CustomizeDialogPage.WALLPAPER_SEARCH:
section = CustomizeChromeSection.kWallpaperSearch;
break;
}
this.pageHandler_.setCustomizeChromeSidePanelVisible(visible, section);
}
private printPerformanceDatum_(
name: string, time: number, auxTime: number = 0) {
if (!this.shouldPrintPerformance_) {
return;
}
console.info(
!auxTime ? `${name}: ${time}` : `${name}: ${time} (${auxTime})`);
}
/**
* Prints performance measurements to the console. Also, installs performance
* observer to continuously print performance measurements after.
*/
private printPerformance_() {
if (!this.shouldPrintPerformance_) {
return;
}
const entryTypes = ['paint', 'measure'];
const log = (entry: PerformanceEntry) => {
this.printPerformanceDatum_(
entry.name, entry.duration ? entry.duration : entry.startTime,
entry.duration && entry.startTime ? entry.startTime : 0);
};
const observer = new PerformanceObserver(list => {
list.getEntries().forEach((entry) => {
log(entry);
});
});
observer.observe({entryTypes: entryTypes});
performance.getEntries().forEach((entry) => {
if (!entryTypes.includes(entry.entryType)) {
return;
}
log(entry);
});
}
protected onWebstoreToastButtonClick_() {
window.location.assign(
`https://chrome.google.com/webstore/category/collection/chrome_color_themes?hl=${
window.navigator.language}`);
}
private onWindowClick_(e: Event) {
if (e.composedPath() && e.composedPath()[0] === $$(this, '#content')) {
recordClick(NtpElement.BACKGROUND);
return;
}
for (const target of e.composedPath()) {
switch (target) {
case $$(this, 'ntp-logo'):
recordClick(NtpElement.LOGO);
return;
case $$(this, 'cr-searchbox'):
recordClick(NtpElement.REALBOX);
return;
case $$(this, 'cr-most-visited'):
recordClick(NtpElement.MOST_VISITED);
return;
case $$(this, 'ntp-middle-slot-promo'):
recordClick(NtpElement.MIDDLE_SLOT_PROMO);
return;
case $$(this, '#modules'):
recordClick(NtpElement.MODULE);
return;
case $$(this, '#customizeButton'):
recordClick(NtpElement.CUSTOMIZE_BUTTON);
return;
case $$(this, '#wallpaperSearchButton'):
recordClick(NtpElement.WALLPAPER_SEARCH_BUTTON);
return;
}
}
recordClick(NtpElement.OTHER);
}
protected isThemeDark_(): boolean {
return !!this.theme_ && this.theme_.isDark;
}
protected showThemeAttribution_(): boolean {
return !!this.theme_?.backgroundImage?.attributionUrl;
}
protected onRealboxHadSecondarySideChanged_(
e: CustomEvent<{value: boolean}>) {
this.realboxHadSecondarySide = e.detail.value;
}
protected onModulesShownToUserChanged_(e: CustomEvent<{value: boolean}>) {
this.modulesShownToUser = e.detail.value;
}
}
declare global {
interface HTMLElementTagNameMap {
'ntp-app': AppElement;
}
}
customElements.define(AppElement.is, AppElement);