| OLD | NEW |
| 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 1 | 4 |
| 2 // Helpers | 5 // Helpers |
| 3 | 6 |
| 4 function findAncestorByClass(el, className) { | 7 function findAncestorByClass(el, className) { |
| 5 return findAncestor(el, function(el) { | 8 return findAncestor(el, function(el) { |
| 6 return hasClass(el, className); | 9 return hasClass(el, className); |
| 7 }); | 10 }); |
| 8 } | 11 } |
| 9 | 12 |
| 10 /** | 13 /** |
| (...skipping 29 matching lines...) Expand all Loading... |
| 40 return function() { | 43 return function() { |
| 41 var args = Array.prototype.slice.call(arguments); | 44 var args = Array.prototype.slice.call(arguments); |
| 42 args.unshift.apply(args, boundArgs); | 45 args.unshift.apply(args, boundArgs); |
| 43 return fn.apply(selfObj, args); | 46 return fn.apply(selfObj, args); |
| 44 } | 47 } |
| 45 } | 48 } |
| 46 | 49 |
| 47 const IS_MAC = /$Mac/.test(navigator.platform); | 50 const IS_MAC = /$Mac/.test(navigator.platform); |
| 48 | 51 |
| 49 var loading = true; | 52 var loading = true; |
| 50 var mostVisitedData = []; | |
| 51 var gotMostVisited = false; | |
| 52 | |
| 53 function mostVisitedPages(data, firstRun) { | |
| 54 logEvent('received most visited pages'); | |
| 55 | |
| 56 // We append the class name with the "filler" so that we can style fillers | |
| 57 // differently. | |
| 58 var maxItems = 8; | |
| 59 data.length = Math.min(maxItems, data.length); | |
| 60 var len = data.length; | |
| 61 for (var i = len; i < maxItems; i++) { | |
| 62 data[i] = {filler: true}; | |
| 63 } | |
| 64 | |
| 65 mostVisitedData = data; | |
| 66 renderMostVisited(data); | |
| 67 | |
| 68 gotMostVisited = true; | |
| 69 onDataLoaded(); | |
| 70 | |
| 71 // Only show the first run notification if first run. | |
| 72 if (firstRun) { | |
| 73 showFirstRunNotification(); | |
| 74 } | |
| 75 } | |
| 76 | 53 |
| 77 function getAppsCallback(data) { | 54 function getAppsCallback(data) { |
| 78 var appsSection = $('apps-section'); | 55 var appsSection = $('apps-section'); |
| 79 appsSection.innerHTML = ''; | 56 appsSection.innerHTML = ''; |
| 80 appsSection.style.display = data.length ? 'block' : ''; | 57 appsSection.style.display = data.length ? 'block' : ''; |
| 81 | 58 |
| 82 data.forEach(function(app) { | 59 data.forEach(function(app) { |
| 83 appsSection.appendChild(apps.createElement(app)); | 60 appsSection.appendChild(apps.createElement(app)); |
| 84 }); | 61 }); |
| 85 } | 62 } |
| (...skipping 127 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 213 | 190 |
| 214 mostVisited.updateDisplayMode(); | 191 mostVisited.updateDisplayMode(); |
| 215 renderRecentlyClosed(); | 192 renderRecentlyClosed(); |
| 216 } | 193 } |
| 217 } | 194 } |
| 218 | 195 |
| 219 function saveShownSections() { | 196 function saveShownSections() { |
| 220 chrome.send('setShownSections', [String(shownSections)]); | 197 chrome.send('setShownSections', [String(shownSections)]); |
| 221 } | 198 } |
| 222 | 199 |
| 223 function getThumbnailClassName(data) { | |
| 224 return 'thumbnail-container' + | |
| 225 (data.pinned ? ' pinned' : '') + | |
| 226 (data.filler ? ' filler' : ''); | |
| 227 } | |
| 228 | |
| 229 function url(s) { | 200 function url(s) { |
| 230 // http://www.w3.org/TR/css3-values/#uris | 201 // http://www.w3.org/TR/css3-values/#uris |
| 231 // Parentheses, commas, whitespace characters, single quotes (') and double | 202 // Parentheses, commas, whitespace characters, single quotes (') and double |
| 232 // quotes (") appearing in a URI must be escaped with a backslash | 203 // quotes (") appearing in a URI must be escaped with a backslash |
| 233 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); | 204 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); |
| 234 // WebKit has a bug when it comes to URLs that end with \ | 205 // WebKit has a bug when it comes to URLs that end with \ |
| 235 // https://bugs.webkit.org/show_bug.cgi?id=28885 | 206 // https://bugs.webkit.org/show_bug.cgi?id=28885 |
| 236 if (/\\\\$/.test(s2)) { | 207 if (/\\\\$/.test(s2)) { |
| 237 // Add a space to work around the WebKit bug. | 208 // Add a space to work around the WebKit bug. |
| 238 s2 += ' '; | 209 s2 += ' '; |
| 239 } | 210 } |
| 240 return 'url("' + s2 + '")'; | 211 return 'url("' + s2 + '")'; |
| 241 } | 212 } |
| 242 | 213 |
| 243 function renderMostVisited(data) { | |
| 244 var parent = $('most-visited'); | |
| 245 var children = parent.children; | |
| 246 for (var i = 0; i < data.length; i++) { | |
| 247 var d = data[i]; | |
| 248 var t = children[i]; | |
| 249 | |
| 250 // If we have a filler continue | |
| 251 var oldClassName = t.className; | |
| 252 var newClassName = getThumbnailClassName(d); | |
| 253 if (oldClassName != newClassName) { | |
| 254 t.className = newClassName; | |
| 255 } | |
| 256 | |
| 257 // No need to continue if this is a filler. | |
| 258 if (newClassName == 'thumbnail-container filler') { | |
| 259 // Make sure the user cannot tab to the filler. | |
| 260 t.tabIndex = -1; | |
| 261 continue; | |
| 262 } | |
| 263 // Allow focus. | |
| 264 t.tabIndex = 1; | |
| 265 | |
| 266 t.href = d.url; | |
| 267 t.querySelector('.pin').title = localStrings.getString(d.pinned ? | |
| 268 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); | |
| 269 t.querySelector('.remove').title = | |
| 270 localStrings.getString('removethumbnailtooltip'); | |
| 271 | |
| 272 // There was some concern that a malformed malicious URL could cause an XSS | |
| 273 // attack but setting style.backgroundImage = 'url(javascript:...)' does | |
| 274 // not execute the JavaScript in WebKit. | |
| 275 | |
| 276 var thumbnailUrl = d.thumbnailUrl || 'chrome://thumb/' + d.url; | |
| 277 t.querySelector('.thumbnail-wrapper').style.backgroundImage = | |
| 278 url(thumbnailUrl); | |
| 279 var titleDiv = t.querySelector('.title > div'); | |
| 280 titleDiv.xtitle = titleDiv.textContent = d.title; | |
| 281 var faviconUrl = d.faviconUrl || 'chrome://favicon/' + d.url; | |
| 282 titleDiv.style.backgroundImage = url(faviconUrl); | |
| 283 titleDiv.dir = d.direction; | |
| 284 } | |
| 285 } | |
| 286 | |
| 287 /** | 214 /** |
| 288 * Calls chrome.send with a callback and restores the original afterwards. | 215 * Calls chrome.send with a callback and restores the original afterwards. |
| 289 */ | 216 */ |
| 290 function chromeSend(name, params, callbackName, callback) { | 217 function chromeSend(name, params, callbackName, callback) { |
| 291 var old = global[callbackName]; | 218 var old = global[callbackName]; |
| 292 global[callbackName] = function() { | 219 global[callbackName] = function() { |
| 293 // restore | 220 // restore |
| 294 global[callbackName] = old; | 221 global[callbackName] = old; |
| 295 | 222 |
| 296 var args = Array.prototype.slice.call(arguments); | 223 var args = Array.prototype.slice.call(arguments); |
| (...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 355 case Section.RECENT: | 282 case Section.RECENT: |
| 356 renderRecentlyClosed(); | 283 renderRecentlyClosed(); |
| 357 break; | 284 break; |
| 358 case Section.TIPS: | 285 case Section.TIPS: |
| 359 addClass($('tip-line'), 'hidden'); | 286 addClass($('tip-line'), 'hidden'); |
| 360 break; | 287 break; |
| 361 } | 288 } |
| 362 } | 289 } |
| 363 } | 290 } |
| 364 | 291 |
| 365 var mostVisited = { | |
| 366 addPinnedUrl_: function(data, index) { | |
| 367 chrome.send('addPinnedURL', [data.url, data.title, data.faviconUrl || '', | |
| 368 data.thumbnailUrl || '', String(index)]); | |
| 369 }, | |
| 370 getItem: function(el) { | |
| 371 return findAncestorByClass(el, 'thumbnail-container'); | |
| 372 }, | |
| 373 | |
| 374 getHref: function(el) { | |
| 375 return el.href; | |
| 376 }, | |
| 377 | |
| 378 togglePinned: function(el) { | |
| 379 var index = this.getThumbnailIndex(el); | |
| 380 var data = mostVisitedData[index]; | |
| 381 data.pinned = !data.pinned; | |
| 382 if (data.pinned) { | |
| 383 this.addPinnedUrl_(data, index); | |
| 384 } else { | |
| 385 chrome.send('removePinnedURL', [data.url]); | |
| 386 } | |
| 387 this.updatePinnedDom_(el, data.pinned); | |
| 388 }, | |
| 389 | |
| 390 updatePinnedDom_: function(el, pinned) { | |
| 391 el.querySelector('.pin').title = localStrings.getString(pinned ? | |
| 392 'unpinthumbnailtooltip' : 'pinthumbnailtooltip'); | |
| 393 if (pinned) { | |
| 394 addClass(el, 'pinned'); | |
| 395 } else { | |
| 396 removeClass(el, 'pinned'); | |
| 397 } | |
| 398 }, | |
| 399 | |
| 400 getThumbnailIndex: function(el) { | |
| 401 var nodes = el.parentNode.querySelectorAll('.thumbnail-container'); | |
| 402 return Array.prototype.indexOf.call(nodes, el); | |
| 403 }, | |
| 404 | |
| 405 swapPosition: function(source, destination) { | |
| 406 var nodes = source.parentNode.querySelectorAll('.thumbnail-container'); | |
| 407 var sourceIndex = this.getThumbnailIndex(source); | |
| 408 var destinationIndex = this.getThumbnailIndex(destination); | |
| 409 swapDomNodes(source, destination); | |
| 410 | |
| 411 var sourceData = mostVisitedData[sourceIndex]; | |
| 412 this.addPinnedUrl_(sourceData, destinationIndex); | |
| 413 sourceData.pinned = true; | |
| 414 this.updatePinnedDom_(source, true); | |
| 415 | |
| 416 var destinationData = mostVisitedData[destinationIndex]; | |
| 417 // Only update the destination if it was pinned before. | |
| 418 if (destinationData.pinned) { | |
| 419 this.addPinnedUrl_(destinationData, sourceIndex); | |
| 420 } | |
| 421 mostVisitedData[destinationIndex] = sourceData; | |
| 422 mostVisitedData[sourceIndex] = destinationData; | |
| 423 }, | |
| 424 | |
| 425 blacklist: function(el) { | |
| 426 var self = this; | |
| 427 var url = this.getHref(el); | |
| 428 chrome.send('blacklistURLFromMostVisited', [url]); | |
| 429 | |
| 430 addClass(el, 'hide'); | |
| 431 | |
| 432 // Find the old item. | |
| 433 var oldUrls = {}; | |
| 434 var oldIndex = -1; | |
| 435 var oldItem; | |
| 436 for (var i = 0; i < mostVisitedData.length; i++) { | |
| 437 if (mostVisitedData[i].url == url) { | |
| 438 oldItem = mostVisitedData[i]; | |
| 439 oldIndex = i; | |
| 440 } | |
| 441 oldUrls[mostVisitedData[i].url] = true; | |
| 442 } | |
| 443 | |
| 444 // Send 'getMostVisitedPages' with a callback since we want to find the new | |
| 445 // page and add that in the place of the removed page. | |
| 446 chromeSend('getMostVisited', [], 'mostVisitedPages', function(data) { | |
| 447 // Find new item. | |
| 448 var newItem; | |
| 449 for (var i = 0; i < data.length; i++) { | |
| 450 if (!(data[i].url in oldUrls)) { | |
| 451 newItem = data[i]; | |
| 452 break; | |
| 453 } | |
| 454 } | |
| 455 | |
| 456 if (!newItem) { | |
| 457 // If no other page is available to replace the blacklisted item, | |
| 458 // we need to reorder items s.t. all filler items are in the rightmost | |
| 459 // indices. | |
| 460 mostVisitedPages(data); | |
| 461 | |
| 462 // Replace old item with new item in the mostVisitedData array. | |
| 463 } else if (oldIndex != -1) { | |
| 464 mostVisitedData.splice(oldIndex, 1, newItem); | |
| 465 mostVisitedPages(mostVisitedData); | |
| 466 addClass(el, 'fade-in'); | |
| 467 } | |
| 468 | |
| 469 // We wrap the title in a <span class=blacklisted-title>. We pass an empty | |
| 470 // string to the notifier function and use DOM to insert the real string. | |
| 471 var actionText = localStrings.getString('undothumbnailremove'); | |
| 472 | |
| 473 // Show notification and add undo callback function. | |
| 474 var wasPinned = oldItem.pinned; | |
| 475 showNotification('', actionText, function() { | |
| 476 self.removeFromBlackList(url); | |
| 477 if (wasPinned) { | |
| 478 self.addPinnedUrl_(oldItem, oldIndex); | |
| 479 } | |
| 480 chrome.send('getMostVisited'); | |
| 481 }); | |
| 482 | |
| 483 // Now change the DOM. | |
| 484 var removeText = localStrings.getString('thumbnailremovednotification'); | |
| 485 var notifySpan = document.querySelector('#notification > span'); | |
| 486 notifySpan.textContent = removeText; | |
| 487 | |
| 488 // Focus the undo link. | |
| 489 var undoLink = document.querySelector( | |
| 490 '#notification > .link > [tabindex]'); | |
| 491 undoLink.focus(); | |
| 492 }); | |
| 493 }, | |
| 494 | |
| 495 removeFromBlackList: function(url) { | |
| 496 chrome.send('removeURLsFromMostVisitedBlacklist', [url]); | |
| 497 }, | |
| 498 | |
| 499 clearAllBlacklisted: function() { | |
| 500 chrome.send('clearMostVisitedURLsBlacklist', []); | |
| 501 hideNotification(); | |
| 502 }, | |
| 503 | |
| 504 updateDisplayMode: function() { | |
| 505 if (!this.dirty_) { | |
| 506 return; | |
| 507 } | |
| 508 updateSimpleSection('most-visited-section', Section.THUMB); | |
| 509 }, | |
| 510 | |
| 511 dirty_: false, | |
| 512 | |
| 513 invalidate: function() { | |
| 514 this.dirty_ = true; | |
| 515 }, | |
| 516 | |
| 517 layout: function() { | |
| 518 if (!this.dirty_) { | |
| 519 return; | |
| 520 } | |
| 521 var d0 = Date.now(); | |
| 522 | |
| 523 var mostVisitedElement = $('most-visited'); | |
| 524 var thumbnails = mostVisitedElement.children; | |
| 525 var hidden = !(shownSections & Section.THUMB); | |
| 526 | |
| 527 | |
| 528 // We set overflow to hidden so that the most visited element does not | |
| 529 // "leak" when we hide and show it. | |
| 530 if (hidden) { | |
| 531 mostVisitedElement.style.overflow = 'hidden'; | |
| 532 } | |
| 533 | |
| 534 applyMostVisitedRects(); | |
| 535 | |
| 536 // Only set overflow to visible if the element is shown. | |
| 537 if (!hidden) { | |
| 538 afterTransition(function() { | |
| 539 mostVisitedElement.style.overflow = ''; | |
| 540 }); | |
| 541 } | |
| 542 | |
| 543 this.dirty_ = false; | |
| 544 | |
| 545 logEvent('mostVisited.layout: ' + (Date.now() - d0)); | |
| 546 }, | |
| 547 | |
| 548 getRectByIndex: function(index) { | |
| 549 return getMostVisitedLayoutRects()[index]; | |
| 550 } | |
| 551 }; | |
| 552 | |
| 553 // Recently closed | 292 // Recently closed |
| 554 | 293 |
| 555 function layoutRecentlyClosed() { | 294 function layoutRecentlyClosed() { |
| 556 var recentShown = shownSections & Section.RECENT; | 295 var recentShown = shownSections & Section.RECENT; |
| 557 updateSimpleSection('recently-closed', Section.RECENT); | 296 updateSimpleSection('recently-closed', Section.RECENT); |
| 558 | 297 |
| 559 if (recentShown) { | 298 if (recentShown) { |
| 560 var recentElement = $('recently-closed'); | 299 var recentElement = $('recently-closed'); |
| 561 var style = recentElement.style; | 300 var style = recentElement.style; |
| 562 // We cannot use clientWidth here since the width has a transition. | 301 // We cannot use clientWidth here since the width has a transition. |
| (...skipping 259 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 822 } | 561 } |
| 823 | 562 |
| 824 function showFirstRunNotification() { | 563 function showFirstRunNotification() { |
| 825 showNotification(localStrings.getString('firstrunnotification'), | 564 showNotification(localStrings.getString('firstrunnotification'), |
| 826 localStrings.getString('closefirstrunnotification'), | 565 localStrings.getString('closefirstrunnotification'), |
| 827 null, 30000); | 566 null, 30000); |
| 828 var notificationElement = $('notification'); | 567 var notificationElement = $('notification'); |
| 829 addClass(notification, 'first-run'); | 568 addClass(notification, 'first-run'); |
| 830 } | 569 } |
| 831 | 570 |
| 832 | |
| 833 /** | 571 /** |
| 834 * This handles the option menu. | 572 * This handles the option menu. |
| 835 * @param {Element} button The button element. | 573 * @param {Element} button The button element. |
| 836 * @param {Element} menu The menu element. | 574 * @param {Element} menu The menu element. |
| 837 * @constructor | 575 * @constructor |
| 838 */ | 576 */ |
| 839 function OptionMenu(button, menu) { | 577 function OptionMenu(button, menu) { |
| 840 this.button = button; | 578 this.button = button; |
| 841 this.menu = menu; | 579 this.menu = menu; |
| 842 this.button.onmousedown = bind(this.handleMouseDown, this); | 580 this.button.onmousedown = bind(this.handleMouseDown, this); |
| (...skipping 178 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1021 showSection(section); | 759 showSection(section); |
| 1022 saveShownSections(); | 760 saveShownSections(); |
| 1023 }, | 761 }, |
| 1024 'hide': function(item) { | 762 'hide': function(item) { |
| 1025 var section = Section[item.getAttribute('section')]; | 763 var section = Section[item.getAttribute('section')]; |
| 1026 hideSection(section); | 764 hideSection(section); |
| 1027 saveShownSections(); | 765 saveShownSections(); |
| 1028 } | 766 } |
| 1029 }; | 767 }; |
| 1030 | 768 |
| 1031 $('most-visited').addEventListener('click', function(e) { | |
| 1032 var target = e.target; | |
| 1033 if (hasClass(target, 'pin')) { | |
| 1034 mostVisited.togglePinned(mostVisited.getItem(target)); | |
| 1035 e.preventDefault(); | |
| 1036 } else if (hasClass(target, 'remove')) { | |
| 1037 mostVisited.blacklist(mostVisited.getItem(target)); | |
| 1038 e.preventDefault(); | |
| 1039 } | |
| 1040 }); | |
| 1041 | |
| 1042 // Allow blacklisting most visited site using the keyboard. | |
| 1043 $('most-visited').addEventListener('keydown', function(e) { | |
| 1044 if (!IS_MAC && e.keyCode == 46 || // Del | |
| 1045 IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace | |
| 1046 mostVisited.blacklist(e.target); | |
| 1047 } | |
| 1048 }); | |
| 1049 | |
| 1050 $('main').addEventListener('click', function(e) { | 769 $('main').addEventListener('click', function(e) { |
| 1051 if (e.target.tagName == 'H2') { | 770 if (e.target.tagName == 'H2') { |
| 1052 var p = e.target.parentNode; | 771 var p = e.target.parentNode; |
| 1053 var section = p.getAttribute('section'); | 772 var section = p.getAttribute('section'); |
| 1054 if (section) { | 773 if (section) { |
| 1055 if (shownSections & Section[section]) | 774 if (shownSections & Section[section]) |
| 1056 hideSection(Section[section]); | 775 hideSection(Section[section]); |
| 1057 else | 776 else |
| 1058 showSection(Section[section]); | 777 showSection(Section[section]); |
| 1059 saveShownSections(); | 778 saveShownSections(); |
| (...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1233 // depends on the value of the attribue 'syncispresent' which the backend sets | 952 // depends on the value of the attribue 'syncispresent' which the backend sets |
| 1234 // to indicate if there is code in the backend which is capable of processing | 953 // to indicate if there is code in the backend which is capable of processing |
| 1235 // this message. This attribute is loaded by the JSTemplate and therefore we | 954 // this message. This attribute is loaded by the JSTemplate and therefore we |
| 1236 // must make sure we check the attribute after the DOM is loaded. | 955 // must make sure we check the attribute after the DOM is loaded. |
| 1237 document.addEventListener('DOMContentLoaded', | 956 document.addEventListener('DOMContentLoaded', |
| 1238 callGetSyncMessageIfSyncIsPresent); | 957 callGetSyncMessageIfSyncIsPresent); |
| 1239 | 958 |
| 1240 // Set up links and text-decoration for promotional message. | 959 // Set up links and text-decoration for promotional message. |
| 1241 document.addEventListener('DOMContentLoaded', setUpPromoMessage); | 960 document.addEventListener('DOMContentLoaded', setUpPromoMessage); |
| 1242 | 961 |
| 1243 // Work around for http://crbug.com/25329 | |
| 1244 function ensureSmallGridCorrect() { | |
| 1245 if (wasSmallGrid != useSmallGrid()) { | |
| 1246 applyMostVisitedRects(); | |
| 1247 } | |
| 1248 } | |
| 1249 document.addEventListener('DOMContentLoaded', ensureSmallGridCorrect); | |
| 1250 | |
| 1251 /** | 962 /** |
| 1252 * The sync code is not yet built by default on all platforms so we have to | 963 * The sync code is not yet built by default on all platforms so we have to |
| 1253 * make sure we don't send the initial sync message to the backend unless the | 964 * make sure we don't send the initial sync message to the backend unless the |
| 1254 * backend told us that the sync code is present. | 965 * backend told us that the sync code is present. |
| 1255 */ | 966 */ |
| 1256 function callGetSyncMessageIfSyncIsPresent() { | 967 function callGetSyncMessageIfSyncIsPresent() { |
| 1257 if (document.documentElement.getAttribute('syncispresent') == 'true') { | 968 if (document.documentElement.getAttribute('syncispresent') == 'true') { |
| 1258 chrome.send('GetSyncMessage'); | 969 chrome.send('GetSyncMessage'); |
| 1259 } | 970 } |
| 1260 } | 971 } |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1294 }); | 1005 }); |
| 1295 if (el && el.xtitle != el.title) { | 1006 if (el && el.xtitle != el.title) { |
| 1296 if (el.scrollWidth > el.clientWidth) { | 1007 if (el.scrollWidth > el.clientWidth) { |
| 1297 el.title = el.xtitle; | 1008 el.title = el.xtitle; |
| 1298 } else { | 1009 } else { |
| 1299 el.title = ''; | 1010 el.title = ''; |
| 1300 } | 1011 } |
| 1301 } | 1012 } |
| 1302 }); | 1013 }); |
| 1303 | 1014 |
| 1304 // DnD | |
| 1305 | |
| 1306 var dnd = { | |
| 1307 currentOverItem_: null, | |
| 1308 get currentOverItem() { | |
| 1309 return this.currentOverItem_; | |
| 1310 }, | |
| 1311 set currentOverItem(item) { | |
| 1312 var style; | |
| 1313 if (item != this.currentOverItem_) { | |
| 1314 if (this.currentOverItem_) { | |
| 1315 style = this.currentOverItem_.firstElementChild.style; | |
| 1316 style.left = style.top = ''; | |
| 1317 } | |
| 1318 this.currentOverItem_ = item; | |
| 1319 | |
| 1320 if (item) { | |
| 1321 // Make the drag over item move 15px towards the source. The movement is | |
| 1322 // done by only moving the edit-mode-border (as in the mocks) and it is | |
| 1323 // done with relative positioning so that the movement does not change | |
| 1324 // the drop target. | |
| 1325 var dragIndex = mostVisited.getThumbnailIndex(this.dragItem); | |
| 1326 var overIndex = mostVisited.getThumbnailIndex(item); | |
| 1327 if (dragIndex == -1 || overIndex == -1) { | |
| 1328 return; | |
| 1329 } | |
| 1330 | |
| 1331 var dragRect = mostVisited.getRectByIndex(dragIndex); | |
| 1332 var overRect = mostVisited.getRectByIndex(overIndex); | |
| 1333 | |
| 1334 var x = dragRect.left - overRect.left; | |
| 1335 var y = dragRect.top - overRect.top; | |
| 1336 var z = Math.sqrt(x * x + y * y); | |
| 1337 var z2 = 15; | |
| 1338 var x2 = x * z2 / z; | |
| 1339 var y2 = y * z2 / z; | |
| 1340 | |
| 1341 style = this.currentOverItem_.firstElementChild.style; | |
| 1342 style.left = x2 + 'px'; | |
| 1343 style.top = y2 + 'px'; | |
| 1344 } | |
| 1345 } | |
| 1346 }, | |
| 1347 dragItem: null, | |
| 1348 startX: 0, | |
| 1349 startY: 0, | |
| 1350 startScreenX: 0, | |
| 1351 startScreenY: 0, | |
| 1352 dragEndTimer: null, | |
| 1353 | |
| 1354 handleDragStart: function(e) { | |
| 1355 var thumbnail = mostVisited.getItem(e.target); | |
| 1356 if (thumbnail) { | |
| 1357 // Don't set data since HTML5 does not allow setting the name for | |
| 1358 // url-list. Instead, we just rely on the dragging of link behavior. | |
| 1359 this.dragItem = thumbnail; | |
| 1360 addClass(this.dragItem, 'dragging'); | |
| 1361 this.dragItem.style.zIndex = 2; | |
| 1362 e.dataTransfer.effectAllowed = 'copyLinkMove'; | |
| 1363 } | |
| 1364 }, | |
| 1365 | |
| 1366 handleDragEnter: function(e) { | |
| 1367 if (this.canDropOnElement(this.currentOverItem)) { | |
| 1368 e.preventDefault(); | |
| 1369 } | |
| 1370 }, | |
| 1371 | |
| 1372 handleDragOver: function(e) { | |
| 1373 var item = mostVisited.getItem(e.target); | |
| 1374 this.currentOverItem = item; | |
| 1375 if (this.canDropOnElement(item)) { | |
| 1376 e.preventDefault(); | |
| 1377 e.dataTransfer.dropEffect = 'move'; | |
| 1378 } | |
| 1379 }, | |
| 1380 | |
| 1381 handleDragLeave: function(e) { | |
| 1382 var item = mostVisited.getItem(e.target); | |
| 1383 if (item) { | |
| 1384 e.preventDefault(); | |
| 1385 } | |
| 1386 | |
| 1387 this.currentOverItem = null; | |
| 1388 }, | |
| 1389 | |
| 1390 handleDrop: function(e) { | |
| 1391 var dropTarget = mostVisited.getItem(e.target); | |
| 1392 if (this.canDropOnElement(dropTarget)) { | |
| 1393 dropTarget.style.zIndex = 1; | |
| 1394 mostVisited.swapPosition(this.dragItem, dropTarget); | |
| 1395 // The timeout below is to allow WebKit to see that we turned off | |
| 1396 // pointer-event before moving the thumbnails so that we can get out of | |
| 1397 // hover mode. | |
| 1398 window.setTimeout(function() { | |
| 1399 mostVisited.invalidate(); | |
| 1400 mostVisited.layout(); | |
| 1401 }, 10); | |
| 1402 e.preventDefault(); | |
| 1403 if (this.dragEndTimer) { | |
| 1404 window.clearTimeout(this.dragEndTimer); | |
| 1405 this.dragEndTimer = null; | |
| 1406 } | |
| 1407 afterTransition(function() { | |
| 1408 dropTarget.style.zIndex = ''; | |
| 1409 }); | |
| 1410 } | |
| 1411 }, | |
| 1412 | |
| 1413 handleDragEnd: function(e) { | |
| 1414 var dragItem = this.dragItem; | |
| 1415 if (dragItem) { | |
| 1416 dragItem.style.pointerEvents = ''; | |
| 1417 removeClass(dragItem, 'dragging'); | |
| 1418 | |
| 1419 afterTransition(function() { | |
| 1420 // Delay resetting zIndex to let the animation finish. | |
| 1421 dragItem.style.zIndex = ''; | |
| 1422 // Same for overflow. | |
| 1423 dragItem.parentNode.style.overflow = ''; | |
| 1424 }); | |
| 1425 | |
| 1426 mostVisited.invalidate(); | |
| 1427 mostVisited.layout(); | |
| 1428 this.dragItem = null; | |
| 1429 } | |
| 1430 }, | |
| 1431 | |
| 1432 handleDrag: function(e) { | |
| 1433 // Moves the drag item making sure that it is not displayed outside the | |
| 1434 // browser viewport. | |
| 1435 var item = mostVisited.getItem(e.target); | |
| 1436 var rect = document.querySelector('#most-visited').getBoundingClientRect(); | |
| 1437 item.style.pointerEvents = 'none'; | |
| 1438 | |
| 1439 var x = this.startX + e.screenX - this.startScreenX; | |
| 1440 var y = this.startY + e.screenY - this.startScreenY; | |
| 1441 | |
| 1442 // The position of the item is relative to #most-visited so we need to | |
| 1443 // subtract that when calculating the allowed position. | |
| 1444 x = Math.max(x, -rect.left); | |
| 1445 x = Math.min(x, document.body.clientWidth - rect.left - item.offsetWidth - | |
| 1446 2); | |
| 1447 // The shadow is 2px | |
| 1448 y = Math.max(-rect.top, y); | |
| 1449 y = Math.min(y, document.body.clientHeight - rect.top - item.offsetHeight - | |
| 1450 2); | |
| 1451 | |
| 1452 // Override right in case of RTL. | |
| 1453 item.style.right = 'auto'; | |
| 1454 item.style.left = x + 'px'; | |
| 1455 item.style.top = y + 'px'; | |
| 1456 item.style.zIndex = 2; | |
| 1457 }, | |
| 1458 | |
| 1459 // We listen to mousedown to get the relative position of the cursor for dnd. | |
| 1460 handleMouseDown: function(e) { | |
| 1461 var item = mostVisited.getItem(e.target); | |
| 1462 if (item) { | |
| 1463 this.startX = item.offsetLeft; | |
| 1464 this.startY = item.offsetTop; | |
| 1465 this.startScreenX = e.screenX; | |
| 1466 this.startScreenY = e.screenY; | |
| 1467 | |
| 1468 // We don't want to focus the item on mousedown. However, to prevent focus | |
| 1469 // one has to call preventDefault but this also prevents the drag and drop | |
| 1470 // (sigh) so we only prevent it when the user is not doing a left mouse | |
| 1471 // button drag. | |
| 1472 if (e.button != 0) // LEFT | |
| 1473 e.preventDefault(); | |
| 1474 } | |
| 1475 }, | |
| 1476 | |
| 1477 canDropOnElement: function(el) { | |
| 1478 return this.dragItem && el && hasClass(el, 'thumbnail-container') && | |
| 1479 !hasClass(el, 'filler'); | |
| 1480 }, | |
| 1481 | |
| 1482 init: function() { | |
| 1483 var el = $('most-visited'); | |
| 1484 el.addEventListener('dragstart', bind(this.handleDragStart, this)); | |
| 1485 el.addEventListener('dragenter', bind(this.handleDragEnter, this)); | |
| 1486 el.addEventListener('dragover', bind(this.handleDragOver, this)); | |
| 1487 el.addEventListener('dragleave', bind(this.handleDragLeave, this)); | |
| 1488 el.addEventListener('drop', bind(this.handleDrop, this)); | |
| 1489 el.addEventListener('dragend', bind(this.handleDragEnd, this)); | |
| 1490 el.addEventListener('drag', bind(this.handleDrag, this)); | |
| 1491 el.addEventListener('mousedown', bind(this.handleMouseDown, this)); | |
| 1492 } | |
| 1493 }; | |
| 1494 | |
| 1495 dnd.init(); | |
| 1496 | |
| 1497 /** | |
| 1498 * Whitelist of tag names allowed in parseHtmlSubset. | |
| 1499 * @type {[string]} | |
| 1500 */ | |
| 1501 var allowedTags = ['A', 'B', 'STRONG']; | |
| 1502 | |
| 1503 /** | |
| 1504 * Parse a very small subset of HTML. | |
| 1505 * @param {string} s The string to parse. | |
| 1506 * @throws {Error} In case of non supported markup. | |
| 1507 * @return {DocumentFragment} A document fragment containing the DOM tree. | |
| 1508 */ | |
| 1509 var allowedAttributes = { | |
| 1510 'href': function(node, value) { | |
| 1511 // Only allow a[href] starting with http:// and https:// | |
| 1512 return node.tagName == 'A' && (value.indexOf('http://') == 0 || | |
| 1513 value.indexOf('https://') == 0); | |
| 1514 }, | |
| 1515 'target': function(node, value) { | |
| 1516 // Allow a[target] but reset the value to "". | |
| 1517 if (node.tagName != 'A') | |
| 1518 return false; | |
| 1519 node.setAttribute('target', ''); | |
| 1520 return true; | |
| 1521 } | |
| 1522 } | |
| 1523 | |
| 1524 /** | |
| 1525 * Parse a very small subset of HTML. This ensures that insecure HTML / | |
| 1526 * javascript cannot be injected into the new tab page. | |
| 1527 * @param {string} s The string to parse. | |
| 1528 * @throws {Error} In case of non supported markup. | |
| 1529 * @return {DocumentFragment} A document fragment containing the DOM tree. | |
| 1530 */ | |
| 1531 function parseHtmlSubset(s) { | |
| 1532 function walk(n, f) { | |
| 1533 f(n); | |
| 1534 for (var i = 0; i < n.childNodes.length; i++) { | |
| 1535 walk(n.childNodes[i], f); | |
| 1536 } | |
| 1537 } | |
| 1538 | |
| 1539 function assertElement(node) { | |
| 1540 if (allowedTags.indexOf(node.tagName) == -1) | |
| 1541 throw Error(node.tagName + ' is not supported'); | |
| 1542 } | |
| 1543 | |
| 1544 function assertAttribute(attrNode, node) { | |
| 1545 var n = attrNode.nodeName; | |
| 1546 var v = attrNode.nodeValue; | |
| 1547 if (!allowedAttributes.hasOwnProperty(n) || !allowedAttributes[n](node, v)) | |
| 1548 throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported'); | |
| 1549 } | |
| 1550 | |
| 1551 var r = document.createRange(); | |
| 1552 r.selectNode(document.body); | |
| 1553 // This does not execute any scripts. | |
| 1554 var df = r.createContextualFragment(s); | |
| 1555 walk(df, function(node) { | |
| 1556 switch (node.nodeType) { | |
| 1557 case Node.ELEMENT_NODE: | |
| 1558 assertElement(node); | |
| 1559 var attrs = node.attributes; | |
| 1560 for (var i = 0; i < attrs.length; i++) { | |
| 1561 assertAttribute(attrs[i], node); | |
| 1562 } | |
| 1563 break; | |
| 1564 | |
| 1565 case Node.COMMENT_NODE: | |
| 1566 case Node.DOCUMENT_FRAGMENT_NODE: | |
| 1567 case Node.TEXT_NODE: | |
| 1568 break; | |
| 1569 | |
| 1570 default: | |
| 1571 throw Error('Node type ' + node.nodeType + ' is not supported'); | |
| 1572 } | |
| 1573 }); | |
| 1574 return df; | |
| 1575 } | |
| 1576 | |
| 1577 /** | 1015 /** |
| 1578 * Makes links and buttons support a different underline color. | 1016 * Makes links and buttons support a different underline color. |
| 1579 * @param {Node} node The node to search for links and buttons in. | 1017 * @param {Node} node The node to search for links and buttons in. |
| 1580 */ | 1018 */ |
| 1581 function fixLinkUnderlines(node) { | 1019 function fixLinkUnderlines(node) { |
| 1582 var elements = node.querySelectorAll('a,button'); | 1020 var elements = node.querySelectorAll('a,button'); |
| 1583 Array.prototype.forEach.call(elements, fixLinkUnderline); | 1021 Array.prototype.forEach.call(elements, fixLinkUnderline); |
| 1584 } | 1022 } |
| 1585 | 1023 |
| 1586 /** | 1024 /** |
| (...skipping 19 matching lines...) Expand all Loading... |
| 1606 }; | 1044 }; |
| 1607 | 1045 |
| 1608 // Set bookmark sync button to start bookmark sync process on click; also set | 1046 // Set bookmark sync button to start bookmark sync process on click; also set |
| 1609 // link underline colors correctly. | 1047 // link underline colors correctly. |
| 1610 function setUpPromoMessage() { | 1048 function setUpPromoMessage() { |
| 1611 var syncButton = document.querySelector('#promo-message button'); | 1049 var syncButton = document.querySelector('#promo-message button'); |
| 1612 syncButton.className = 'sync-button link'; | 1050 syncButton.className = 'sync-button link'; |
| 1613 syncButton.onclick = syncSectionLinkClicked; | 1051 syncButton.onclick = syncSectionLinkClicked; |
| 1614 fixLinkUnderlines($('promo-message')); | 1052 fixLinkUnderlines($('promo-message')); |
| 1615 } | 1053 } |
| OLD | NEW |