diff options
author | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-26 22:30:44 +0000 |
---|---|---|
committer | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-26 22:30:44 +0000 |
commit | 86f76ebbe5b1da4438e257663daa40091a5883ee (patch) | |
tree | 648b69f05e0a048a6de59e53a00df225d154ee50 /chrome | |
parent | 9a1e6d4cfc5028c9c7c431bbf7ed100b28271957 (diff) | |
download | chromium_src-86f76ebbe5b1da4438e257663daa40091a5883ee.zip chromium_src-86f76ebbe5b1da4438e257663daa40091a5883ee.tar.gz chromium_src-86f76ebbe5b1da4438e257663daa40091a5883ee.tar.bz2 |
Refactor parts of the NTP to split things into more managable chunks.
BUG=None
TEST=Manual
Review URL: http://codereview.chromium.org/1759007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@45631 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r-- | chrome/browser/browser_resources.grd | 2 | ||||
-rw-r--r-- | chrome/browser/resources/downloads.html | 2 | ||||
-rw-r--r-- | chrome/browser/resources/filebrowse.html | 2 | ||||
-rw-r--r-- | chrome/browser/resources/history.html | 2 | ||||
-rw-r--r-- | chrome/browser/resources/mediaplayer.html | 10 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.html | 6 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.js | 568 | ||||
-rw-r--r-- | chrome/browser/resources/ntp/most_visited.js | 492 | ||||
-rw-r--r-- | chrome/browser/resources/playlist.html | 4 | ||||
-rw-r--r-- | chrome/browser/resources/shared/js/i18n_template.js (renamed from chrome/browser/resources/i18n_template.js) | 4 | ||||
-rw-r--r-- | chrome/browser/resources/shared/js/local_strings.js (renamed from chrome/browser/resources/local_strings.js) | 2 | ||||
-rw-r--r-- | chrome/browser/resources/shared/js/parse_html_subset.js | 79 | ||||
-rw-r--r-- | chrome/browser/resources/shared/js/parse_html_subset_test.html | 85 | ||||
-rw-r--r-- | chrome/common/common_resources.grd | 2 |
14 files changed, 680 insertions, 580 deletions
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd index 9bf2c33..43e46c6 100644 --- a/chrome/browser/browser_resources.grd +++ b/chrome/browser/browser_resources.grd @@ -48,7 +48,7 @@ without changes to the corresponding grd file. ptrrraer --> <include name="IDR_CALENDAR_APP_MANIFEST" file="resources\calendar_app\manifest.json" type="BINDATA" /> <include name="IDR_DOCS_APP_MANIFEST" file="resources\docs_app\manifest.json" type="BINDATA" /> <include name="IDR_DOWNLOADS_HTML" file="resources\downloads.html" flattenhtml="true" type="BINDATA" /> - <include name="IDR_LOCAL_STRINGS_JS" file="resources\local_strings.js" type="BINDATA" /> + <include name="IDR_LOCAL_STRINGS_JS" file="resources\shared\js\local_strings.js" type="BINDATA" /> <include name="IDR_DOM_UI_CSS" file="resources\dom_ui.css" flattenhtml="true" type="BINDATA" /> <include name="IDR_EXTENSIONS_UI_HTML" file="resources\extensions_ui.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_EXTENSION_DEFAULT_ICON" file="resources\extension_default_icon.png" type="BINDATA" /> diff --git a/chrome/browser/resources/downloads.html b/chrome/browser/resources/downloads.html index 0b31aac..449fb19 100644 --- a/chrome/browser/resources/downloads.html +++ b/chrome/browser/resources/downloads.html @@ -191,7 +191,7 @@ html[dir=rtl] .name { } </style> -<script src="local_strings.js"></script> +<script src="shared/js/local_strings.js"></script> <script> /////////////////////////////////////////////////////////////////////////////// diff --git a/chrome/browser/resources/filebrowse.html b/chrome/browser/resources/filebrowse.html index 53adc98..61ec0ea 100644 --- a/chrome/browser/resources/filebrowse.html +++ b/chrome/browser/resources/filebrowse.html @@ -559,7 +559,7 @@ div.fullcontainer { } </style> -<script src="local_strings.js"></script> +<script src="shared/js/local_strings.js"></script> <script> diff --git a/chrome/browser/resources/history.html b/chrome/browser/resources/history.html index 69fbca5..ae78ff2 100644 --- a/chrome/browser/resources/history.html +++ b/chrome/browser/resources/history.html @@ -4,7 +4,7 @@ <meta charset="utf-8"> <title i18n-content="title"></title> <link rel="icon" href="../../app/theme/history_favicon.png"> -<script src="local_strings.js"></script> +<script src="shared/js/local_strings.js"></script> <script> /////////////////////////////////////////////////////////////////////////////// // Globals: diff --git a/chrome/browser/resources/mediaplayer.html b/chrome/browser/resources/mediaplayer.html index ba14bde..51483fa 100644 --- a/chrome/browser/resources/mediaplayer.html +++ b/chrome/browser/resources/mediaplayer.html @@ -259,7 +259,7 @@ body { } </style> -<script src='local_strings.js'></script> +<script src="shared/js/local_strings.js"></script> <script> function $(o) { @@ -326,7 +326,7 @@ function onMediaComplete() { mediaElement.removeEventListener("play", onMediaPlay, true); mediaElement.removeEventListener("pause", onMediaPause, true); currentItem ++; - + if (currentItem >= currentPlaylist.length) { currentItem = -1; return; @@ -346,7 +346,7 @@ function onMediaPause() { var pausebutton = $('pausebutton'); var playbutton = $('playbutton'); playbutton.style.display = 'block'; - pausebutton.style.display = 'none'; + pausebutton.style.display = 'none'; }; function setupMediaEvents(element) { @@ -444,7 +444,7 @@ function setupPlaybackControls() { playbutton.className = controlsclass + ' playbutton'; playbutton.onclick = playPauseButtonClick; element.appendChild(playbutton); - + var pausebutton = document.createElement('div'); pausebutton.id = 'pausebutton'; @@ -480,7 +480,7 @@ function setupPlaybackControls() { var sliderback = document.createElement('div'); sliderback.className = 'sliderback'; element.appendChild(sliderback); - + var loaded = document.createElement('div'); loaded.id = 'sliderloaded'; loaded.className = 'sliderloaded'; diff --git a/chrome/browser/resources/new_new_tab.html b/chrome/browser/resources/new_new_tab.html index 019701e..cfebba8 100644 --- a/chrome/browser/resources/new_new_tab.html +++ b/chrome/browser/resources/new_new_tab.html @@ -294,7 +294,9 @@ function updateSimpleSection(id, section) { <div class="window-menu" id="window-tooltip"></div> </body> -<script src="i18n_template.js"></script> -<script src="local_strings.js"></script> +<script src="shared/js/i18n_template.js"></script> +<script src="shared/js/local_strings.js"></script> +<script src="shared/js/parse_html_subset.js"></script> <script src="new_new_tab.js"></script> +<script src="ntp/most_visited.js"></script> </html> diff --git a/chrome/browser/resources/new_new_tab.js b/chrome/browser/resources/new_new_tab.js index 0add83b..64c7e1a 100644 --- a/chrome/browser/resources/new_new_tab.js +++ b/chrome/browser/resources/new_new_tab.js @@ -1,3 +1,6 @@ +// Copyright (c) 2010 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. // Helpers @@ -47,32 +50,6 @@ function bind(fn, selfObj, var_args) { const IS_MAC = /$Mac/.test(navigator.platform); var loading = true; -var mostVisitedData = []; -var gotMostVisited = false; - -function mostVisitedPages(data, firstRun) { - logEvent('received most visited pages'); - - // We append the class name with the "filler" so that we can style fillers - // differently. - var maxItems = 8; - data.length = Math.min(maxItems, data.length); - var len = data.length; - for (var i = len; i < maxItems; i++) { - data[i] = {filler: true}; - } - - mostVisitedData = data; - renderMostVisited(data); - - gotMostVisited = true; - onDataLoaded(); - - // Only show the first run notification if first run. - if (firstRun) { - showFirstRunNotification(); - } -} function getAppsCallback(data) { var appsSection = $('apps-section'); @@ -239,12 +216,6 @@ function saveShownSections() { chrome.send('setShownSections', [String(shownSections)]); } -function getThumbnailClassName(data) { - return 'thumbnail-container' + - (data.pinned ? ' pinned' : '') + - (data.filler ? ' filler' : ''); -} - function url(s) { // http://www.w3.org/TR/css3-values/#uris // Parentheses, commas, whitespace characters, single quotes (') and double @@ -259,50 +230,6 @@ function url(s) { return 'url("' + s2 + '")'; } -function renderMostVisited(data) { - var parent = $('most-visited'); - var children = parent.children; - for (var i = 0; i < data.length; i++) { - var d = data[i]; - var t = children[i]; - - // If we have a filler continue - var oldClassName = t.className; - var newClassName = getThumbnailClassName(d); - if (oldClassName != newClassName) { - t.className = newClassName; - } - - // No need to continue if this is a filler. - if (newClassName == 'thumbnail-container filler') { - // Make sure the user cannot tab to the filler. - t.tabIndex = -1; - continue; - } - // Allow focus. - t.tabIndex = 1; - - t.href = d.url; - t.querySelector('.pin').title = localStrings.getString(d.pinned ? - 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); - t.querySelector('.remove').title = - localStrings.getString('removethumbnailtooltip'); - - // There was some concern that a malformed malicious URL could cause an XSS - // attack but setting style.backgroundImage = 'url(javascript:...)' does - // not execute the JavaScript in WebKit. - - var thumbnailUrl = d.thumbnailUrl || 'chrome://thumb/' + d.url; - t.querySelector('.thumbnail-wrapper').style.backgroundImage = - url(thumbnailUrl); - var titleDiv = t.querySelector('.title > div'); - titleDiv.xtitle = titleDiv.textContent = d.title; - var faviconUrl = d.faviconUrl || 'chrome://favicon/' + d.url; - titleDiv.style.backgroundImage = url(faviconUrl); - titleDiv.dir = d.direction; - } -} - /** * Calls chrome.send with a callback and restores the original afterwards. */ @@ -387,194 +314,6 @@ function hideSection(section) { } } -var mostVisited = { - addPinnedUrl_: function(data, index) { - chrome.send('addPinnedURL', [data.url, data.title, data.faviconUrl || '', - data.thumbnailUrl || '', String(index)]); - }, - getItem: function(el) { - return findAncestorByClass(el, 'thumbnail-container'); - }, - - getHref: function(el) { - return el.href; - }, - - togglePinned: function(el) { - var index = this.getThumbnailIndex(el); - var data = mostVisitedData[index]; - data.pinned = !data.pinned; - if (data.pinned) { - this.addPinnedUrl_(data, index); - } else { - chrome.send('removePinnedURL', [data.url]); - } - this.updatePinnedDom_(el, data.pinned); - }, - - updatePinnedDom_: function(el, pinned) { - el.querySelector('.pin').title = localStrings.getString(pinned ? - 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); - if (pinned) { - addClass(el, 'pinned'); - } else { - removeClass(el, 'pinned'); - } - }, - - getThumbnailIndex: function(el) { - var nodes = el.parentNode.querySelectorAll('.thumbnail-container'); - return Array.prototype.indexOf.call(nodes, el); - }, - - swapPosition: function(source, destination) { - var nodes = source.parentNode.querySelectorAll('.thumbnail-container'); - var sourceIndex = this.getThumbnailIndex(source); - var destinationIndex = this.getThumbnailIndex(destination); - swapDomNodes(source, destination); - - var sourceData = mostVisitedData[sourceIndex]; - this.addPinnedUrl_(sourceData, destinationIndex); - sourceData.pinned = true; - this.updatePinnedDom_(source, true); - - var destinationData = mostVisitedData[destinationIndex]; - // Only update the destination if it was pinned before. - if (destinationData.pinned) { - this.addPinnedUrl_(destinationData, sourceIndex); - } - mostVisitedData[destinationIndex] = sourceData; - mostVisitedData[sourceIndex] = destinationData; - }, - - blacklist: function(el) { - var self = this; - var url = this.getHref(el); - chrome.send('blacklistURLFromMostVisited', [url]); - - addClass(el, 'hide'); - - // Find the old item. - var oldUrls = {}; - var oldIndex = -1; - var oldItem; - for (var i = 0; i < mostVisitedData.length; i++) { - if (mostVisitedData[i].url == url) { - oldItem = mostVisitedData[i]; - oldIndex = i; - } - oldUrls[mostVisitedData[i].url] = true; - } - - // Send 'getMostVisitedPages' with a callback since we want to find the new - // page and add that in the place of the removed page. - chromeSend('getMostVisited', [], 'mostVisitedPages', function(data) { - // Find new item. - var newItem; - for (var i = 0; i < data.length; i++) { - if (!(data[i].url in oldUrls)) { - newItem = data[i]; - break; - } - } - - if (!newItem) { - // If no other page is available to replace the blacklisted item, - // we need to reorder items s.t. all filler items are in the rightmost - // indices. - mostVisitedPages(data); - - // Replace old item with new item in the mostVisitedData array. - } else if (oldIndex != -1) { - mostVisitedData.splice(oldIndex, 1, newItem); - mostVisitedPages(mostVisitedData); - addClass(el, 'fade-in'); - } - - // We wrap the title in a <span class=blacklisted-title>. We pass an empty - // string to the notifier function and use DOM to insert the real string. - var actionText = localStrings.getString('undothumbnailremove'); - - // Show notification and add undo callback function. - var wasPinned = oldItem.pinned; - showNotification('', actionText, function() { - self.removeFromBlackList(url); - if (wasPinned) { - self.addPinnedUrl_(oldItem, oldIndex); - } - chrome.send('getMostVisited'); - }); - - // Now change the DOM. - var removeText = localStrings.getString('thumbnailremovednotification'); - var notifySpan = document.querySelector('#notification > span'); - notifySpan.textContent = removeText; - - // Focus the undo link. - var undoLink = document.querySelector( - '#notification > .link > [tabindex]'); - undoLink.focus(); - }); - }, - - removeFromBlackList: function(url) { - chrome.send('removeURLsFromMostVisitedBlacklist', [url]); - }, - - clearAllBlacklisted: function() { - chrome.send('clearMostVisitedURLsBlacklist', []); - hideNotification(); - }, - - updateDisplayMode: function() { - if (!this.dirty_) { - return; - } - updateSimpleSection('most-visited-section', Section.THUMB); - }, - - dirty_: false, - - invalidate: function() { - this.dirty_ = true; - }, - - layout: function() { - if (!this.dirty_) { - return; - } - var d0 = Date.now(); - - var mostVisitedElement = $('most-visited'); - var thumbnails = mostVisitedElement.children; - var hidden = !(shownSections & Section.THUMB); - - - // We set overflow to hidden so that the most visited element does not - // "leak" when we hide and show it. - if (hidden) { - mostVisitedElement.style.overflow = 'hidden'; - } - - applyMostVisitedRects(); - - // Only set overflow to visible if the element is shown. - if (!hidden) { - afterTransition(function() { - mostVisitedElement.style.overflow = ''; - }); - } - - this.dirty_ = false; - - logEvent('mostVisited.layout: ' + (Date.now() - d0)); - }, - - getRectByIndex: function(index) { - return getMostVisitedLayoutRects()[index]; - } -}; - // Recently closed function layoutRecentlyClosed() { @@ -854,7 +593,6 @@ function showFirstRunNotification() { addClass(notification, 'first-run'); } - /** * This handles the option menu. * @param {Element} button The button element. @@ -1053,25 +791,6 @@ optionMenu.commands = { } }; -$('most-visited').addEventListener('click', function(e) { - var target = e.target; - if (hasClass(target, 'pin')) { - mostVisited.togglePinned(mostVisited.getItem(target)); - e.preventDefault(); - } else if (hasClass(target, 'remove')) { - mostVisited.blacklist(mostVisited.getItem(target)); - e.preventDefault(); - } -}); - -// Allow blacklisting most visited site using the keyboard. -$('most-visited').addEventListener('keydown', function(e) { - if (!IS_MAC && e.keyCode == 46 || // Del - IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace - mostVisited.blacklist(e.target); - } -}); - $('main').addEventListener('click', function(e) { if (e.target.tagName == 'H2') { var p = e.target.parentNode; @@ -1265,14 +984,6 @@ document.addEventListener('DOMContentLoaded', // Set up links and text-decoration for promotional message. document.addEventListener('DOMContentLoaded', setUpPromoMessage); -// Work around for http://crbug.com/25329 -function ensureSmallGridCorrect() { - if (wasSmallGrid != useSmallGrid()) { - applyMostVisitedRects(); - } -} -document.addEventListener('DOMContentLoaded', ensureSmallGridCorrect); - /** * The sync code is not yet built by default on all platforms so we have to * make sure we don't send the initial sync message to the backend unless the @@ -1326,279 +1037,6 @@ document.addEventListener('mouseover', function(e) { } }); -// DnD - -var dnd = { - currentOverItem_: null, - get currentOverItem() { - return this.currentOverItem_; - }, - set currentOverItem(item) { - var style; - if (item != this.currentOverItem_) { - if (this.currentOverItem_) { - style = this.currentOverItem_.firstElementChild.style; - style.left = style.top = ''; - } - this.currentOverItem_ = item; - - if (item) { - // Make the drag over item move 15px towards the source. The movement is - // done by only moving the edit-mode-border (as in the mocks) and it is - // done with relative positioning so that the movement does not change - // the drop target. - var dragIndex = mostVisited.getThumbnailIndex(this.dragItem); - var overIndex = mostVisited.getThumbnailIndex(item); - if (dragIndex == -1 || overIndex == -1) { - return; - } - - var dragRect = mostVisited.getRectByIndex(dragIndex); - var overRect = mostVisited.getRectByIndex(overIndex); - - var x = dragRect.left - overRect.left; - var y = dragRect.top - overRect.top; - var z = Math.sqrt(x * x + y * y); - var z2 = 15; - var x2 = x * z2 / z; - var y2 = y * z2 / z; - - style = this.currentOverItem_.firstElementChild.style; - style.left = x2 + 'px'; - style.top = y2 + 'px'; - } - } - }, - dragItem: null, - startX: 0, - startY: 0, - startScreenX: 0, - startScreenY: 0, - dragEndTimer: null, - - handleDragStart: function(e) { - var thumbnail = mostVisited.getItem(e.target); - if (thumbnail) { - // Don't set data since HTML5 does not allow setting the name for - // url-list. Instead, we just rely on the dragging of link behavior. - this.dragItem = thumbnail; - addClass(this.dragItem, 'dragging'); - this.dragItem.style.zIndex = 2; - e.dataTransfer.effectAllowed = 'copyLinkMove'; - } - }, - - handleDragEnter: function(e) { - if (this.canDropOnElement(this.currentOverItem)) { - e.preventDefault(); - } - }, - - handleDragOver: function(e) { - var item = mostVisited.getItem(e.target); - this.currentOverItem = item; - if (this.canDropOnElement(item)) { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - } - }, - - handleDragLeave: function(e) { - var item = mostVisited.getItem(e.target); - if (item) { - e.preventDefault(); - } - - this.currentOverItem = null; - }, - - handleDrop: function(e) { - var dropTarget = mostVisited.getItem(e.target); - if (this.canDropOnElement(dropTarget)) { - dropTarget.style.zIndex = 1; - mostVisited.swapPosition(this.dragItem, dropTarget); - // The timeout below is to allow WebKit to see that we turned off - // pointer-event before moving the thumbnails so that we can get out of - // hover mode. - window.setTimeout(function() { - mostVisited.invalidate(); - mostVisited.layout(); - }, 10); - e.preventDefault(); - if (this.dragEndTimer) { - window.clearTimeout(this.dragEndTimer); - this.dragEndTimer = null; - } - afterTransition(function() { - dropTarget.style.zIndex = ''; - }); - } - }, - - handleDragEnd: function(e) { - var dragItem = this.dragItem; - if (dragItem) { - dragItem.style.pointerEvents = ''; - removeClass(dragItem, 'dragging'); - - afterTransition(function() { - // Delay resetting zIndex to let the animation finish. - dragItem.style.zIndex = ''; - // Same for overflow. - dragItem.parentNode.style.overflow = ''; - }); - - mostVisited.invalidate(); - mostVisited.layout(); - this.dragItem = null; - } - }, - - handleDrag: function(e) { - // Moves the drag item making sure that it is not displayed outside the - // browser viewport. - var item = mostVisited.getItem(e.target); - var rect = document.querySelector('#most-visited').getBoundingClientRect(); - item.style.pointerEvents = 'none'; - - var x = this.startX + e.screenX - this.startScreenX; - var y = this.startY + e.screenY - this.startScreenY; - - // The position of the item is relative to #most-visited so we need to - // subtract that when calculating the allowed position. - x = Math.max(x, -rect.left); - x = Math.min(x, document.body.clientWidth - rect.left - item.offsetWidth - - 2); - // The shadow is 2px - y = Math.max(-rect.top, y); - y = Math.min(y, document.body.clientHeight - rect.top - item.offsetHeight - - 2); - - // Override right in case of RTL. - item.style.right = 'auto'; - item.style.left = x + 'px'; - item.style.top = y + 'px'; - item.style.zIndex = 2; - }, - - // We listen to mousedown to get the relative position of the cursor for dnd. - handleMouseDown: function(e) { - var item = mostVisited.getItem(e.target); - if (item) { - this.startX = item.offsetLeft; - this.startY = item.offsetTop; - this.startScreenX = e.screenX; - this.startScreenY = e.screenY; - - // We don't want to focus the item on mousedown. However, to prevent focus - // one has to call preventDefault but this also prevents the drag and drop - // (sigh) so we only prevent it when the user is not doing a left mouse - // button drag. - if (e.button != 0) // LEFT - e.preventDefault(); - } - }, - - canDropOnElement: function(el) { - return this.dragItem && el && hasClass(el, 'thumbnail-container') && - !hasClass(el, 'filler'); - }, - - init: function() { - var el = $('most-visited'); - el.addEventListener('dragstart', bind(this.handleDragStart, this)); - el.addEventListener('dragenter', bind(this.handleDragEnter, this)); - el.addEventListener('dragover', bind(this.handleDragOver, this)); - el.addEventListener('dragleave', bind(this.handleDragLeave, this)); - el.addEventListener('drop', bind(this.handleDrop, this)); - el.addEventListener('dragend', bind(this.handleDragEnd, this)); - el.addEventListener('drag', bind(this.handleDrag, this)); - el.addEventListener('mousedown', bind(this.handleMouseDown, this)); - } -}; - -dnd.init(); - -/** - * Whitelist of tag names allowed in parseHtmlSubset. - * @type {[string]} - */ -var allowedTags = ['A', 'B', 'STRONG']; - -/** - * Parse a very small subset of HTML. - * @param {string} s The string to parse. - * @throws {Error} In case of non supported markup. - * @return {DocumentFragment} A document fragment containing the DOM tree. - */ -var allowedAttributes = { - 'href': function(node, value) { - // Only allow a[href] starting with http:// and https:// - return node.tagName == 'A' && (value.indexOf('http://') == 0 || - value.indexOf('https://') == 0); - }, - 'target': function(node, value) { - // Allow a[target] but reset the value to "". - if (node.tagName != 'A') - 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. - * @throws {Error} In case of non supported markup. - * @return {DocumentFragment} A document fragment containing the DOM tree. - */ -function parseHtmlSubset(s) { - function walk(n, f) { - f(n); - for (var i = 0; i < n.childNodes.length; i++) { - walk(n.childNodes[i], f); - } - } - - function assertElement(node) { - if (allowedTags.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)) - throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported'); - } - - var r = document.createRange(); - r.selectNode(document.body); - // This does not execute any scripts. - var df = r.createContextualFragment(s); - walk(df, function(node) { - switch (node.nodeType) { - case Node.ELEMENT_NODE: - assertElement(node); - var attrs = node.attributes; - for (var i = 0; i < attrs.length; i++) { - assertAttribute(attrs[i], node); - } - break; - - case Node.COMMENT_NODE: - case Node.DOCUMENT_FRAGMENT_NODE: - case Node.TEXT_NODE: - break; - - default: - throw Error('Node type ' + node.nodeType + ' is not supported'); - } - }); - return df; -} - /** * Makes links and buttons support a different underline color. * @param {Node} node The node to search for links and buttons in. diff --git a/chrome/browser/resources/ntp/most_visited.js b/chrome/browser/resources/ntp/most_visited.js new file mode 100644 index 0000000..9a19c0d --- /dev/null +++ b/chrome/browser/resources/ntp/most_visited.js @@ -0,0 +1,492 @@ +// Copyright (c) 2010 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. + +var mostVisitedData = []; +var gotMostVisited = false; + +function mostVisitedPages(data, firstRun) { + logEvent('received most visited pages'); + + // We append the class name with the "filler" so that we can style fillers + // differently. + var maxItems = 8; + data.length = Math.min(maxItems, data.length); + var len = data.length; + for (var i = len; i < maxItems; i++) { + data[i] = {filler: true}; + } + + mostVisitedData = data; + renderMostVisited(data); + + gotMostVisited = true; + onDataLoaded(); + + // Only show the first run notification if first run. + if (firstRun) { + showFirstRunNotification(); + } +} +function getThumbnailClassName(data) { + return 'thumbnail-container' + + (data.pinned ? ' pinned' : '') + + (data.filler ? ' filler' : ''); +} + +function renderMostVisited(data) { + var parent = $('most-visited'); + var children = parent.children; + for (var i = 0; i < data.length; i++) { + var d = data[i]; + var t = children[i]; + + // If we have a filler continue + var oldClassName = t.className; + var newClassName = getThumbnailClassName(d); + if (oldClassName != newClassName) { + t.className = newClassName; + } + + // No need to continue if this is a filler. + if (newClassName == 'thumbnail-container filler') { + // Make sure the user cannot tab to the filler. + t.tabIndex = -1; + continue; + } + // Allow focus. + t.tabIndex = 1; + + t.href = d.url; + t.querySelector('.pin').title = localStrings.getString(d.pinned ? + 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); + t.querySelector('.remove').title = + localStrings.getString('removethumbnailtooltip'); + + // There was some concern that a malformed malicious URL could cause an XSS + // attack but setting style.backgroundImage = 'url(javascript:...)' does + // not execute the JavaScript in WebKit. + + var thumbnailUrl = d.thumbnailUrl || 'chrome://thumb/' + d.url; + t.querySelector('.thumbnail-wrapper').style.backgroundImage = + url(thumbnailUrl); + var titleDiv = t.querySelector('.title > div'); + titleDiv.xtitle = titleDiv.textContent = d.title; + var faviconUrl = d.faviconUrl || 'chrome://favicon/' + d.url; + titleDiv.style.backgroundImage = url(faviconUrl); + titleDiv.dir = d.direction; + } +} + +var mostVisited = { + addPinnedUrl_: function(data, index) { + chrome.send('addPinnedURL', [data.url, data.title, data.faviconUrl || '', + data.thumbnailUrl || '', String(index)]); + }, + getItem: function(el) { + return findAncestorByClass(el, 'thumbnail-container'); + }, + + getHref: function(el) { + return el.href; + }, + + togglePinned: function(el) { + var index = this.getThumbnailIndex(el); + var data = mostVisitedData[index]; + data.pinned = !data.pinned; + if (data.pinned) { + this.addPinnedUrl_(data, index); + } else { + chrome.send('removePinnedURL', [data.url]); + } + this.updatePinnedDom_(el, data.pinned); + }, + + updatePinnedDom_: function(el, pinned) { + el.querySelector('.pin').title = localStrings.getString(pinned ? + 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); + if (pinned) { + addClass(el, 'pinned'); + } else { + removeClass(el, 'pinned'); + } + }, + + getThumbnailIndex: function(el) { + var nodes = el.parentNode.querySelectorAll('.thumbnail-container'); + return Array.prototype.indexOf.call(nodes, el); + }, + + swapPosition: function(source, destination) { + var nodes = source.parentNode.querySelectorAll('.thumbnail-container'); + var sourceIndex = this.getThumbnailIndex(source); + var destinationIndex = this.getThumbnailIndex(destination); + swapDomNodes(source, destination); + + var sourceData = mostVisitedData[sourceIndex]; + this.addPinnedUrl_(sourceData, destinationIndex); + sourceData.pinned = true; + this.updatePinnedDom_(source, true); + + var destinationData = mostVisitedData[destinationIndex]; + // Only update the destination if it was pinned before. + if (destinationData.pinned) { + this.addPinnedUrl_(destinationData, sourceIndex); + } + mostVisitedData[destinationIndex] = sourceData; + mostVisitedData[sourceIndex] = destinationData; + }, + + blacklist: function(el) { + var self = this; + var url = this.getHref(el); + chrome.send('blacklistURLFromMostVisited', [url]); + + addClass(el, 'hide'); + + // Find the old item. + var oldUrls = {}; + var oldIndex = -1; + var oldItem; + for (var i = 0; i < mostVisitedData.length; i++) { + if (mostVisitedData[i].url == url) { + oldItem = mostVisitedData[i]; + oldIndex = i; + } + oldUrls[mostVisitedData[i].url] = true; + } + + // Send 'getMostVisitedPages' with a callback since we want to find the new + // page and add that in the place of the removed page. + chromeSend('getMostVisited', [], 'mostVisitedPages', function(data) { + // Find new item. + var newItem; + for (var i = 0; i < data.length; i++) { + if (!(data[i].url in oldUrls)) { + newItem = data[i]; + break; + } + } + + if (!newItem) { + // If no other page is available to replace the blacklisted item, + // we need to reorder items s.t. all filler items are in the rightmost + // indices. + mostVisitedPages(data); + + // Replace old item with new item in the mostVisitedData array. + } else if (oldIndex != -1) { + mostVisitedData.splice(oldIndex, 1, newItem); + mostVisitedPages(mostVisitedData); + addClass(el, 'fade-in'); + } + + // We wrap the title in a <span class=blacklisted-title>. We pass an empty + // string to the notifier function and use DOM to insert the real string. + var actionText = localStrings.getString('undothumbnailremove'); + + // Show notification and add undo callback function. + var wasPinned = oldItem.pinned; + showNotification('', actionText, function() { + self.removeFromBlackList(url); + if (wasPinned) { + self.addPinnedUrl_(oldItem, oldIndex); + } + chrome.send('getMostVisited'); + }); + + // Now change the DOM. + var removeText = localStrings.getString('thumbnailremovednotification'); + var notifySpan = document.querySelector('#notification > span'); + notifySpan.textContent = removeText; + + // Focus the undo link. + var undoLink = document.querySelector( + '#notification > .link > [tabindex]'); + undoLink.focus(); + }); + }, + + removeFromBlackList: function(url) { + chrome.send('removeURLsFromMostVisitedBlacklist', [url]); + }, + + clearAllBlacklisted: function() { + chrome.send('clearMostVisitedURLsBlacklist', []); + hideNotification(); + }, + + updateDisplayMode: function() { + if (!this.dirty_) { + return; + } + updateSimpleSection('most-visited-section', Section.THUMB); + }, + + dirty_: false, + + invalidate: function() { + this.dirty_ = true; + }, + + layout: function() { + if (!this.dirty_) { + return; + } + var d0 = Date.now(); + + var mostVisitedElement = $('most-visited'); + var thumbnails = mostVisitedElement.children; + var hidden = !(shownSections & Section.THUMB); + + + // We set overflow to hidden so that the most visited element does not + // "leak" when we hide and show it. + if (hidden) { + mostVisitedElement.style.overflow = 'hidden'; + } + + applyMostVisitedRects(); + + // Only set overflow to visible if the element is shown. + if (!hidden) { + afterTransition(function() { + mostVisitedElement.style.overflow = ''; + }); + } + + this.dirty_ = false; + + logEvent('mostVisited.layout: ' + (Date.now() - d0)); + }, + + getRectByIndex: function(index) { + return getMostVisitedLayoutRects()[index]; + } +}; + +$('most-visited').addEventListener('click', function(e) { + var target = e.target; + if (hasClass(target, 'pin')) { + mostVisited.togglePinned(mostVisited.getItem(target)); + e.preventDefault(); + } else if (hasClass(target, 'remove')) { + mostVisited.blacklist(mostVisited.getItem(target)); + e.preventDefault(); + } +}); + +// Allow blacklisting most visited site using the keyboard. +$('most-visited').addEventListener('keydown', function(e) { + if (!IS_MAC && e.keyCode == 46 || // Del + IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace + mostVisited.blacklist(e.target); + } +}); + +window.addEventListener('load', onDataLoaded); + +window.addEventListener('resize', handleWindowResize); + +// Work around for http://crbug.com/25329 +function ensureSmallGridCorrect() { + if (wasSmallGrid != useSmallGrid()) { + applyMostVisitedRects(); + } +} +document.addEventListener('DOMContentLoaded', ensureSmallGridCorrect); + +// DnD + +var dnd = { + currentOverItem_: null, + get currentOverItem() { + return this.currentOverItem_; + }, + set currentOverItem(item) { + var style; + if (item != this.currentOverItem_) { + if (this.currentOverItem_) { + style = this.currentOverItem_.firstElementChild.style; + style.left = style.top = ''; + } + this.currentOverItem_ = item; + + if (item) { + // Make the drag over item move 15px towards the source. The movement is + // done by only moving the edit-mode-border (as in the mocks) and it is + // done with relative positioning so that the movement does not change + // the drop target. + var dragIndex = mostVisited.getThumbnailIndex(this.dragItem); + var overIndex = mostVisited.getThumbnailIndex(item); + if (dragIndex == -1 || overIndex == -1) { + return; + } + + var dragRect = mostVisited.getRectByIndex(dragIndex); + var overRect = mostVisited.getRectByIndex(overIndex); + + var x = dragRect.left - overRect.left; + var y = dragRect.top - overRect.top; + var z = Math.sqrt(x * x + y * y); + var z2 = 15; + var x2 = x * z2 / z; + var y2 = y * z2 / z; + + style = this.currentOverItem_.firstElementChild.style; + style.left = x2 + 'px'; + style.top = y2 + 'px'; + } + } + }, + dragItem: null, + startX: 0, + startY: 0, + startScreenX: 0, + startScreenY: 0, + dragEndTimer: null, + + handleDragStart: function(e) { + var thumbnail = mostVisited.getItem(e.target); + if (thumbnail) { + // Don't set data since HTML5 does not allow setting the name for + // url-list. Instead, we just rely on the dragging of link behavior. + this.dragItem = thumbnail; + addClass(this.dragItem, 'dragging'); + this.dragItem.style.zIndex = 2; + e.dataTransfer.effectAllowed = 'copyLinkMove'; + } + }, + + handleDragEnter: function(e) { + if (this.canDropOnElement(this.currentOverItem)) { + e.preventDefault(); + } + }, + + handleDragOver: function(e) { + var item = mostVisited.getItem(e.target); + this.currentOverItem = item; + if (this.canDropOnElement(item)) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + } + }, + + handleDragLeave: function(e) { + var item = mostVisited.getItem(e.target); + if (item) { + e.preventDefault(); + } + + this.currentOverItem = null; + }, + + handleDrop: function(e) { + var dropTarget = mostVisited.getItem(e.target); + if (this.canDropOnElement(dropTarget)) { + dropTarget.style.zIndex = 1; + mostVisited.swapPosition(this.dragItem, dropTarget); + // The timeout below is to allow WebKit to see that we turned off + // pointer-event before moving the thumbnails so that we can get out of + // hover mode. + window.setTimeout(function() { + mostVisited.invalidate(); + mostVisited.layout(); + }, 10); + e.preventDefault(); + if (this.dragEndTimer) { + window.clearTimeout(this.dragEndTimer); + this.dragEndTimer = null; + } + afterTransition(function() { + dropTarget.style.zIndex = ''; + }); + } + }, + + handleDragEnd: function(e) { + var dragItem = this.dragItem; + if (dragItem) { + dragItem.style.pointerEvents = ''; + removeClass(dragItem, 'dragging'); + + afterTransition(function() { + // Delay resetting zIndex to let the animation finish. + dragItem.style.zIndex = ''; + // Same for overflow. + dragItem.parentNode.style.overflow = ''; + }); + + mostVisited.invalidate(); + mostVisited.layout(); + this.dragItem = null; + } + }, + + handleDrag: function(e) { + // Moves the drag item making sure that it is not displayed outside the + // browser viewport. + var item = mostVisited.getItem(e.target); + var rect = document.querySelector('#most-visited').getBoundingClientRect(); + item.style.pointerEvents = 'none'; + + var x = this.startX + e.screenX - this.startScreenX; + var y = this.startY + e.screenY - this.startScreenY; + + // The position of the item is relative to #most-visited so we need to + // subtract that when calculating the allowed position. + x = Math.max(x, -rect.left); + x = Math.min(x, document.body.clientWidth - rect.left - item.offsetWidth - + 2); + // The shadow is 2px + y = Math.max(-rect.top, y); + y = Math.min(y, document.body.clientHeight - rect.top - item.offsetHeight - + 2); + + // Override right in case of RTL. + item.style.right = 'auto'; + item.style.left = x + 'px'; + item.style.top = y + 'px'; + item.style.zIndex = 2; + }, + + // We listen to mousedown to get the relative position of the cursor for dnd. + handleMouseDown: function(e) { + var item = mostVisited.getItem(e.target); + if (item) { + this.startX = item.offsetLeft; + this.startY = item.offsetTop; + this.startScreenX = e.screenX; + this.startScreenY = e.screenY; + + // We don't want to focus the item on mousedown. However, to prevent focus + // one has to call preventDefault but this also prevents the drag and drop + // (sigh) so we only prevent it when the user is not doing a left mouse + // button drag. + if (e.button != 0) // LEFT + e.preventDefault(); + } + }, + + canDropOnElement: function(el) { + return this.dragItem && el && hasClass(el, 'thumbnail-container') && + !hasClass(el, 'filler'); + }, + + init: function() { + var el = $('most-visited'); + el.addEventListener('dragstart', bind(this.handleDragStart, this)); + el.addEventListener('dragenter', bind(this.handleDragEnter, this)); + el.addEventListener('dragover', bind(this.handleDragOver, this)); + el.addEventListener('dragleave', bind(this.handleDragLeave, this)); + el.addEventListener('drop', bind(this.handleDrop, this)); + el.addEventListener('dragend', bind(this.handleDragEnd, this)); + el.addEventListener('drag', bind(this.handleDrag, this)); + el.addEventListener('mousedown', bind(this.handleMouseDown, this)); + } +}; + +dnd.init(); + diff --git a/chrome/browser/resources/playlist.html b/chrome/browser/resources/playlist.html index 80415f6..3bf0d19 100644 --- a/chrome/browser/resources/playlist.html +++ b/chrome/browser/resources/playlist.html @@ -35,11 +35,11 @@ } .title { - + } </style> -<script src='local_strings.js'></script> +<script src="shared/js/local_strings.js"></script> <script> function $(o) { diff --git a/chrome/browser/resources/i18n_template.js b/chrome/browser/resources/shared/js/i18n_template.js index 8166ddc..2645bdb 100644 --- a/chrome/browser/resources/i18n_template.js +++ b/chrome/browser/resources/shared/js/i18n_template.js @@ -1,3 +1,7 @@ +// Copyright (c) 2010 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. + /** * @fileoverview This is a simple template engine inspired by JsTemplates * optimized for i18n. diff --git a/chrome/browser/resources/local_strings.js b/chrome/browser/resources/shared/js/local_strings.js index ecf3f5a..75b9859 100644 --- a/chrome/browser/resources/local_strings.js +++ b/chrome/browser/resources/shared/js/local_strings.js @@ -1,4 +1,4 @@ -// Copyright (c) 2009-2010 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 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. diff --git a/chrome/browser/resources/shared/js/parse_html_subset.js b/chrome/browser/resources/shared/js/parse_html_subset.js new file mode 100644 index 0000000..eecf6ee --- /dev/null +++ b/chrome/browser/resources/shared/js/parse_html_subset.js @@ -0,0 +1,79 @@ +/** + * Whitelist of tag names allowed in parseHtmlSubset. + * @type {[string]} + */ +var allowedTags = ['A', 'B', 'STRONG']; + +/** + * Parse a very small subset of HTML. + * @param {string} s The string to parse. + * @throws {Error} In case of non supported markup. + * @return {DocumentFragment} A document fragment containing the DOM tree. + */ +var allowedAttributes = { + 'href': function(node, value) { + // Only allow a[href] starting with http:// and https:// + return node.tagName == 'A' && (value.indexOf('http://') == 0 || + value.indexOf('https://') == 0); + }, + 'target': function(node, value) { + // Allow a[target] but reset the value to "". + if (node.tagName != 'A') + 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. + * @throws {Error} In case of non supported markup. + * @return {DocumentFragment} A document fragment containing the DOM tree. + */ +function parseHtmlSubset(s) { + function walk(n, f) { + f(n); + for (var i = 0; i < n.childNodes.length; i++) { + walk(n.childNodes[i], f); + } + } + + function assertElement(node) { + if (allowedTags.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)) + throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported'); + } + + var r = document.createRange(); + r.selectNode(document.body); + // This does not execute any scripts. + var df = r.createContextualFragment(s); + walk(df, function(node) { + switch (node.nodeType) { + case Node.ELEMENT_NODE: + assertElement(node); + var attrs = node.attributes; + for (var i = 0; i < attrs.length; i++) { + assertAttribute(attrs[i], node); + } + break; + + case Node.COMMENT_NODE: + case Node.DOCUMENT_FRAGMENT_NODE: + case Node.TEXT_NODE: + break; + + default: + throw Error('Node type ' + node.nodeType + ' is not supported'); + } + }); + return df; +} diff --git a/chrome/browser/resources/shared/js/parse_html_subset_test.html b/chrome/browser/resources/shared/js/parse_html_subset_test.html new file mode 100644 index 0000000..54bb3da --- /dev/null +++ b/chrome/browser/resources/shared/js/parse_html_subset_test.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<html> +<head> +<title></title> +<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script> +<script src="parse_html_subset.js"></script> +<script> + +goog.require('goog.testing.jsunit'); + +</script> + +</head> +<body> +<script> + +function parseAndAssertThrows(s) { + assertThrows(function() { + parseHtmlSubset(s); + }); +} + +function parseAndAssertNotThrows(s) { + assertNotThrows(function() { + parseHtmlSubset(s); + }); +} + +function testText() { + parseAndAssertNotThrows(''); + parseAndAssertNotThrows('abc'); + parseAndAssertNotThrows(' '); +} + +function testSupportedTags() { + parseAndAssertNotThrows('<b>bold</b>'); + parseAndAssertNotThrows('Some <b>bold</b> text'); + parseAndAssertNotThrows('Some <strong>strong</strong> text'); + parseAndAssertNotThrows('<B>bold</B>'); + parseAndAssertNotThrows('Some <B>bold</B> text'); + parseAndAssertNotThrows('Some <STRONG>strong</STRONG> text'); +} + +function testInvaliTags() { + parseAndAssertThrows('<unknown_tag>x</unknown_tag>'); + parseAndAssertThrows('<img>'); + parseAndAssertThrows('<script>alert(1)<' + '/script>'); +} + +function testInvalidAttributes() { + parseAndAssertThrows('<b onclick="alert(1)">x</b>'); + parseAndAssertThrows('<b style="color:red">x</b>'); + parseAndAssertThrows('<b foo>x</b>'); + parseAndAssertThrows('<b foo=bar></b>'); +} + +function testValidAnchors() { + parseAndAssertNotThrows('<a href="http://google.com">Google</a>'); + parseAndAssertNotThrows('<a href="https://google.com">Google</a>'); +} + +function testInvalidAnchorHrefs() { + parseAndAssertThrows('<a href="ftp://google.com">Google</a>'); + parseAndAssertThrows('<a href="http/google.com">Google</a>'); + parseAndAssertThrows('<a href="javascript:alert(1)">Google</a>'); +} + +function testInvalidAnchorAttributes() { + parseAndAssertThrows('<a name=foo>Google</a>'); + parseAndAssertThrows( + '<a onclick="alert(1)" href="http://google.com">Google</a>'); + parseAndAssertThrows('<a foo="bar(1)" href="http://google.com">Google</a>'); +} + +function testAnchorTarget() { + parseAndAssertNotThrows( + '<a href="http://google.com" target="blank_">Google</a>'); + parseAndAssertNotThrows( + '<a href="http://google.com" target="foo">Google</a>'); +} + +</script> + +</body> +</html> diff --git a/chrome/common/common_resources.grd b/chrome/common/common_resources.grd index f7cb3ac..6dda963 100644 --- a/chrome/common/common_resources.grd +++ b/chrome/common/common_resources.grd @@ -10,7 +10,7 @@ <release seq="1"> <includes> <include name="IDR_JSTEMPLATE_JS" file="..\third_party\jstemplate\jstemplate_compiled.js" type="BINDATA" /> - <include name="IDR_I18N_TEMPLATE_JS" file="..\browser\resources\i18n_template.js" type="BINDATA" /> + <include name="IDR_I18N_TEMPLATE_JS" file="..\browser\resources\shared\js\i18n_template.js" type="BINDATA" /> <include name="IDR_EXTENSION_API_JSON" file="extensions\api\extension_api.json" type="BINDATA" /> </includes> </release> |