summaryrefslogtreecommitdiffstats
path: root/chrome/browser/resources/new_new_tab.js
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/resources/new_new_tab.js')
-rw-r--r--chrome/browser/resources/new_new_tab.js891
1 files changed, 891 insertions, 0 deletions
diff --git a/chrome/browser/resources/new_new_tab.js b/chrome/browser/resources/new_new_tab.js
new file mode 100644
index 0000000..b62aefc
--- /dev/null
+++ b/chrome/browser/resources/new_new_tab.js
@@ -0,0 +1,891 @@
+
+// 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.style.direction = 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');
+ // TODO(arv): Update grd file to use %s so we can use formatString
+ // http://crbug.com/14878
+ // return localStrings.formatString('closedwindowmultiple', numTabs);
+ return localStrings.getString('closedwindowmultiple').replace(/%/, 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();