| |
| // Helpers |
| |
| function $(id) { |
| return document.getElementById(id); |
| } |
| |
| // TODO(arv): Remove these when classList is available in HTML5. |
| // https://bugs.webkit.org/show_bug.cgi?id=20709 |
| function hasClass(el, name) { |
| return el.nodeType == 1 && el.className.split(/\s+/).indexOf(name) != -1; |
| } |
| |
| function addClass(el, name) { |
| el.className += ' ' + name; |
| } |
| |
| function removeClass(el, name) { |
| var names = el.className.split(/\s+/); |
| el.className = names.filter(function(n) { |
| return name != n; |
| }).join(' '); |
| } |
| |
| function findAncestorByClass(el, className) { |
| return findAncestor(el, function(el) { |
| return hasClass(el, className); |
| }); |
| } |
| |
| function findAncestor(el, predicate) { |
| while (el != null && !predicate(el)) { |
| el = el.parentNode; |
| } |
| return el; |
| } |
| |
| // WebKit does not have Node.prototype.swapNode |
| // https://bugs.webkit.org/show_bug.cgi?id=26525 |
| function swapDomNodes(a, b) { |
| var afterA = a.nextSibling; |
| if (afterA == b) { |
| swapDomNodes(b, a); |
| return; |
| } |
| var aParent = a.parentNode; |
| b.parentNode.replaceChild(a, b); |
| aParent.insertBefore(b, afterA); |
| } |
| |
| function bind(fn, selfObj, var_args) { |
| var boundArgs = Array.prototype.slice.call(arguments, 2); |
| return function() { |
| var args = Array.prototype.slice.call(arguments); |
| args.unshift.apply(args, boundArgs); |
| return fn.apply(selfObj, args); |
| } |
| } |
| |
| var mostVisitedData = []; |
| var gotMostVisited = false; |
| var gotShownSections = false; |
| |
| function mostVisitedPages(data) { |
| 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); |
| layoutMostVisited(); |
| |
| gotMostVisited = true; |
| onDataLoaded(); |
| } |
| |
| function downloadsList(data) { |
| logEvent('received downloads'); |
| data.length = Math.min(data.length, 5); |
| processData('#download-items', data); |
| } |
| |
| function recentlyClosedTabs(data) { |
| logEvent('received recently closed tabs'); |
| data.length = Math.min(data.length, 5); |
| processData('#tab-items', data); |
| } |
| |
| function onShownSections(m) { |
| logEvent('received shown sections'); |
| setShownSections(m); |
| gotShownSections = true; |
| onDataLoaded(); |
| } |
| |
| function saveShownSections() { |
| chrome.send('setShownSections', [String(shownSections)]); |
| } |
| |
| function layoutMostVisited() { |
| var d0 = Date.now(); |
| var mostVisitedElement = $('most-visited'); |
| var thumbnails = mostVisitedElement.querySelectorAll('.thumbnail-container'); |
| |
| if (thumbnails.length < 8) { |
| return; |
| } |
| |
| var small = useSmallGrid(); |
| |
| var cols = 4; |
| var rows = 2; |
| var marginWidth = 10; |
| var marginHeight = 7; |
| var borderWidth = 4; |
| var thumbWidth = small ? 150 : 212; |
| var thumbHeight = small ? 93 : 132; |
| var w = thumbWidth + 2 * borderWidth + 2 * marginWidth; |
| var h = thumbHeight + 40 + 2 * marginHeight; |
| var sumWidth = cols * w - 2 * marginWidth; |
| var sumHeight = rows * h; |
| var opacity = 1; |
| |
| if (shownSections & Section.LIST) { |
| w = (sumWidth + 2 * marginWidth) / 2; |
| h = 45; |
| rows = 4; |
| cols = 2; |
| sumHeight = rows * h; |
| addClass(mostVisitedElement, 'list'); |
| } else if (shownSections & Section.THUMB) { |
| removeClass(mostVisitedElement, 'list'); |
| } else { |
| sumHeight = 0; |
| opacity = 0; |
| } |
| |
| mostVisitedElement.style.height = sumHeight + 'px'; |
| mostVisitedElement.style.opacity = opacity; |
| // We set overflow to hidden so that the most visited element does not |
| // "leak" when we hide and show it. |
| mostVisitedElement.style.overflow = 'hidden'; |
| |
| var rtl = document.documentElement.dir == 'rtl'; |
| |
| if (shownSections & Section.THUMB || shownSections & Section.LIST) { |
| for (var i = 0; i < thumbnails.length; i++) { |
| var t = thumbnails[i]; |
| |
| var row, col; |
| if (shownSections & Section.THUMB) { |
| row = Math.floor(i / cols); |
| col = i % cols; |
| } else { |
| col = Math.floor(i / rows); |
| row = i % rows; |
| } |
| |
| if (shownSections & Section.THUMB) { |
| t.style.left = (rtl ? |
| sumWidth - col * w - thumbWidth - 2 * borderWidth : |
| col * w) + 'px'; |
| } else { |
| t.style.left = (rtl ? |
| sumWidth - col * w - w + 2 * marginWidth : |
| col * w) + 'px'; |
| } |
| t.style.top = row * h + 'px'; |
| |
| if (shownSections & Section.LIST) { |
| t.style.width = w - 2 * marginWidth + 'px'; |
| } else { |
| t.style.width = ''; |
| } |
| } |
| } |
| |
| afterTransition(function() { |
| mostVisitedElement.style.overflow = ''; |
| }); |
| |
| logEvent('layoutMostVisited: ' + (Date.now() - d0)); |
| } |
| |
| // This global variable is used to skip parts of the DOM tree for the global |
| // jst processing done by the i18n. |
| var processing = false; |
| |
| function processData(selector, data) { |
| var output = document.querySelector(selector); |
| |
| // Wait until ready |
| if (typeof JsEvalContext !== 'function' || !output) { |
| logEvent('JsEvalContext is not yet available, ' + selector); |
| document.addEventListener('DOMContentLoaded', function() { |
| processData(selector, data); |
| }); |
| } else { |
| var d0 = Date.now(); |
| var input = new JsEvalContext(data); |
| processing = true; |
| jstProcess(input, output); |
| processing = false; |
| logEvent('processData: ' + selector + ', ' + (Date.now() - d0)); |
| } |
| } |
| |
| var thumbnailTemplate; |
| |
| function getThumbnailClassName(data) { |
| return 'thumbnail-container' + |
| (data.pinned ? ' pinned' : '') + |
| (data.filler ? ' filler' : ''); |
| } |
| |
| function renderMostVisited(data) { |
| var parent = $('most-visited'); |
| if (!thumbnailTemplate) { |
| thumbnailTemplate = $('thumbnail-template'); |
| thumbnailTemplate.parentNode.removeChild(thumbnailTemplate); |
| } |
| |
| var children = parent.children; |
| for (var i = 0; i < data.length; i++) { |
| var d = data[i]; |
| var reuse = !!children[i]; |
| var t = children[i] || thumbnailTemplate.cloneNode(true); |
| t.style.display = ''; |
| t.className = getThumbnailClassName(d); |
| t.title = d.title; |
| t.href = d.url; |
| t.querySelector('.edit-link').textContent = |
| localStrings.getString('editthumbnail'); |
| |
| // 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. |
| t.querySelector('.thumbnail-wrapper').style.backgroundImage = |
| 'url(chrome://thumb/' + d.url + ')'; |
| var titleDiv = t.querySelector('.title > div'); |
| titleDiv.textContent = d.title; |
| titleDiv.style.backgroundImage = 'url(chrome://favicon/' + d.url + ')'; |
| titleDiv.dir = d.direction; |
| if (!reuse) { |
| parent.appendChild(t); |
| } |
| } |
| } |
| |
| /** |
| * Calls chrome.send with a callback and restores the original afterwards. |
| */ |
| function chromeSend(name, params, callbackName, callback) { |
| var old = global[callbackName]; |
| global[callbackName] = function() { |
| // restore |
| global[callbackName] = old; |
| |
| var args = Array.prototype.slice.call(arguments); |
| return callback.apply(global, args); |
| }; |
| chrome.send(name, params); |
| } |
| |
| function useSmallGrid() { |
| return document.body.clientWidth <= 940; |
| } |
| |
| function handleWindowResize(e, opt_noUpdate) { |
| var body = document.body; |
| if (!body || body.clientWidth < 10) { |
| // We're probably a background tab, so don't do anything. |
| return; |
| } |
| |
| var hasSmallClass = hasClass(body, 'small'); |
| if (hasSmallClass && !useSmallGrid()) { |
| removeClass(body, 'small'); |
| if (!opt_noUpdate) { |
| layoutMostVisited(); |
| layoutLowerSections(); |
| } |
| } else if (!hasSmallClass && useSmallGrid()) { |
| addClass(body, 'small'); |
| if (!opt_noUpdate) { |
| layoutMostVisited(); |
| layoutLowerSections(); |
| } |
| } |
| } |
| |
| /** |
| * Bitmask for the different UI sections. |
| * This matches the Section enum in ../dom_ui/shown_sections_handler.h |
| * @enum {number} |
| */ |
| var Section = { |
| THUMB: 1, |
| LIST: 2, |
| RECENT: 4, |
| RECOMMENDATIONS: 8 |
| }; |
| |
| var shownSections = Section.RECENT | Section.RECOMMENDATIONS; |
| |
| function showSection(section) { |
| if (!(section & shownSections)) { |
| // THUMBS and LIST are mutually exclusive. |
| if (section == Section.THUMB) { |
| hideSection(Section.LIST); |
| } else if (section == Section.LIST) { |
| hideSection(Section.THUMB); |
| } |
| |
| shownSections |= section; |
| notifyLowerSectionForChange(section, false); |
| |
| mostVisited.updateDisplayMode(); |
| layoutMostVisited(); |
| updateOptionMenu(); |
| layoutLowerSections(); |
| } |
| } |
| |
| function hideSection(section) { |
| if (section & shownSections) { |
| shownSections &= ~section; |
| notifyLowerSectionForChange(section, true); |
| |
| mostVisited.updateDisplayMode(); |
| layoutMostVisited(); |
| updateOptionMenu(); |
| layoutLowerSections(); |
| } |
| } |
| |
| function notifyLowerSectionForChange(section, large) { |
| // Notify recent and recommendations if they need to display more data. |
| if (section == Section.RECENT || section == Section.RECOMMENDATIONS) { |
| // we are hiding one of them so if the other one is visible it is now |
| // {@code large}. |
| if (shownSections & Section.RECENT) { |
| recentChangedSize(large); |
| } else if (shownSections & Section.RECOMMENDATIONS) { |
| recommendationsChangedSize(large); |
| } |
| } |
| } |
| |
| /** |
| * This is called when we get the shown sections pref from the backend. |
| */ |
| function setShownSections(mask) { |
| if (mask != shownSections) { |
| shownSections = mask; |
| mostVisited.updateDisplayMode(); |
| layoutMostVisited(); |
| layoutLowerSections(); |
| updateOptionMenu(); |
| } |
| } |
| |
| var mostVisited = { |
| 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]; |
| if (data.pinned) { |
| removeClass(el, 'pinned'); |
| chrome.send('removePinnedURL', [data.url]); |
| } else { |
| addClass(el, 'pinned'); |
| chrome.send('addPinnedURL', [data.url, data.title, String(index)]); |
| } |
| data.pinned = !data.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]; |
| chrome.send('addPinnedURL', [sourceData.url, sourceData.title, |
| String(destinationIndex)]); |
| sourceData.pinned = true; |
| addClass(source, 'pinned'); |
| var destinationData = mostVisitedData[destinationIndex]; |
| // Only update the destination if it was pinned before. |
| if (destinationData.pinned) { |
| chrome.send('addPinnedURL', [destinationData.url, destinationData.title, |
| String(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) { |
| newItem = {filler: true}; |
| } |
| |
| // Replace old item with new item in the mostVisitedData array. |
| if (oldIndex != -1) { |
| mostVisitedData.splice(oldIndex, 1, newItem); |
| mostVisitedPages(mostVisitedData); |
| addClass(el, 'fade-in'); |
| } |
| |
| var text = localStrings.formatString('thumbnailremovednotification', |
| oldItem.title); |
| var actionText = localStrings.getString('undothumbnailremove'); |
| |
| // Show notification and add undo callback function. |
| var wasPinned = oldItem.pinned; |
| showNotification(text, actionText, function() { |
| self.removeFromBlackList(url); |
| if (wasPinned) { |
| chromeSend('addPinnedURL', [url, oldItem.title, String(oldIndex)]); |
| } |
| chrome.send('getMostVisited'); |
| }); |
| }); |
| }, |
| |
| removeFromBlackList: function(url) { |
| chrome.send('removeURLsFromMostVisitedBlacklist', [url]); |
| }, |
| |
| clearAllBlacklisted: function() { |
| chrome.send('clearMostVisitedURLsBlacklist', []); |
| }, |
| |
| updateDisplayMode: function() { |
| var thumbCheckbox = $('thumb-checkbox'); |
| var listCheckbox = $('list-checkbox'); |
| var mostVisitedElement = $('most-visited'); |
| |
| if (shownSections & Section.THUMB) { |
| thumbCheckbox.checked = true; |
| listCheckbox.checked = false; |
| removeClass(mostVisitedElement, 'list'); |
| } else if (shownSections & Section.LIST) { |
| thumbCheckbox.checked = false; |
| listCheckbox.checked = true; |
| addClass(mostVisitedElement, 'list'); |
| } else { |
| thumbCheckbox.checked = false; |
| listCheckbox.checked = false; |
| } |
| } |
| }; |
| |
| function recentChangedSize(large) { |
| // TODO(arv): Implement |
| } |
| |
| function recommendationsChangedSize(large) { |
| // TODO(arv): Implement |
| } |
| |
| // Recent activities |
| |
| function layoutLowerSections() { |
| // This lower sections are inline blocks so all we need to do is to set the |
| // width and opacity. |
| var lowerSectionsElement = $('lower-sections'); |
| var recentElement = $('recent-activities'); |
| var recommendationsElement = $('recommendations'); |
| var spacer = recentElement.nextElementSibling; |
| |
| var totalWidth = useSmallGrid() ? 692 : 940; |
| var spacing = 20; |
| var rtl = document.documentElement.dir == 'rtl'; |
| |
| var recentShown = shownSections & Section.RECENT; |
| var recommendationsShown = shownSections & Section.RECOMMENDATIONS; |
| |
| if (recentShown || recommendationsShown) { |
| lowerSectionsElement.style.height = '175px' |
| lowerSectionsElement.style.opacity = ''; |
| } else { |
| lowerSectionsElement.style.height = lowerSectionsElement.style.opacity = 0; |
| } |
| |
| if (recentShown && recommendationsShown) { |
| var w = (totalWidth - spacing) / 2; |
| recentElement.style.width = recommendationsElement.style.width = w + 'px' |
| recentElement.style.opacity = recommendationsElement.style.opacity = ''; |
| spacer.style.width = spacing + 'px'; |
| } else if (recentShown) { |
| recentElement.style.width = totalWidth + 'px'; |
| recentElement.style.opacity = ''; |
| recommendationsElement.style.width = |
| recommendationsElement.style.opacity = 0; |
| spacer.style.width = 0; |
| } else if (recommendationsShown) { |
| recommendationsElement.style.width = totalWidth + 'px'; |
| recommendationsElement.style.opacity = ''; |
| recentElement.style.width = recentElement.style.opacity = 0; |
| spacer.style.width = 0; |
| } |
| } |
| |
| /** |
| * Returns the text used for a recently closed window. |
| * @param {number} numTabs Number of tabs in the window. |
| * @return {string} The text to use. |
| */ |
| function formatTabsText(numTabs) { |
| if (numTabs == 1) |
| return localStrings.getString('closedwindowsingle'); |
| return localStrings.formatString('closedwindowmultiple', numTabs); |
| } |
| |
| /** |
| * We need both most visited and the shown sections to be considered loaded. |
| * @return {boolean} |
| */ |
| function onDataLoaded() { |
| if (gotMostVisited && gotShownSections) { |
| // Remove class name in a timeout so that changes done in this JS thread are |
| // not animated. |
| window.setTimeout(function() { |
| removeClass(document.body, 'loading'); |
| }, 10); |
| } |
| } |
| |
| // Theme related |
| |
| function themeChanged() { |
| $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); |
| updateAttribution(); |
| } |
| |
| function updateAttribution() { |
| // TODO(arv): Implement |
| //$('attribution-img').src = 'chrome://theme/theme_ntp_attribution?' + |
| // Date.now(); |
| } |
| |
| function bookmarkBarAttached() { |
| document.documentElement.setAttribute("bookmarkbarattached", "true"); |
| } |
| |
| function bookmarkBarDetached() { |
| document.documentElement.setAttribute("bookmarkbarattached", "false"); |
| } |
| |
| function viewLog() { |
| var lines = []; |
| var start = log[0][1]; |
| |
| for (var i = 0; i < log.length; i++) { |
| lines.push((log[i][1] - start) + ': ' + log[i][0]); |
| } |
| |
| console.log(lines.join('\n')); |
| } |
| |
| // Updates the visibility of the menu items. |
| function updateOptionMenu() { |
| var menuItems = $('option-menu').children; |
| for (var i = 0; i < menuItems.length; i++) { |
| var item = menuItems[i]; |
| var section = Section[item.getAttribute('section')]; |
| var show = item.getAttribute('show') == 'true'; |
| // Hide show items if already shown. Hide hide items if already hidden. |
| var hideMenuItem = show == !!(shownSections & section); |
| item.style.display = hideMenuItem ? 'none' : ''; |
| } |
| } |
| |
| // We apply the size class here so that we don't trigger layout animations |
| // onload. |
| |
| handleWindowResize(null, true); |
| |
| var localStrings = new LocalStrings(); |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Things we know are not needed at startup go below here |
| |
| // Notification |
| |
| function afterTransition(f) { |
| // The duration of all transitions are 500ms |
| window.setTimeout(f, 500); |
| } |
| |
| function showNotification(text, actionText, f) { |
| var notificationElement = $('notification'); |
| var actionLink = notificationElement.querySelector('.link'); |
| notificationElement.firstElementChild.textContent = text; |
| |
| actionLink.textContent = actionText; |
| actionLink.onclick = function() { |
| f(); |
| removeClass(notificationElement, 'show'); |
| // Since we have a :hover rule to not hide the notification banner when the |
| // mouse is over we need force it to hide. We remove the hide class after |
| // a short timeout to allow the banner to be shown again. |
| addClass(notificationElement, 'hide'); |
| afterTransition(function() { |
| removeClass(notificationElement, 'hide'); |
| }) |
| }; |
| addClass(notificationElement, 'show'); |
| window.setTimeout(function() { |
| removeClass(notificationElement, 'show'); |
| }, 10000); |
| } |
| |
| // Options menu |
| // TODO(arv): Keyboard navigation of the menu items. |
| |
| function showMenu(button, menu) { |
| function hide() { |
| menu.style.display = ''; |
| menu.removeEventListener('blur', hide); |
| window.removeEventListener('blur', hide); |
| }; |
| menu.addEventListener('blur', hide); |
| window.addEventListener('blur', hide); |
| menu.style.display = 'block'; |
| menu.focus(); |
| } |
| |
| $('option-button').addEventListener('click', function(e) { |
| showMenu(this, $('option-menu')); |
| }); |
| |
| $('option-menu').addEventListener('click', function(e) { |
| var section = Section[e.target.getAttribute('section')]; |
| var show = e.target.getAttribute('show') == 'true'; |
| if (show) { |
| showSection(section); |
| } else { |
| hideSection(section); |
| } |
| |
| // Hide menu now. |
| this.style.display = 'none'; |
| |
| layoutLowerSections(); |
| mostVisited.updateDisplayMode(); |
| layoutMostVisited(); |
| |
| saveShownSections(); |
| }); |
| |
| $('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(); |
| } else if (hasClass(target, 'edit-link')) { |
| alert('Not implemented yet') |
| e.preventDefault(); |
| } |
| }); |
| |
| $('downloads').addEventListener('click', function(e) { |
| var el = findAncestor(e.target, function(el) { |
| return el.fileId !== undefined; |
| }); |
| if (el && el.fileId) { |
| chrome.send('openFile', [String(el.fileId)]); |
| e.preventDefault(); |
| } |
| }); |
| |
| $('recent-tabs').addEventListener('click', function(e) { |
| var el = findAncestor(e.target, function(el) { |
| return el.sessionId !== undefined; |
| }); |
| if (el && el.sessionId !== undefined) { |
| chrome.send('reopenTab', [String(el.sessionId)]); |
| e.preventDefault(); |
| } |
| }); |
| |
| $('thumb-checkbox').addEventListener('change', function(e) { |
| if (e.target.checked) { |
| showSection(Section.THUMB); |
| } else { |
| hideSection(Section.THUMB); |
| } |
| mostVisited.updateDisplayMode(); |
| layoutMostVisited(); |
| saveShownSections(); |
| }); |
| |
| $('list-checkbox').addEventListener('change', function(e) { |
| var newValue = shownSections; |
| if (e.target.checked) { |
| showSection(Section.LIST); |
| } else { |
| hideSection(Section.LIST); |
| } |
| mostVisited.updateDisplayMode(); |
| layoutMostVisited(); |
| saveShownSections(); |
| }); |
| |
| window.addEventListener('load', bind(logEvent, global, 'onload fired')); |
| window.addEventListener('load', onDataLoaded); |
| window.addEventListener('resize', handleWindowResize); |
| document.addEventListener('DOMContentLoaded', bind(logEvent, global, |
| 'domcontentloaded fired')); |
| |
| // DnD |
| |
| var dnd = { |
| currentOverItem: null, |
| dragItem: null, |
| startX: 0, |
| startY: 0, |
| startScreenX: 0, |
| startScreenY: 0, |
| dragEndTimer: null, |
| |
| handleDragStart: function(e) { |
| var thumbnail = mostVisited.getItem(e.target); |
| if (thumbnail) { |
| e.dataTransfer.setData('text/uri-list', mostVisited.getHref(thumbnail)); |
| this.dragItem = thumbnail; |
| addClass(this.dragItem, 'dragging'); |
| this.dragItem.style.zIndex = 2; |
| } |
| }, |
| |
| handleDragEnter: function(e) { |
| this.currentOverItem = mostVisited.getItem(e.target); |
| if (this.canDropOnElement(this.currentOverItem)) { |
| e.preventDefault(); |
| } |
| }, |
| |
| handleDragOver: function(e) { |
| var item = mostVisited.getItem(e.target); |
| if (this.canDropOnElement(item)) { |
| e.preventDefault(); |
| } |
| }, |
| |
| 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); |
| layoutMostVisited(); |
| e.preventDefault(); |
| if (this.dragEndTimer) { |
| window.clearTimeout(this.dragEndTimer); |
| this.dragEndTimer = null; |
| } |
| afterTransition(function() { |
| dropTarget.style.zIndex = ''; |
| }); |
| } |
| }, |
| |
| handleDragEnd: function(e) { |
| // WebKit fires dragend before drop. |
| 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 = ''; |
| }); |
| var self = this; |
| this.dragEndTimer = window.setTimeout(function() { |
| // These things needto happen after the drop event. |
| layoutMostVisited(); |
| self.dragItem = null; |
| }, 10); |
| |
| } |
| }, |
| |
| handleDrag: function(e) { |
| var item = mostVisited.getItem(e.target); |
| var rect = document.querySelector('#most-visited').getBoundingClientRect(); |
| item.style.pointerEvents = 'none'; |
| |
| item.style.left = this.startX + e.screenX - this.startScreenX + 'px'; |
| item.style.top = this.startY + e.screenY - this.startScreenY + 'px'; |
| }, |
| |
| // 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; |
| } |
| }, |
| |
| 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(); |