Commit 97c6133d authored by Tibor Goldschwendt's avatar Tibor Goldschwendt Committed by Commit Bot

[webui][ntp] Support animated doodles and image doodle clicks

Animated doodles show a static preview image first and an animated image
(e.g. a gif) on click. Clicking the animation of an animated doodle or
the image of a simple doodle opens a doodle-associated URL in a new
tab/window.

To support displaying the animated image (which is external content) this
CL redefines the chrome-untrusted://new-tab-page/image?<url> helper that
now behaves like an img tag. The previous
chrome-untrusted://new-tab-page/image?<url> moved to
chrome-untrusted://new-tab-page/background_image?<url>.

Bug: 1039910
Change-Id: I57a6e0ca07271e5b9a055791a545d031d8694dc1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2134695Reviewed-by: default avatarAlex Gough <ajgo@chromium.org>
Reviewed-by: default avatarEsmael Elmoslimany <aee@chromium.org>
Commit-Queue: Tibor Goldschwendt <tiborg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#756776}
parent 3b3edd9e
......@@ -53,7 +53,7 @@
}
#logo {
margin-bottom: 38px;
margin-bottom: 8px;
}
ntp-fakebox {
......
......@@ -163,7 +163,7 @@ class AppElement extends PolymerElement {
if (!this.theme_ || !this.theme_.backgroundImageUrl) {
return '';
}
return `image?${this.theme_.backgroundImageUrl.url}`;
return `background_image?${this.theme_.backgroundImageUrl.url}`;
}
/**
......
......@@ -30,6 +30,11 @@ export class BrowserProxy {
window.location.href = href;
}
/** @param {string} url */
open(url) {
window.open(url, '_blank');
}
/**
* @param {function()} callback
* @param {number} duration
......
......@@ -40,7 +40,7 @@
<div class="tile" tabindex="0" title="[[item.label]]" role="button"
on-click="onCollectionClick_">
<ntp-untrusted-iframe class="image"
path="image?[[item.previewImageUrl.url]]">
path="background_image?[[item.previewImageUrl.url]]">
</ntp-untrusted-iframe>
<div class="label">[[item.label]]</div>
</div>
......@@ -52,7 +52,7 @@
<template>
<div class="tile" tabindex="0" title="[[item.label]]" role="button">
<ntp-untrusted-iframe class="image"
path="image?[[item.previewImageUrl.url]]">
path="background_image?[[item.previewImageUrl.url]]">
</ntp-untrusted-iframe>
</div>
</template>
......
<style>
:host(:not([hidden])) {
<style include="cr-hidden-style">
:host {
display: inline-block;
min-height: 230px;
}
#singleColoredLogo,
......@@ -21,8 +22,32 @@
background-image: url(chrome://resources/images/google_logo.svg);
}
#imageContainer {
cursor: pointer;
display: grid;
height: 230px;
width: 500px;
}
#imageContainer > * {
grid-column-start: 1;
grid-row-start: 1;
}
#image {
max-height: 100%;
max-width: 100%;
}
#animation {
height: 100%;
pointer-events: none;
width: 100%;
}
#iframe {
height: var(--height, 200px);
margin-bottom: 30px;
transition-duration: var(--duration, 100ms);
transition-property: height, width;
width: var(--width, 500px);
......@@ -35,7 +60,17 @@
<div id="multiColoredLogo" hidden="[[singleColored]]"></div>
</div>
<div id="doodle">
<img id="image" src="[[imageUrl_]]" hidden="[[!imageUrl_]]"></img>
<div id="imageContainer" hidden="[[!doodle_.content.imageDoodle]]"
on-click="onImageClick_">
<!-- The static image is always visible and the animated image is stacked
on top of the static image so that there is no flicker when starting
the animation. -->
<img id="image" src="[[imageUrl_]]">
</img>
<ntp-untrusted-iframe id="animation" path="[[animationUrl_]]"
hidden="[[!showAnimation_]]">
</ntp-untrusted-iframe>
</div>
<ntp-untrusted-iframe id="iframe" path="[[iframeUrl_]]"
hidden="[[!iframeUrl_]]"
style="--duration: [[valueOrUnset_(duration_)]];
......
......@@ -2,6 +2,7 @@
// 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/hidden_style_css.m.js';
import 'chrome://resources/polymer/v3_0/iron-pages/iron-pages.js';
import './untrusted_iframe.js';
......@@ -60,6 +61,18 @@ class LogoElement extends PolymerElement {
type: String,
},
/** @private */
showAnimation_: {
type: Boolean,
value: false,
},
/** @private */
animationUrl_: {
computed: 'computeAnimationUrl_(doodle_)',
type: String,
},
/** @private */
iframeUrl_: {
computed: 'computeIframeUrl_(doodle_)',
......@@ -134,13 +147,41 @@ class LogoElement extends PolymerElement {
return 'logo';
}
/**
* 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.
* @private
*/
onImageClick_() {
if (!this.showAnimation_ && this.doodle_ &&
this.doodle_.content.imageDoodle.animationUrl) {
this.showAnimation_ = true;
return;
}
BrowserProxy.getInstance().open(
this.doodle_.content.imageDoodle.onClickUrl.url);
}
/**
* @return {string}
* @private
*/
computeImageUrl_() {
return (this.doodle_ && this.doodle_.content.image) ?
this.doodle_.content.image :
return (this.doodle_ && this.doodle_.content.imageDoodle &&
this.doodle_.content.imageDoodle.imageUrl) ?
this.doodle_.content.imageDoodle.imageUrl.url :
'';
}
/**
* @return {string}
* @private
*/
computeAnimationUrl_() {
return (this.doodle_ && this.doodle_.content.imageDoodle &&
this.doodle_.content.imageDoodle.animationUrl) ?
`image?${this.doodle_.content.imageDoodle.animationUrl.url}` :
'';
}
......
......@@ -107,6 +107,9 @@
file="untrusted/promo.js" type="chrome_html" compress="gzip" />
<structure name="IDR_NEW_TAB_PAGE_UNTRUSTED_IMAGE_HTML"
file="untrusted/image.html" type="chrome_html" compress="gzip" />
<structure name="IDR_NEW_TAB_PAGE_UNTRUSTED_BACKGROUND_IMAGE_HTML"
file="untrusted/background_image.html" type="chrome_html"
compress="gzip" />
<structure name="IDR_NEW_TAB_PAGE_UNTRUSTED_IFRAME_HTML"
file="untrusted/iframe.html" type="chrome_html" compress="gzip" />
</structures>
......
<!doctype html>
<html>
<head>
<style>
html,
body {
height: 100%;
width: 100%;
}
body {
background-image: url($i18nRaw{url});
background-position: center;
background-size: cover;
margin: 0
}
</style>
</head>
<body></body>
</html>
......@@ -9,12 +9,16 @@
}
body {
background-image: url($i18nRaw{url});
background-position: center;
background-size: cover;
margin: 0
}
img {
max-height: 100%;
max-width: 100%;
}
</style>
</head>
<body></body>
<body>
<img src="$i18nRaw{url}"></img>
</body>
</html>
......@@ -111,10 +111,20 @@ struct Theme {
ThemeInfo info;
};
// The contents of simple and animated doodles.
struct ImageDoodleContent {
// Doodle image encoded as data URL.
url.mojom.Url image_url;
// URL opened in new tab when the doodle is clicked.
url.mojom.Url on_click_url;
// URL pointing to animated content (e.g. gif). Only set for animated doodles.
url.mojom.Url? animation_url;
};
// The contents of a doodle.
union DoodleContent {
// Doodle image encoded as data URL. Set for static and animated doodles.
string image;
// Set for simple and animated doodles.
ImageDoodleContent image_doodle;
// URL pointing to doodle page. Set for interactive doodles.
url.mojom.Url url;
};
......
......@@ -418,27 +418,30 @@ void NewTabPageHandler::OnLogoAvailable(
return;
}
auto doodle = new_tab_page::mojom::Doodle::New();
switch (logo->metadata.type) {
case search_provider_logos::LogoType::SIMPLE:
case search_provider_logos::LogoType::ANIMATED: {
if (!logo->encoded_image) {
std::move(callback).Run(nullptr);
return;
}
std::string base64;
base::Base64Encode(logo->encoded_image->data(), &base64);
auto data_url =
base::StringPrintf("data:%s;base64,%s",
logo->metadata.mime_type.c_str(), base64.c_str());
doodle->content = new_tab_page::mojom::DoodleContent::NewImage(data_url);
} break;
case search_provider_logos::LogoType::INTERACTIVE:
doodle->content = new_tab_page::mojom::DoodleContent::NewUrl(
logo->metadata.full_page_url);
break;
default:
if (logo->metadata.type == search_provider_logos::LogoType::SIMPLE ||
logo->metadata.type == search_provider_logos::LogoType::ANIMATED) {
if (!logo->encoded_image) {
std::move(callback).Run(nullptr);
return;
}
auto image_doodle_content = new_tab_page::mojom::ImageDoodleContent::New();
std::string base64;
base::Base64Encode(logo->encoded_image->data(), &base64);
image_doodle_content->image_url = GURL(base::StringPrintf(
"data:%s;base64,%s", logo->metadata.mime_type.c_str(), base64.c_str()));
image_doodle_content->on_click_url = logo->metadata.on_click_url;
if (logo->metadata.type == search_provider_logos::LogoType::ANIMATED) {
image_doodle_content->animation_url = logo->metadata.animated_url;
}
doodle->content = new_tab_page::mojom::DoodleContent::NewImageDoodle(
std::move(image_doodle_content));
} else if (logo->metadata.type ==
search_provider_logos::LogoType::INTERACTIVE) {
doodle->content = new_tab_page::mojom::DoodleContent::NewUrl(
logo->metadata.full_page_url);
} else {
std::move(callback).Run(nullptr);
return;
}
std::move(callback).Run(std::move(doodle));
}
......@@ -98,21 +98,17 @@ void UntrustedSource::StartDataRequest(
bundle.LoadDataResourceBytes(IDR_NEW_TAB_PAGE_UNTRUSTED_PROMO_JS));
return;
}
if (path == "image" && url_param.is_valid() &&
url_param.SchemeIs(url::kHttpsScheme)) {
if ((path == "image" || path == "background_image" || path == "iframe") &&
url_param.is_valid() && url_param.SchemeIs(url::kHttpsScheme)) {
ui::TemplateReplacements replacements;
replacements["url"] = url_param.spec();
std::string html =
FormatTemplate(IDR_NEW_TAB_PAGE_UNTRUSTED_IMAGE_HTML, replacements);
std::move(callback).Run(base::RefCountedString::TakeString(&html));
return;
}
if (path == "iframe" && url_param.is_valid() &&
url_param.SchemeIs(url::kHttpsScheme)) {
ui::TemplateReplacements replacements;
replacements["url"] = url_param.spec();
std::string html =
FormatTemplate(IDR_NEW_TAB_PAGE_UNTRUSTED_IFRAME_HTML, replacements);
int resource_id =
(path == "image")
? IDR_NEW_TAB_PAGE_UNTRUSTED_IMAGE_HTML
: (path == "background_image")
? IDR_NEW_TAB_PAGE_UNTRUSTED_BACKGROUND_IMAGE_HTML
: IDR_NEW_TAB_PAGE_UNTRUSTED_IFRAME_HTML;
std::string html = FormatTemplate(resource_id, replacements);
std::move(callback).Run(base::RefCountedString::TakeString(&html));
return;
}
......@@ -149,7 +145,7 @@ bool UntrustedSource::ShouldServiceRequest(
const std::string path = url.path().substr(1);
return path == "one-google-bar" || path == "one_google_bar.js" ||
path == "promo" || path == "promo.js" || path == "image" ||
path == "iframe";
path == "background_image" || path == "iframe";
}
void UntrustedSource::OnOneGoogleBarDataUpdated() {
......
......@@ -21,6 +21,17 @@ class Profile;
// from outside the chromium codebase. The chrome-untrusted://new-tab-page/*
// sources can only be embedded in the chrome://new-tab-page by using an
// <iframe>.
//
// Offers the following helpers to embed content into chrome://new-tab-page in a
// generalized way:
// * chrome-untrusted://new-tab-page/image?<url>: Behaves like an img element
// with src set to <url>.
// * chrome-untrusted://new-tab-page/background_image?<url>: Behaves like an
// element that has <url> set as the background image, such that the image
// will cover the entire element.
// * chrome-untrusted://new-tab-page/iframe?<url>: Behaves like an iframe with
// src set to <url>.
// Each of those helpers only accept HTTPS URLs.
class UntrustedSource : public content::URLDataSource,
public OneGoogleBarServiceObserver,
public PromoServiceObserver {
......
......@@ -129,7 +129,8 @@ suite('NewTabPageAppTest', () => {
assertNotStyle(app.$.backgroundImage, 'display', 'none');
assertNotStyle(app.$.backgroundGradient, 'display', 'none');
assertNotStyle(app.$.backgroundImageAttribution, 'text-shadow', 'none');
assertEquals(app.$.backgroundImage.path, 'image?https://img.png');
assertEquals(
app.$.backgroundImage.path, 'background_image?https://img.png');
assertFalse(app.$.logo.doodleAllowed);
});
......
......@@ -57,7 +57,8 @@ suite('NewTabPageCustomizeBackgroundsTest', () => {
assertEquals(tiles.length, 1);
assertEquals(tiles[0].getAttribute('title'), 'col_0');
assertEquals(
tiles[0].querySelector('.image').path, 'image?https://col_0.jpg');
tiles[0].querySelector('.image').path,
'background_image?https://col_0.jpg');
});
test('clicking collection selects collection', async function() {
......@@ -112,7 +113,7 @@ suite('NewTabPageCustomizeBackgroundsTest', () => {
assertEquals(tiles.length, 1);
assertEquals(
tiles[0].querySelector('.image').path,
'image?https://example.com/image.png');
'background_image?https://example.com/image.png');
});
test('Going back shows collections', async function() {
......
......@@ -32,15 +32,37 @@ suite('NewTabPageLogoTest', () => {
BrowserProxy.instance_ = testProxy;
});
test('setting static, animated doodle shows image', async () => {
test('setting simple doodle shows image', async () => {
// Act.
const logo = await createLogo({content: {image: 'data:foo'}});
const logo = await createLogo(
{content: {imageDoodle: {imageUrl: {url: 'data:foo'}}}});
// Assert.
assertNotStyle(logo.$.doodle, 'display', 'none');
assertStyle(logo.$.logo, 'display', 'none');
assertEquals(logo.$.image.src, 'data:foo');
assertNotStyle(logo.$.image, 'display', 'none');
assertStyle(logo.$.animation, 'display', 'none');
assertStyle(logo.$.iframe, 'display', 'none');
});
test('setting animated doodle shows image', async () => {
// Act.
const logo = await createLogo({
content: {
imageDoodle: {
imageUrl: {url: 'data:foo'},
animationUrl: {url: 'https://foo.com'},
}
}
});
// Assert.
assertNotStyle(logo.$.doodle, 'display', 'none');
assertStyle(logo.$.logo, 'display', 'none');
assertEquals(logo.$.image.src, 'data:foo');
assertNotStyle(logo.$.image, 'display', 'none');
assertStyle(logo.$.animation, 'display', 'none');
assertStyle(logo.$.iframe, 'display', 'none');
});
......@@ -53,7 +75,7 @@ suite('NewTabPageLogoTest', () => {
assertStyle(logo.$.logo, 'display', 'none');
assertEquals(logo.$.iframe.path, 'iframe?https://foo.com');
assertNotStyle(logo.$.iframe, 'display', 'none');
assertStyle(logo.$.image, 'display', 'none');
assertStyle(logo.$.imageContainer, 'display', 'none');
});
test('disallowing doodle shows logo', async () => {
......@@ -160,4 +182,65 @@ suite('NewTabPageLogoTest', () => {
assertEquals(logo.$.iframe.offsetHeight, height);
assertEquals(logo.$.iframe.offsetWidth, width);
});
test('clicking simple doodle opens link', async () => {
// Arrange.
const logo = await createLogo({
content: {
imageDoodle: {
imageUrl: {url: 'data:foo'},
onClickUrl: {url: 'https://foo.com'},
}
}
});
// Act.
logo.$.image.click();
const url = await testProxy.whenCalled('open');
// Assert.
assertEquals(url, 'https://foo.com');
});
test('clicking image of animated doodle starts animation', async () => {
// Arrange.
const logo = await createLogo({
content: {
imageDoodle: {
imageUrl: {url: 'data:foo'},
animationUrl: {url: 'https://foo.com'},
}
}
});
// Act.
logo.$.image.click();
// Assert.
assertEquals(testProxy.getCallCount('open'), 0);
assertNotStyle(logo.$.image, 'display', 'none');
assertNotStyle(logo.$.animation, 'display', 'none');
assertEquals(logo.$.animation.path, 'image?https://foo.com');
});
test('clicking animation of animated doodle opens link', async () => {
// Arrange.
const logo = await createLogo({
content: {
imageDoodle: {
imageUrl: {url: 'data:foo'},
animationUrl: {url: 'https://foo.com'},
onClickUrl: {url: 'https://bar.com'},
}
}
});
logo.$.image.click();
// Act.
logo.$.animation.click();
const url = await testProxy.whenCalled('open');
// Assert.
assertEquals(url, 'https://bar.com');
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment