diff options
author | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-30 00:41:37 +0000 |
---|---|---|
committer | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-30 00:41:37 +0000 |
commit | a02afd36072896ba584f32664cc8e4bc1889bae0 (patch) | |
tree | d202bf35af23f91f629df2680d3e2af3218f9b8d /chrome/browser/resources/ntp/most_visited.js | |
parent | e030e767b4f0fef8ef1de21ebe977c2a4dec840b (diff) | |
download | chromium_src-a02afd36072896ba584f32664cc8e4bc1889bae0.zip chromium_src-a02afd36072896ba584f32664cc8e4bc1889bae0.tar.gz chromium_src-a02afd36072896ba584f32664cc8e4bc1889bae0.tar.bz2 |
NTP - Refactor the most visited code to uncouple it from the rest of the NTP.
The goal of this refactoring is to allow splitting the different parts of the NTP into different reusable components.
BUG=None
TEST=Manually
Review URL: http://codereview.chromium.org/1695022
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@46021 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/resources/ntp/most_visited.js')
-rw-r--r-- | chrome/browser/resources/ntp/most_visited.js | 987 |
1 files changed, 537 insertions, 450 deletions
diff --git a/chrome/browser/resources/ntp/most_visited.js b/chrome/browser/resources/ntp/most_visited.js index 9a19c0d..fbdffb4 100644 --- a/chrome/browser/resources/ntp/most_visited.js +++ b/chrome/browser/resources/ntp/most_visited.js @@ -2,491 +2,578 @@ // 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}; +// Dependencies that we should remove/formalize: +// ../shared/js/class_list.js +// util.js +// +// afterTransition +// chrome.send +// hideNotification +// isRtl +// localStrings +// logEvent +// showNotification + + +var MostVisited = (function() { + + function addPinnedUrl(item, index) { + chrome.send('addPinnedURL', [item.url, item.title, item.faviconUrl || '', + item.thumbnailUrl || '', String(index)]); } - 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) { + function getItem(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) { + function updatePinnedDom(el, pinned) { el.querySelector('.pin').title = localStrings.getString(pinned ? 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); if (pinned) { - addClass(el, 'pinned'); + el.classList.add('pinned'); } else { - removeClass(el, 'pinned'); + el.classList.remove('pinned'); } - }, + } - getThumbnailIndex: function(el) { + function getThumbnailIndex(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; + function MostVisited(el, useSmallGrid, visible) { + this.element = el; + this.useSmallGrid_ = useSmallGrid; + this.visible_ = visible; + + this.createThumbnails_(); + this.applyMostVisitedRects_(); + + el.addEventListener('click', bind(this.handleClick_, this)); + el.addEventListener('keydown', bind(this.handleKeyDown_, this)); + + document.addEventListener('DOMContentLoaded', + bind(this.ensureSmallGridCorrect, this)); + + // DND + 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)); + } + + MostVisited.prototype = { + togglePinned_: function(el) { + var index = getThumbnailIndex(el); + var item = this.data[index]; + item.pinned = !item.pinned; + if (item.pinned) { + addPinnedUrl(item, index); + } else { + chrome.send('removePinnedURL', [item.url]); + } + updatePinnedDom(el, item.pinned); + }, + + swapPosition_: function(source, destination) { + var nodes = source.parentNode.querySelectorAll('.thumbnail-container'); + var sourceIndex = getThumbnailIndex(source); + var destinationIndex = getThumbnailIndex(destination); + swapDomNodes(source, destination); + + var sourceData = this.data[sourceIndex]; + addPinnedUrl(sourceData, destinationIndex); + sourceData.pinned = true; + updatePinnedDom(source, true); + + var destinationData = this.data[destinationIndex]; + // Only update the destination if it was pinned before. + if (destinationData.pinned) { + addPinnedUrl(destinationData, sourceIndex); + } + this.data[destinationIndex] = sourceData; + this.data[sourceIndex] = destinationData; + }, + + blacklist: function(el) { + var self = this; + var url = el.href; + chrome.send('blacklistURLFromMostVisited', [url]); + + el.classList.add('hide'); + + // Find the old item. + var oldUrls = {}; + var oldIndex = -1; + var oldItem; + var data = this.data; for (var i = 0; i < data.length; i++) { - if (!(data[i].url in oldUrls)) { - newItem = data[i]; - break; + if (data[i].url == url) { + oldItem = data[i]; + oldIndex = i; } + oldUrls[data[i].url] = true; } - 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); + // 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; + } } - 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(); + 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. + self.data = data; + + // Replace old item with new item in the most visited data array. + } else if (oldIndex != -1) { + var oldData = self.data.concat(); + oldData.splice(oldIndex, 1, newItem); + self.data = oldData; + el.classList.add('fade-in'); + } - // Only set overflow to visible if the element is shown. - if (!hidden) { - afterTransition(function() { - mostVisitedElement.style.overflow = ''; + // 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) { + 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(); }); - } - - 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); + }, + + removeFromBlackList: function(url) { + chrome.send('removeURLsFromMostVisitedBlacklist', [url]); + }, + + clearAllBlacklisted: function() { + chrome.send('clearMostVisitedURLsBlacklist', []); + hideNotification(); + }, + + dirty_: false, + invalidate_: function() { + this.dirty_ = true; + }, + + visible_: true, + get visible() { + return this.visible_; + }, + set visible(visible) { + if (this.visible_ != visible) { + this.visible_ = visible; + this.invalidate_(); + } + }, + + useSmallGrid_: false, + get useSmallGrid() { + return this.useSmallGrid_; + }, + set useSmallGrid(b) { + if (this.useSmallGrid_ != b) { + this.useSmallGrid_ = b; + this.invalidate_(); + } + }, + + layout: function() { + if (!this.dirty_) + return; + var d0 = Date.now(); + this.applyMostVisitedRects_(); + this.dirty_ = false; + logEvent('mostVisited.layout: ' + (Date.now() - d0)); + }, + + createThumbnails_: function() { + var singleHtml = + '<a class="thumbnail-container filler" tabindex="1">' + + '<div class="edit-mode-border">' + + '<div class="edit-bar">' + + '<div class="pin"></div>' + + '<div class="spacer"></div>' + + '<div class="remove"></div>' + + '</div>' + + '<span class="thumbnail-wrapper">' + + '<span class="thumbnail"></span>' + + '</span>' + + '</div>' + + '<div class="title">' + + '<div></div>' + + '</div>' + + '</a>'; + this.element.innerHTML = Array(8 + 1).join(singleHtml); + var children = this.element.children; + for (var i = 0; i < 8; i++) { + children[i].id = 't' + i; + } + }, + + getMostVisitedLayoutRects_: function() { + var small = this.useSmallGrid; + + var cols = 4; + var rows = 2; + var marginWidth = 10; + var marginHeight = 7; + var borderWidth = 4; + var thumbWidth = small ? 150 : 207; + var thumbHeight = small ? 93 : 129; + var w = thumbWidth + 2 * borderWidth + 2 * marginWidth; + var h = thumbHeight + 40 + 2 * marginHeight; + var sumWidth = cols * w - 2 * marginWidth; + + var rtl = isRtl(); + var rects = []; + + if (this.visible) { + for (var i = 0; i < rows * cols; i++) { + var row = Math.floor(i / cols); + var col = i % cols; + var left = rtl ? sumWidth - col * w - thumbWidth - 2 * borderWidth : + col * w; + + var top = row * h; + + rects[i] = {left: left, top: top}; + } + } + return rects; + }, + + applyMostVisitedRects_: function() { + if (this.visible) { + var rects = this.getMostVisitedLayoutRects_(); + var children = this.element.children; + for (var i = 0; i < 8; i++) { + var t = children[i]; + t.style.left = rects[i].left + 'px'; + t.style.top = rects[i].top + 'px'; + t.style.right = ''; + var innerStyle = t.firstElementChild.style; + innerStyle.left = innerStyle.top = ''; + } + } + }, + + // Work around for http://crbug.com/25329 + ensureSmallGridCorrect: function(expected) { + if (expected != this.useSmallGrid) + this.applyMostVisitedRects_(); + }, + + getRectByIndex_: function(index) { + return this.getMostVisitedLayoutRects_()[index]; + }, + + // 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 = getThumbnailIndex(this.dragItem_); + var overIndex = getThumbnailIndex(item); + if (dragIndex == -1 || overIndex == -1) { + return; + } + + var dragRect = this.getRectByIndex_(dragIndex); + var overRect = this.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, + + isDragging: function() { + return !!this.dragItem_; + }, + + handleDragStart_: function(e) { + var thumbnail = 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; + this.dragItem_.classList.add('dragging'); + this.dragItem_.style.zIndex = 2; + e.dataTransfer.effectAllowed = 'copyLinkMove'; + } + }, -window.addEventListener('resize', handleWindowResize); + handleDragEnter_: function(e) { + if (this.canDropOnElement_(this.currentOverItem)) { + e.preventDefault(); + } + }, -// 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 = ''; + handleDragOver_: function(e) { + var item = getItem(e.target); + this.currentOverItem = item; + if (this.canDropOnElement_(item)) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; } - this.currentOverItem_ = item; + }, + handleDragLeave_: function(e) { + var item = getItem(e.target); 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; + e.preventDefault(); + } + + this.currentOverItem = null; + }, + + handleDrop_: function(e) { + var dropTarget = getItem(e.target); + if (this.canDropOnElement_(dropTarget)) { + dropTarget.style.zIndex = 1; + this.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(bind(function() { + this.invalidate_(); + this.layout(); + }, this), 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 = ''; + dragItem.classList.remove('dragging'); + + afterTransition(function() { + // Delay resetting zIndex to let the animation finish. + dragItem.style.zIndex = ''; + // Same for overflow. + dragItem.parentNode.style.overflow = ''; + }); + + this.invalidate_(); + this.layout(); + this.dragItem_ = null; + } + }, + + handleDrag_: function(e) { + // Moves the drag item making sure that it is not displayed outside the + // browser viewport. + var item = getItem(e.target); + var rect = this.element.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 = 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 && + el.classList.contains('thumbnail-container') && + !el.classList.contains('filler'); + }, + + + /// data + + data_: null, + get data() { + return this.data_; + }, + set data(data) { + // 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}; + } - var dragRect = mostVisited.getRectByIndex(dragIndex); - var overRect = mostVisited.getRectByIndex(overIndex); + // On setting we need to update the items + this.data_ = data; + this.updateMostVisited_(); + }, - 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; + updateMostVisited_: function() { - style = this.currentOverItem_.firstElementChild.style; - style.left = x2 + 'px'; - style.top = y2 + 'px'; + function getThumbnailClassName(item) { + return 'thumbnail-container' + + (item.pinned ? ' pinned' : '') + + (item.filler ? ' filler' : ''); } - } - }, - 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(); - } + var data = this.data; + var children = this.element.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; + } - 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; + // 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; } - 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 + handleClick_: function(e) { + var target = e.target; + if (target.classList.contains('pin')) { + this.togglePinned_(getItem(target)); e.preventDefault(); + } else if (target.classList.contains('remove')) { + this.blacklist(getItem(target)); + e.preventDefault(); + } + }, + + /** + * Allow blacklisting most visited site using the keyboard. + */ + handleKeyDown_: function(e) { + if (!IS_MAC && e.keyCode == 46 || // Del + IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace + this.blacklist(e.target); + } } - }, - - 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(); + }; + return MostVisited; +})(); |