Commit e0b2c312 authored by dbeam@chromium.org's avatar dbeam@chromium.org

[NTP4] Redesign of notification promo.

R=estade@chromium.org
BUG=110912
TEST=Notification promo still works but looks cooler.

Review URL: https://chromiumcodereview.appspot.com/9318017

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@120755 0039d316-1c4b-4281-b951-d872f2087c98
parent d9e3dc67
...@@ -212,3 +212,9 @@ html[dir='rtl'] .apps-promo-extras { ...@@ -212,3 +212,9 @@ html[dir='rtl'] .apps-promo-extras {
html.dragging-mode .app-install-hint { html.dragging-mode .app-install-hint {
opacity: 0; opacity: 0;
} }
/* Move the notification lower on apps pages to account for the 16px of
* transparency each app icon should have. */
.apps-page #notification-container {
bottom: 15px;
}
...@@ -12,7 +12,7 @@ html { ...@@ -12,7 +12,7 @@ html {
} }
body { body {
/* Don't highlight links when they're tapped. Safari has bugs here that /* Don't highlight links when they're tapped. Safari has bugs here that
show up as flicker when dragging in some situations */ show up as flicker when dragging in some situations */
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
/* Don't allow selecting text - can occur when dragging */ /* Don't allow selecting text - can occur when dragging */
...@@ -27,28 +27,33 @@ body { ...@@ -27,28 +27,33 @@ body {
} }
#notification-container { #notification-container {
-webkit-transition-duration: 100ms; -webkit-transition: opacity 200ms, margin-left 200ms;
-webkit-transition-property: opacity; bottom: 31px;
display: block; display: block;
margin-top: 2px; float: left;
position: relative; position: relative;
text-align: center; text-align: start;
z-index: 15; z-index: 15;
} }
html[dir='rtl'] #notification-container {
float: right;
}
#notification-container.card-changed {
-webkit-transition: none;
opacity: 0;
}
#notification-container.inactive { #notification-container.inactive {
-webkit-transition-duration: 200ms; -webkit-transition: opacity 200ms;
opacity: 0; opacity: 0;
} }
#notification { #notification {
background-color: rgb(255, 241, 153);
border: 1px solid lightGrey;
border-radius: 6px;
color: black; color: black;
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
padding: 7px 15px;
} }
#notification > div > div, #notification > div > div,
...@@ -56,6 +61,12 @@ body { ...@@ -56,6 +61,12 @@ body {
display: inline-block; display: inline-block;
} }
/* NOTE: This is in the probable case that we start stuffing 16x16 data URI'd
* icons in the promo notification responses. */
#notification > span > img {
margin-bottom: -3px;
}
#notification .close-button { #notification .close-button {
-webkit-margin-start: 0.5em; -webkit-margin-start: 0.5em;
vertical-align: middle; vertical-align: middle;
......
...@@ -137,7 +137,7 @@ cr.define('ntp4', function() { ...@@ -137,7 +137,7 @@ cr.define('ntp4', function() {
localStrings.getString('login_status_url'); localStrings.getString('login_status_url');
$('login-status-advanced').onclick = function() { $('login-status-advanced').onclick = function() {
chrome.send('showAdvancedLoginUI'); chrome.send('showAdvancedLoginUI');
} };
$('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble); $('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble);
var bubbleContent = $('login-status-bubble-contents'); var bubbleContent = $('login-status-bubble-contents');
...@@ -153,7 +153,7 @@ cr.define('ntp4', function() { ...@@ -153,7 +153,7 @@ cr.define('ntp4', function() {
infoBubble.handleCloseEvent = function() { infoBubble.handleCloseEvent = function() {
this.hide(); this.hide();
chrome.send('introMessageDismissed'); chrome.send('introMessageDismissed');
} };
var bubbleContent = $('ntp4-intro-bubble-contents'); var bubbleContent = $('ntp4-intro-bubble-contents');
infoBubble.content = bubbleContent; infoBubble.content = bubbleContent;
...@@ -166,9 +166,16 @@ cr.define('ntp4', function() { ...@@ -166,9 +166,16 @@ cr.define('ntp4', function() {
chrome.send('introMessageSeen'); chrome.send('introMessageSeen');
} }
var serverpromo = localStrings.getString('serverpromo'); var promo = localStrings.getString('serverpromo');
if (serverpromo) { if (promo) {
showNotification(parseHtmlSubset(serverpromo), [], function() { var tags = ['IMG'];
var attrs = {
src: function(node, value) {
return node.tagName == 'IMG' &&
/^data\:image\/(?:png|gif|jpe?g)/.test(value);
},
};
showNotification(parseHtmlSubset(promo, tags, attrs), [], function() {
chrome.send('closeNotificationPromo'); chrome.send('closeNotificationPromo');
}, 60000); }, 60000);
chrome.send('notificationPromoViewed'); chrome.send('notificationPromoViewed');
...@@ -194,6 +201,45 @@ cr.define('ntp4', function() { ...@@ -194,6 +201,45 @@ cr.define('ntp4', function() {
ntp4.APP_LAUNCH.NTP_WEBSTORE_FOOTER]); ntp4.APP_LAUNCH.NTP_WEBSTORE_FOOTER]);
} }
/*
* The number of sections to wait on.
* @type {number}
*/
var sectionsToWaitFor = 2;
/**
* Queued callbacks which lie in wait for all sections to be ready.
* @type {array}
*/
var readyCallbacks = [];
/**
* Fired as each section of pages becomes ready.
* @param {Event} e Each page's synthetic DOM event.
*/
document.addEventListener('sectionready', function(e) {
if (--sectionsToWaitFor <= 0) {
while (readyCallbacks.length) {
readyCallbacks.shift()();
}
}
});
/**
* This is used to simulate a fire-once event (i.e. $(document).ready() in
* jQuery or Y.on('domready') in YUI. If all sections are ready, the callback
* is fired right away. If all pages are not ready yet, the function is queued
* for later execution.
* @param {function} callback The work to be done when ready.
*/
function doWhenAllSectionsReady(callback) {
assert(typeof callback == 'function');
if (sectionsToWaitFor > 0)
readyCallbacks.push(callback);
else
window.setTimeout(callback, 0); // Do soon after, but asynchronously.
}
/** /**
* Fills in an invisible div with the 'Most Visited' string so that * Fills in an invisible div with the 'Most Visited' string so that
* its length may be measured and the nav dots sized accordingly. * its length may be measured and the nav dots sized accordingly.
...@@ -249,7 +295,7 @@ cr.define('ntp4', function() { ...@@ -249,7 +295,7 @@ cr.define('ntp4', function() {
* Timeout ID. * Timeout ID.
* @type {number} * @type {number}
*/ */
var notificationTimeout_ = 0; var notificationTimeout = 0;
/** /**
* Shows the notification bubble. * Shows the notification bubble.
...@@ -263,7 +309,7 @@ cr.define('ntp4', function() { ...@@ -263,7 +309,7 @@ cr.define('ntp4', function() {
* manually dismisses the notification. * manually dismisses the notification.
*/ */
function showNotification(message, links, opt_closeHandler, opt_timeout) { function showNotification(message, links, opt_closeHandler, opt_timeout) {
window.clearTimeout(notificationTimeout_); window.clearTimeout(notificationTimeout);
var span = document.querySelector('#notification > span'); var span = document.querySelector('#notification > span');
if (typeof message == 'string') { if (typeof message == 'string') {
...@@ -282,23 +328,30 @@ cr.define('ntp4', function() { ...@@ -282,23 +328,30 @@ cr.define('ntp4', function() {
link.onclick = function() { link.onclick = function() {
this.action(); this.action();
hideNotification(); hideNotification();
} };
link.setAttribute('role', 'button'); link.setAttribute('role', 'button');
link.setAttribute('tabindex', 0); link.setAttribute('tabindex', 0);
link.className = 'link-button'; link.className = 'link-button';
linksBin.appendChild(link); linksBin.appendChild(link);
} }
document.querySelector('#notification button').onclick = function(e) { function closeFunc(e) {
if (opt_closeHandler) if (opt_closeHandler)
opt_closeHandler(); opt_closeHandler();
hideNotification(); hideNotification();
}; }
document.querySelector('#notification button').onclick = closeFunc;
document.addEventListener('dragstart', closeFunc);
var timeout = opt_timeout || 10000;
notificationContainer.hidden = false; notificationContainer.hidden = false;
notificationContainer.classList.remove('inactive'); showNotificationOnCurrentPage();
notificationTimeout_ = window.setTimeout(hideNotification, timeout);
newTabView.cardSlider.frame.addEventListener(
'cardSlider:card_change_ended', onCardChangeEnded);
var timeout = opt_timeout || 10000;
notificationTimeout = window.setTimeout(hideNotification, timeout);
} }
/** /**
...@@ -306,14 +359,56 @@ cr.define('ntp4', function() { ...@@ -306,14 +359,56 @@ cr.define('ntp4', function() {
*/ */
function hideNotification() { function hideNotification() {
notificationContainer.classList.add('inactive'); notificationContainer.classList.add('inactive');
newTabView.cardSlider.frame.removeEventListener(
'cardSlider:card_change_ended', onCardChangeEnded);
}
/**
* Happens when 1 or more consecutive card changes end.
* @param {Event} e The cardSlider:card_change_ended event.
*/
function onCardChangeEnded(e) {
// If we ended on the same page as we started, ignore.
if (newTabView.cardSlider.currentCardValue.notification)
return;
// Hide the notification the old page.
notificationContainer.classList.add('card-changed');
showNotificationOnCurrentPage();
}
/**
* Move and show the notification on the current page.
*/
function showNotificationOnCurrentPage() {
var page = newTabView.cardSlider.currentCardValue;
doWhenAllSectionsReady(function() {
if (page != newTabView.cardSlider.currentCardValue)
return;
// NOTE: This moves the notification to inside of the current page.
page.notification = notificationContainer;
// Reveal the notification and instruct it to hide itself if ignored.
notificationContainer.classList.remove('inactive');
// Gives the browser time to apply this rule before we remove it (causing
// a transition).
window.setTimeout(function() {
notificationContainer.classList.remove('card-changed');
}, 0);
});
} }
/** /**
* When done fading out, set hidden to true so the notification can't be * When done fading out, set hidden to true so the notification can't be
* tabbed to or clicked. * tabbed to or clicked.
* @param {Event} e The webkitTransitionEnd event.
*/ */
function onNotificationTransitionEnd(e) { function onNotificationTransitionEnd(e) {
if (notificationContainer.classList.contains('inactive')); if (notificationContainer.classList.contains('inactive'))
notificationContainer.hidden = true; notificationContainer.hidden = true;
} }
...@@ -323,6 +418,7 @@ cr.define('ntp4', function() { ...@@ -323,6 +418,7 @@ cr.define('ntp4', function() {
function setMostVisitedPages(data, hasBlacklistedUrls) { function setMostVisitedPages(data, hasBlacklistedUrls) {
newTabView.mostVisitedPage.data = data; newTabView.mostVisitedPage.data = data;
cr.dispatchSimpleEvent(document, 'sectionready', true, true);
} }
/** /**
......
...@@ -344,6 +344,8 @@ cr.define('ntp4', function() { ...@@ -344,6 +344,8 @@ cr.define('ntp4', function() {
logEvent('apps.layout: ' + (Date.now() - startTime)); logEvent('apps.layout: ' + (Date.now() - startTime));
document.documentElement.classList.remove('starting-up'); document.documentElement.classList.remove('starting-up');
cr.dispatchSimpleEvent(document, 'sectionready', true, true);
}, },
/** /**
......
...@@ -109,6 +109,10 @@ ...@@ -109,6 +109,10 @@
-webkit-transition: margin-bottom 200ms; -webkit-transition: margin-bottom 200ms;
} }
.animating-tile-page #notification-container {
-webkit-transition: margin 200ms, opacity 200ms;
}
@-webkit-keyframes bounce { @-webkit-keyframes bounce {
0% { 0% {
-webkit-transform: scale(0, 0); -webkit-transform: scale(0, 0);
......
...@@ -502,6 +502,25 @@ cr.define('ntp4', function() { ...@@ -502,6 +502,25 @@ cr.define('ntp4', function() {
return 0; return 0;
}, },
/**
* The notification content of this tile (if any, otherwise null).
* @type {!HTMLElement}
*/
get notification() {
return this.topMargin_.nextElementSibling.id == 'notification-container' ?
this.topMargin_.nextElementSibling : null;
},
/**
* The notification content of this tile (if any, otherwise null).
* @param {!HTMLElement}
*/
set notification(node) {
assert(node instanceof HTMLElement, '|node| isn\'t an HTMLElement!');
// NOTE: Implicitly removes from DOM if |node| is inside it.
this.content_.insertBefore(node, this.topMargin_.nextElementSibling);
this.positionNotification_();
},
/** /**
* Removes the tilePage from the DOM and cleans up event handlers. * Removes the tilePage from the DOM and cleans up event handlers.
*/ */
...@@ -910,6 +929,7 @@ cr.define('ntp4', function() { ...@@ -910,6 +929,7 @@ cr.define('ntp4', function() {
this.classList.add('animating-tile-page'); this.classList.add('animating-tile-page');
this.heightChanged_(); this.heightChanged_();
this.positionNotification_();
this.repositionTiles_(); this.repositionTiles_();
}, },
...@@ -983,6 +1003,17 @@ cr.define('ntp4', function() { ...@@ -983,6 +1003,17 @@ cr.define('ntp4', function() {
(this.topMarginPx_ - this.animatedTopMarginPx_) + 'px'; (this.topMarginPx_ - this.animatedTopMarginPx_) + 'px';
}, },
/**
* Position the notification if there's one showing.
*/
positionNotification_: function() {
if (this.notification && !this.notification.hidden) {
this.notification.style.margin =
-this.notification.offsetHeight + 'px ' +
this.layoutValues_.leftMargin + 'px 0';
}
},
/** /**
* Handles final setup that can only happen after |this| is inserted into * Handles final setup that can only happen after |this| is inserted into
* the page. * the page.
......
...@@ -220,6 +220,14 @@ cr.define('cr.ui', function() { ...@@ -220,6 +220,14 @@ cr.define('cr.ui', function() {
return this.cards_[this.currentCard_]; return this.cards_[this.currentCard_];
}, },
/**
* Returns the frame holding the cards.
* @return {Element} The frame used to position the cards.
*/
get frame() {
return this.frame_;
},
/** /**
* Handle horizontal scrolls to flip between pages. * Handle horizontal scrolls to flip between pages.
* @private * @private
......
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/** /**
* Whitelist of tag names allowed in parseHtmlSubset. * Whitelist of tag names allowed in parseHtmlSubset.
* @type {[string]} * @type {[string]}
...@@ -22,17 +28,33 @@ var allowedAttributes = { ...@@ -22,17 +28,33 @@ var allowedAttributes = {
return false; return false;
node.setAttribute('target', ''); node.setAttribute('target', '');
return true; return true;
} },
} };
/** /**
* Parse a very small subset of HTML. This ensures that insecure HTML / * Parse a very small subset of HTML. This ensures that insecure HTML /
* javascript cannot be injected into the new tab page. * javascript cannot be injected into the new tab page.
* @param {string} s The string to parse. * @param {string} s The string to parse.
* @param {array=} extraTags Extra allowed tags.
* @param {object=} extraAttrs Extra allowed attributes (all tags are run
* through these).
* @throws {Error} In case of non supported markup. * @throws {Error} In case of non supported markup.
* @return {DocumentFragment} A document fragment containing the DOM tree. * @return {DocumentFragment} A document fragment containing the DOM tree.
*/ */
function parseHtmlSubset(s) { function parseHtmlSubset(s, extraTags, extraAttrs) {
function merge() {
var clone = {};
for (var i = 0; i < arguments.length; ++i) {
if (typeof arguments[i] == 'object') {
for (var key in arguments[i]) {
if (arguments[i].hasOwnProperty(key))
clone[key] = arguments[i][key];
}
}
}
return clone;
}
function walk(n, f) { function walk(n, f) {
f(n); f(n);
for (var i = 0; i < n.childNodes.length; i++) { for (var i = 0; i < n.childNodes.length; i++) {
...@@ -41,17 +63,20 @@ function parseHtmlSubset(s) { ...@@ -41,17 +63,20 @@ function parseHtmlSubset(s) {
} }
function assertElement(node) { function assertElement(node) {
if (allowedTags.indexOf(node.tagName) == -1) if (tags.indexOf(node.tagName) == -1)
throw Error(node.tagName + ' is not supported'); throw Error(node.tagName + ' is not supported');
} }
function assertAttribute(attrNode, node) { function assertAttribute(attrNode, node) {
var n = attrNode.nodeName; var n = attrNode.nodeName;
var v = attrNode.nodeValue; var v = attrNode.nodeValue;
if (!allowedAttributes.hasOwnProperty(n) || !allowedAttributes[n](node, v)) if (!attrs.hasOwnProperty(n) || !attrs[n](node, v))
throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported'); throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported');
} }
var tags = allowedTags.concat(extraTags);
var attrs = merge(allowedAttributes, extraAttrs);
var r = document.createRange(); var r = document.createRange();
r.selectNode(document.body); r.selectNode(document.body);
// This does not execute any scripts. // This does not execute any scripts.
...@@ -61,7 +86,7 @@ function parseHtmlSubset(s) { ...@@ -61,7 +86,7 @@ function parseHtmlSubset(s) {
case Node.ELEMENT_NODE: case Node.ELEMENT_NODE:
assertElement(node); assertElement(node);
var attrs = node.attributes; var attrs = node.attributes;
for (var i = 0; i < attrs.length; i++) { for (var i = 0; i < attrs.length; ++i) {
assertAttribute(attrs[i], node); assertAttribute(attrs[i], node);
} }
break; break;
......
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