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 {
html.dragging-mode .app-install-hint {
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;
}
......@@ -27,28 +27,33 @@ body {
}
#notification-container {
-webkit-transition-duration: 100ms;
-webkit-transition-property: opacity;
-webkit-transition: opacity 200ms, margin-left 200ms;
bottom: 31px;
display: block;
margin-top: 2px;
float: left;
position: relative;
text-align: center;
text-align: start;
z-index: 15;
}
html[dir='rtl'] #notification-container {
float: right;
}
#notification-container.card-changed {
-webkit-transition: none;
opacity: 0;
}
#notification-container.inactive {
-webkit-transition-duration: 200ms;
-webkit-transition: opacity 200ms;
opacity: 0;
}
#notification {
background-color: rgb(255, 241, 153);
border: 1px solid lightGrey;
border-radius: 6px;
color: black;
display: inline-block;
font-weight: bold;
padding: 7px 15px;
}
#notification > div > div,
......@@ -56,6 +61,12 @@ body {
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 {
-webkit-margin-start: 0.5em;
vertical-align: middle;
......
......@@ -137,7 +137,7 @@ cr.define('ntp4', function() {
localStrings.getString('login_status_url');
$('login-status-advanced').onclick = function() {
chrome.send('showAdvancedLoginUI');
}
};
$('login-status-dismiss').onclick = loginBubble.hide.bind(loginBubble);
var bubbleContent = $('login-status-bubble-contents');
......@@ -153,7 +153,7 @@ cr.define('ntp4', function() {
infoBubble.handleCloseEvent = function() {
this.hide();
chrome.send('introMessageDismissed');
}
};
var bubbleContent = $('ntp4-intro-bubble-contents');
infoBubble.content = bubbleContent;
......@@ -166,9 +166,16 @@ cr.define('ntp4', function() {
chrome.send('introMessageSeen');
}
var serverpromo = localStrings.getString('serverpromo');
if (serverpromo) {
showNotification(parseHtmlSubset(serverpromo), [], function() {
var promo = localStrings.getString('serverpromo');
if (promo) {
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');
}, 60000);
chrome.send('notificationPromoViewed');
......@@ -194,6 +201,45 @@ cr.define('ntp4', function() {
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
* its length may be measured and the nav dots sized accordingly.
......@@ -249,7 +295,7 @@ cr.define('ntp4', function() {
* Timeout ID.
* @type {number}
*/
var notificationTimeout_ = 0;
var notificationTimeout = 0;
/**
* Shows the notification bubble.
......@@ -263,7 +309,7 @@ cr.define('ntp4', function() {
* manually dismisses the notification.
*/
function showNotification(message, links, opt_closeHandler, opt_timeout) {
window.clearTimeout(notificationTimeout_);
window.clearTimeout(notificationTimeout);
var span = document.querySelector('#notification > span');
if (typeof message == 'string') {
......@@ -282,23 +328,30 @@ cr.define('ntp4', function() {
link.onclick = function() {
this.action();
hideNotification();
}
};
link.setAttribute('role', 'button');
link.setAttribute('tabindex', 0);
link.className = 'link-button';
linksBin.appendChild(link);
}
document.querySelector('#notification button').onclick = function(e) {
function closeFunc(e) {
if (opt_closeHandler)
opt_closeHandler();
hideNotification();
};
}
document.querySelector('#notification button').onclick = closeFunc;
document.addEventListener('dragstart', closeFunc);
var timeout = opt_timeout || 10000;
notificationContainer.hidden = false;
notificationContainer.classList.remove('inactive');
notificationTimeout_ = window.setTimeout(hideNotification, timeout);
showNotificationOnCurrentPage();
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() {
*/
function hideNotification() {
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
* tabbed to or clicked.
* @param {Event} e The webkitTransitionEnd event.
*/
function onNotificationTransitionEnd(e) {
if (notificationContainer.classList.contains('inactive'));
if (notificationContainer.classList.contains('inactive'))
notificationContainer.hidden = true;
}
......@@ -323,6 +418,7 @@ cr.define('ntp4', function() {
function setMostVisitedPages(data, hasBlacklistedUrls) {
newTabView.mostVisitedPage.data = data;
cr.dispatchSimpleEvent(document, 'sectionready', true, true);
}
/**
......
......@@ -344,6 +344,8 @@ cr.define('ntp4', function() {
logEvent('apps.layout: ' + (Date.now() - startTime));
document.documentElement.classList.remove('starting-up');
cr.dispatchSimpleEvent(document, 'sectionready', true, true);
},
/**
......
......@@ -109,6 +109,10 @@
-webkit-transition: margin-bottom 200ms;
}
.animating-tile-page #notification-container {
-webkit-transition: margin 200ms, opacity 200ms;
}
@-webkit-keyframes bounce {
0% {
-webkit-transform: scale(0, 0);
......
......@@ -502,6 +502,25 @@ cr.define('ntp4', function() {
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.
*/
......@@ -910,6 +929,7 @@ cr.define('ntp4', function() {
this.classList.add('animating-tile-page');
this.heightChanged_();
this.positionNotification_();
this.repositionTiles_();
},
......@@ -983,6 +1003,17 @@ cr.define('ntp4', function() {
(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
* the page.
......
......@@ -220,6 +220,14 @@ cr.define('cr.ui', function() {
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.
* @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.
* @type {[string]}
......@@ -22,17 +28,33 @@ var allowedAttributes = {
return false;
node.setAttribute('target', '');
return true;
}
}
},
};
/**
* Parse a very small subset of HTML. This ensures that insecure HTML /
* javascript cannot be injected into the new tab page.
* @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.
* @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) {
f(n);
for (var i = 0; i < n.childNodes.length; i++) {
......@@ -41,17 +63,20 @@ function parseHtmlSubset(s) {
}
function assertElement(node) {
if (allowedTags.indexOf(node.tagName) == -1)
if (tags.indexOf(node.tagName) == -1)
throw Error(node.tagName + ' is not supported');
}
function assertAttribute(attrNode, node) {
var n = attrNode.nodeName;
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');
}
var tags = allowedTags.concat(extraTags);
var attrs = merge(allowedAttributes, extraAttrs);
var r = document.createRange();
r.selectNode(document.body);
// This does not execute any scripts.
......@@ -61,7 +86,7 @@ function parseHtmlSubset(s) {
case Node.ELEMENT_NODE:
assertElement(node);
var attrs = node.attributes;
for (var i = 0; i < attrs.length; i++) {
for (var i = 0; i < attrs.length; ++i) {
assertAttribute(attrs[i], node);
}
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