// Copyright 2020 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_button/cr_button.js';
import './iframe.js';
import './doodle_share_dialog.js';
import {assert} from 'chrome://resources/js/assert.js';
import {skColorToRgba} from 'chrome://resources/js/color_utils.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.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 type {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';
import {loadTimeData} from './i18n_setup.js';
import type {IframeElement} from './iframe.js';
import {getCss} from './logo.css.js';
import {getHtml} from './logo.html.js';
import type {Doodle, DoodleShareChannel, ImageDoodle, PageHandlerRemote} from './new_tab_page.mojom-webui.js';
import {DoodleImageType} from './new_tab_page.mojom-webui.js';
import {NewTabPageProxy} from './new_tab_page_proxy.js';
import {$$} from './utils.js';
import {WindowProxy} from './window_proxy.js';
// Shows the Google logo or a doodle if available.
export class LogoElement extends CrLitElement {
static get is() {
return 'ntp-logo';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
/**
* If true displays the Google logo single-colored.
*/
singleColored: {
reflect: true,
type: Boolean,
},
/**
* If true displays the dark mode doodle if possible.
*/
dark: {type: Boolean},
/**
* The NTP's background color. If null or undefined the NTP does not have
* a single background color, e.g. when a background image is set.
*/
backgroundColor: {type: Object},
loaded_: {type: Boolean},
doodle_: {type: Object},
imageDoodle_: {type: Object},
showLogo_: {type: Boolean},
showDoodle_: {type: Boolean},
doodleBoxed_: {
reflect: true,
type: Boolean,
},
imageUrl_: {type: String},
showAnimation_: {type: Boolean},
animationUrl_: {type: String},
iframeUrl_: {type: String},
duration_: {type: String},
height_: {type: String},
width_: {type: String},
expanded_: {type: Boolean},
showShareDialog_: {type: Boolean},
imageDoodleTabIndex_: {type: Number},
reducedLogoSpaceEnabled_: {
type: Boolean,
reflect: true,
},
};
}
singleColored: boolean = false;
dark: boolean;
backgroundColor: SkColor;
private loaded_: boolean;
protected doodle_: Doodle|null;
protected imageDoodle_: ImageDoodle|null;
protected showLogo_: boolean;
protected showDoodle_: boolean;
private doodleBoxed_: boolean;
protected imageUrl_: string;
protected showAnimation_: boolean = false;
protected animationUrl_: string;
protected iframeUrl_: string;
private duration_: string;
private height_: string;
private width_: string;
protected expanded_: boolean;
protected showShareDialog_: boolean;
protected imageDoodleTabIndex_: number;
protected reducedLogoSpaceEnabled_: boolean =
loadTimeData.getBoolean('reducedLogoSpaceEnabled');
private eventTracker_: EventTracker = new EventTracker();
private pageHandler_: PageHandlerRemote;
private imageClickParams_: string|null = null;
private interactionLogUrl_: Url|null = null;
private shareId_: string|null = null;
constructor() {
performance.mark('logo-creation-start');
super();
this.pageHandler_ = NewTabPageProxy.getInstance().handler;
this.pageHandler_.getDoodle().then(({doodle}) => {
this.doodle_ = doodle;
this.loaded_ = true;
if (this.doodle_ && this.doodle_.interactive) {
this.width_ = `${this.doodle_.interactive.width}px`;
this.height_ = `${this.doodle_.interactive.height}px`;
}
});
}
override connectedCallback() {
super.connectedCallback();
this.eventTracker_.add(window, 'message', ({data}: MessageEvent) => {
if (data['cmd'] === 'resizeDoodle') {
assert(data.duration);
this.duration_ = data.duration;
assert(data.height);
this.height_ = data.height;
assert(data.width);
this.width_ = data.width;
this.expanded_ = true;
} else if (data['cmd'] === 'sendMode') {
this.sendMode_();
}
});
// Make sure the doodle gets the mode in case it has already requested it.
this.sendMode_();
}
override disconnectedCallback() {
super.disconnectedCallback();
this.eventTracker_.removeAll();
}
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
this.imageDoodle_ = this.computeImageDoodle_();
this.imageUrl_ = this.computeImageUrl_();
this.animationUrl_ = this.computeAnimationUrl_();
this.showDoodle_ = this.computeShowDoodle_();
this.iframeUrl_ = this.computeIframeUrl_();
this.showLogo_ = this.computeShowLogo_();
this.doodleBoxed_ = this.computeDoodleBoxed_();
this.imageDoodleTabIndex_ = this.computeImageDoodleTabIndex_();
}
override firstUpdated() {
performance.measure('logo-creation', 'logo-creation-start');
}
override updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('dark')) {
this.onDarkChange_();
}
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
if (changedPrivateProperties.has('duration_') ||
changedPrivateProperties.has('height_') ||
changedPrivateProperties.has('width_')) {
this.onDurationHeightWidthChange_();
}
if (changedPrivateProperties.has('imageDoodle_')) {
this.onImageDoodleChange_();
}
}
private onImageDoodleChange_() {
if (this.imageDoodle_) {
this.style.setProperty(
'--ntp-logo-box-color',
skColorToRgba(this.imageDoodle_.backgroundColor));
} else {
this.style.removeProperty('--ntp-logo-box-color');
}
// Stop the animation (if it is running) and reset logging params since
// mode change constitutes a new doodle session.
this.showAnimation_ = false;
this.imageClickParams_ = null;
this.interactionLogUrl_ = null;
this.shareId_ = null;
}
private computeImageDoodle_(): ImageDoodle|null {
return this.doodle_ && this.doodle_.image &&
(this.dark ? this.doodle_.image.dark : this.doodle_.image.light) ||
null;
}
private computeShowLogo_(): boolean {
return !!this.loaded_ && !this.showDoodle_;
}
private computeShowDoodle_(): boolean {
return !!this.imageDoodle_ ||
/* We hide interactive doodles when offline. Otherwise, the iframe
would show an ugly error page. */
!!this.doodle_ && !!this.doodle_.interactive && window.navigator.onLine;
}
private computeDoodleBoxed_(): boolean {
return !this.backgroundColor ||
!!this.imageDoodle_ &&
this.imageDoodle_.backgroundColor.value !== this.backgroundColor.value;
}
/**
* Called when a simple or animated doodle was clicked. Starts animation if
* clicking preview image of animated doodle. Otherwise, opens
* doodle-associated URL in new tab/window.
*/
protected onImageClick_() {
if ($$<HTMLElement>(this, '#imageDoodle')!.tabIndex < 0) {
return;
}
if (this.isCtaImageShown_()) {
this.showAnimation_ = true;
this.pageHandler_.onDoodleImageClicked(
DoodleImageType.kCta, this.interactionLogUrl_);
// TODO(tiborg): This is technically not correct since we don't know if
// the animation has loaded yet. However, since the animation is loaded
// inside an iframe retrieving the proper load signal is not trivial. In
// practice this should be good enough but we could improve that in the
// future.
this.logImageRendered_(
DoodleImageType.kAnimation,
this.imageDoodle_!.animationImpressionLogUrl!);
if (!this.doodle_!.image!.onClickUrl) {
$$<HTMLElement>(this, '#imageDoodle')!.blur();
}
return;
}
assert(this.doodle_!.image!.onClickUrl);
this.pageHandler_.onDoodleImageClicked(
this.showAnimation_ ? DoodleImageType.kAnimation :
DoodleImageType.kStatic,
null);
const onClickUrl = new URL(this.doodle_!.image!.onClickUrl!.url);
if (this.imageClickParams_) {
for (const param of new URLSearchParams(this.imageClickParams_)) {
onClickUrl.searchParams.append(param[0], param[1]);
}
}
WindowProxy.getInstance().open(onClickUrl.toString());
}
protected onImageLoad_() {
this.logImageRendered_(
this.isCtaImageShown_() ? DoodleImageType.kCta :
DoodleImageType.kStatic,
this.imageDoodle_!.imageImpressionLogUrl);
}
private async logImageRendered_(type: DoodleImageType, logUrl: Url) {
const {imageClickParams, interactionLogUrl, shareId} =
await this.pageHandler_.onDoodleImageRendered(
type, WindowProxy.getInstance().now(), logUrl);
this.imageClickParams_ = imageClickParams;
this.interactionLogUrl_ = interactionLogUrl;
this.shareId_ = shareId;
}
protected onImageKeydown_(e: KeyboardEvent) {
if ([' ', 'Enter'].includes(e.key)) {
this.onImageClick_();
}
}
protected onShare_(e: CustomEvent<DoodleShareChannel>) {
const doodleId =
new URL(this.doodle_!.image!.onClickUrl!.url).searchParams.get('ct');
if (!doodleId) {
return;
}
this.pageHandler_.onDoodleShared(e.detail, doodleId, this.shareId_);
}
private isCtaImageShown_(): boolean {
return !this.showAnimation_ && !!this.imageDoodle_ &&
!!this.imageDoodle_.animationUrl;
}
/**
* Sends a postMessage to the interactive doodle whether the current theme is
* dark or light. Won't do anything if we don't have an interactive doodle or
* we haven't been told yet whether the current theme is dark or light.
*/
private sendMode_() {
const iframe = $$<IframeElement>(this, '#iframe');
if (this.dark === undefined || !iframe) {
return;
}
iframe.postMessage({cmd: 'changeMode', dark: this.dark});
}
private onDarkChange_() {
this.sendMode_();
}
private computeImageUrl_(): string {
return this.imageDoodle_ ? this.imageDoodle_.imageUrl.url : '';
}
private computeAnimationUrl_(): string {
return this.imageDoodle_ && this.imageDoodle_.animationUrl ?
`chrome-untrusted://new-tab-page/image?${
this.imageDoodle_.animationUrl.url}` :
'';
}
private computeIframeUrl_(): string {
if (this.doodle_ && this.doodle_.interactive) {
const url = new URL(this.doodle_.interactive.url.url);
url.searchParams.append('theme_messages', '0');
return url.href;
} else {
return '';
}
}
protected onShareButtonClick_(e: Event) {
e.stopPropagation();
this.showShareDialog_ = true;
}
protected onShareDialogClose_() {
this.showShareDialog_ = false;
}
private onDurationHeightWidthChange_() {
this.style.setProperty('--duration', this.duration_);
this.style.setProperty('--height', this.height_);
this.style.setProperty('--width', this.width_);
}
private computeImageDoodleTabIndex_(): number {
return (this.doodle_ && this.doodle_.image &&
(this.isCtaImageShown_() || this.doodle_.image.onClickUrl)) ?
0 :
-1;
}
}
declare global {
interface HTMLElementTagNameMap {
'ntp-logo': LogoElement;
}
}
customElements.define(LogoElement.is, LogoElement);