| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011 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. | |
| 4 | |
| 5 // To avoid creating tons of unnecessary nodes. We assume we cannot fit more | |
| 6 // than this many items in the miniview. | |
| 7 var MAX_MINIVIEW_ITEMS = 15; | |
| 8 | |
| 9 // Extra spacing at the top of the layout. | |
| 10 var LAYOUT_SPACING_TOP = 25; | |
| 11 | |
| 12 // The visible height of the expanded maxiview. | |
| 13 var maxiviewVisibleHeight = 0; | |
| 14 | |
| 15 var APP_LAUNCH = { | |
| 16 // The histogram buckets (keep in sync with extension_constants.h). | |
| 17 NTP_APPS_MAXIMIZED: 0, | |
| 18 NTP_APPS_COLLAPSED: 1, | |
| 19 NTP_APPS_MENU: 2, | |
| 20 NTP_MOST_VISITED: 3, | |
| 21 NTP_RECENTLY_CLOSED: 4, | |
| 22 NTP_APP_RE_ENABLE: 16 | |
| 23 }; | |
| 24 | |
| 25 var APP_LAUNCH_URL = { | |
| 26 // The URL prefix for pings that record app launches by URL. | |
| 27 PING_BY_URL: 'record-app-launch-by-url', | |
| 28 | |
| 29 // The URL prefix for pings that record app launches by ID. | |
| 30 PING_BY_ID: 'record-app-launch-by-id', | |
| 31 | |
| 32 // The URL prefix used by the webstore link 'ping' attributes. | |
| 33 PING_WEBSTORE: 'record-webstore-launch' | |
| 34 }; | |
| 35 | |
| 36 function getAppPingUrl(prefix, data, bucket) { | |
| 37 return [APP_LAUNCH_URL[prefix], | |
| 38 encodeURIComponent(data), | |
| 39 APP_LAUNCH[bucket]].join('+'); | |
| 40 } | |
| 41 | |
| 42 function getSectionCloseButton(sectionId) { | |
| 43 return document.querySelector('#' + sectionId + ' .section-close-button'); | |
| 44 } | |
| 45 | |
| 46 function getSectionMenuButton(sectionId) { | |
| 47 return $(sectionId + '-button'); | |
| 48 } | |
| 49 | |
| 50 function getSectionMenuButtonTextId(sectionId) { | |
| 51 return sectionId.replace(/-/g, ''); | |
| 52 } | |
| 53 | |
| 54 function setSectionMenuMode(sectionId, section, menuModeEnabled, menuModeMask) { | |
| 55 var el = $(sectionId); | |
| 56 if (!menuModeEnabled) { | |
| 57 // Because sections are collapsed when they are in menu mode, it is not | |
| 58 // necessary to restore the maxiview here. It will happen if the section | |
| 59 // header is clicked. | |
| 60 // TODO(aa): Sections should maintain their collapse state when minimized. | |
| 61 el.classList.remove('menu'); | |
| 62 shownSections &= ~menuModeMask; | |
| 63 } else { | |
| 64 if (section) { | |
| 65 hideSection(section); // To hide the maxiview. | |
| 66 } | |
| 67 el.classList.add('menu'); | |
| 68 shownSections |= menuModeMask; | |
| 69 } | |
| 70 layoutSections(); | |
| 71 } | |
| 72 | |
| 73 function clearClosedMenu(menu) { | |
| 74 menu.innerHTML = ''; | |
| 75 } | |
| 76 | |
| 77 function addClosedMenuEntryWithLink(menu, a) { | |
| 78 var span = document.createElement('span'); | |
| 79 a.className += ' item menuitem'; | |
| 80 span.appendChild(a); | |
| 81 menu.appendChild(span); | |
| 82 } | |
| 83 | |
| 84 function addClosedMenuEntry(menu, url, title, imageUrl, opt_pingUrl) { | |
| 85 var a = document.createElement('a'); | |
| 86 a.href = url; | |
| 87 a.textContent = title; | |
| 88 a.style.backgroundImage = 'url(' + imageUrl + ')'; | |
| 89 if (opt_pingUrl) | |
| 90 a.ping = opt_pingUrl; | |
| 91 addClosedMenuEntryWithLink(menu, a); | |
| 92 } | |
| 93 | |
| 94 function addClosedMenuFooter(menu, sectionId, mask, opt_section) { | |
| 95 menu.appendChild(document.createElement('hr')); | |
| 96 | |
| 97 var span = document.createElement('span'); | |
| 98 var a = span.appendChild(document.createElement('a')); | |
| 99 a.href = ''; | |
| 100 if (cr.isChromeOS) { | |
| 101 a.textContent = localStrings.getString('expandMenu'); | |
| 102 } else { | |
| 103 a.textContent = | |
| 104 localStrings.getString(getSectionMenuButtonTextId(sectionId)); | |
| 105 } | |
| 106 a.className = 'item'; | |
| 107 a.addEventListener( | |
| 108 'click', | |
| 109 function(e) { | |
| 110 getSectionMenuButton(sectionId).hideMenu(); | |
| 111 e.preventDefault(); | |
| 112 setSectionMenuMode(sectionId, opt_section, false, mask); | |
| 113 shownSections &= ~mask; | |
| 114 saveShownSections(); | |
| 115 }); | |
| 116 menu.appendChild(span); | |
| 117 } | |
| 118 | |
| 119 function initializeSection(sectionId, mask, opt_section) { | |
| 120 var button = getSectionCloseButton(sectionId); | |
| 121 button.addEventListener( | |
| 122 'click', | |
| 123 function() { | |
| 124 setSectionMenuMode(sectionId, opt_section, true, mask); | |
| 125 saveShownSections(); | |
| 126 }); | |
| 127 } | |
| 128 | |
| 129 function updateSimpleSection(id, section) { | |
| 130 var elm = $(id); | |
| 131 var maxiview = getSectionMaxiview(elm); | |
| 132 var miniview = getSectionMiniview(elm); | |
| 133 if (shownSections & section) { | |
| 134 // The section is expanded, so the maxiview should be opaque (visible) and | |
| 135 // the miniview should be hidden. | |
| 136 elm.classList.remove('collapsed'); | |
| 137 if (maxiview) { | |
| 138 maxiview.classList.remove('collapsed'); | |
| 139 maxiview.classList.add('opaque'); | |
| 140 } | |
| 141 if (miniview) | |
| 142 miniview.classList.remove('opaque'); | |
| 143 } else { | |
| 144 // The section is collapsed, so the maxiview should be hidden and the | |
| 145 // miniview should be opaque. | |
| 146 elm.classList.add('collapsed'); | |
| 147 if (maxiview) { | |
| 148 maxiview.classList.add('collapsed'); | |
| 149 maxiview.classList.remove('opaque'); | |
| 150 } | |
| 151 if (miniview) | |
| 152 miniview.classList.add('opaque'); | |
| 153 } | |
| 154 } | |
| 155 | |
| 156 var sessionItems = []; | |
| 157 | |
| 158 function foreignSessions(data) { | |
| 159 logEvent('received foreign sessions'); | |
| 160 // We need to store the foreign sessions so we can update the layout on a | |
| 161 // resize. | |
| 162 sessionItems = data; | |
| 163 renderForeignSessions(); | |
| 164 layoutSections(); | |
| 165 } | |
| 166 | |
| 167 function renderForeignSessions() { | |
| 168 // Remove all existing items and create new items. | |
| 169 var sessionElement = $('foreign-sessions'); | |
| 170 var parentSessionElement = sessionElement.lastElementChild; | |
| 171 parentSessionElement.textContent = ''; | |
| 172 | |
| 173 // For each client, create entries and append the lists together. | |
| 174 sessionItems.forEach(function(item, i) { | |
| 175 // TODO(zea): Get real client names. See crbug/59672. | |
| 176 var name = 'Client ' + i; | |
| 177 parentSessionElement.appendChild(createForeignSession(item, name)); | |
| 178 }); | |
| 179 | |
| 180 layoutForeignSessions(); | |
| 181 } | |
| 182 | |
| 183 function layoutForeignSessions() { | |
| 184 var sessionElement = $('foreign-sessions'); | |
| 185 // We cannot use clientWidth here since the width has a transition. | |
| 186 var availWidth = useSmallGrid() ? 692 : 920; | |
| 187 var parentSessEl = sessionElement.lastElementChild; | |
| 188 | |
| 189 if (parentSessEl.hasChildNodes()) { | |
| 190 sessionElement.classList.remove('disabled'); | |
| 191 sessionElement.classList.remove('opaque'); | |
| 192 } else { | |
| 193 sessionElement.classList.add('disabled'); | |
| 194 sessionElement.classList.add('opaque'); | |
| 195 } | |
| 196 } | |
| 197 | |
| 198 function createForeignSession(client, name) { | |
| 199 // Vertically stack the windows in a client. | |
| 200 var stack = document.createElement('div'); | |
| 201 stack.className = 'foreign-session-client item link'; | |
| 202 stack.textContent = name; | |
| 203 stack.sessionTag = client[0].sessionTag; | |
| 204 | |
| 205 client.forEach(function(win, i) { | |
| 206 // Create a window entry. | |
| 207 var winSpan = document.createElement('span'); | |
| 208 var winEl = document.createElement('p'); | |
| 209 winEl.className = 'item link window'; | |
| 210 winEl.tabItems = win.tabs; | |
| 211 winEl.tabIndex = 0; | |
| 212 winEl.textContent = formatTabsText(win.tabs.length); | |
| 213 winEl.xtitle = win.title; | |
| 214 winEl.sessionTag = win.sessionTag; | |
| 215 winEl.winNum = i; | |
| 216 winEl.addEventListener('click', maybeOpenForeignWindow); | |
| 217 winEl.addEventListener('keydown', | |
| 218 handleIfEnterKey(maybeOpenForeignWindow)); | |
| 219 winSpan.appendChild(winEl); | |
| 220 | |
| 221 // Sort tabs by MRU order | |
| 222 win.tabs.sort(function(a, b) { | |
| 223 return a.timestamp < b.timestamp; | |
| 224 }); | |
| 225 | |
| 226 // Create individual tab information. | |
| 227 win.tabs.forEach(function(data) { | |
| 228 var tabEl = document.createElement('a'); | |
| 229 tabEl.className = 'item link tab'; | |
| 230 tabEl.href = data.timestamp; | |
| 231 tabEl.style.backgroundImage = url('chrome://favicon/' + data.url); | |
| 232 tabEl.dir = data.direction; | |
| 233 tabEl.textContent = data.title; | |
| 234 tabEl.sessionTag = win.sessionTag; | |
| 235 tabEl.winNum = i; | |
| 236 tabEl.sessionId = data.sessionId; | |
| 237 tabEl.addEventListener('click', maybeOpenForeignTab); | |
| 238 tabEl.addEventListener('keydown', | |
| 239 handleIfEnterKey(maybeOpenForeignTab)); | |
| 240 | |
| 241 winSpan.appendChild(tabEl); | |
| 242 }); | |
| 243 | |
| 244 // Append the window. | |
| 245 stack.appendChild(winSpan); | |
| 246 }); | |
| 247 return stack; | |
| 248 } | |
| 249 | |
| 250 var recentItems = []; | |
| 251 | |
| 252 function recentlyClosedTabs(data) { | |
| 253 logEvent('received recently closed tabs'); | |
| 254 // We need to store the recent items so we can update the layout on a resize. | |
| 255 recentItems = data; | |
| 256 renderRecentlyClosed(); | |
| 257 layoutSections(); | |
| 258 } | |
| 259 | |
| 260 function renderRecentlyClosed() { | |
| 261 // Remove all existing items and create new items. | |
| 262 var recentElement = $('recently-closed'); | |
| 263 var parentEl = recentElement.lastElementChild; | |
| 264 parentEl.textContent = ''; | |
| 265 var recentMenu = $('recently-closed-menu'); | |
| 266 clearClosedMenu(recentMenu); | |
| 267 | |
| 268 recentItems.forEach(function(item) { | |
| 269 parentEl.appendChild(createRecentItem(item)); | |
| 270 addRecentMenuItem(recentMenu, item); | |
| 271 }); | |
| 272 addClosedMenuFooter(recentMenu, 'recently-closed', MENU_RECENT); | |
| 273 | |
| 274 layoutRecentlyClosed(); | |
| 275 } | |
| 276 | |
| 277 function createRecentItem(data) { | |
| 278 var isWindow = data.type == 'window'; | |
| 279 var el; | |
| 280 if (isWindow) { | |
| 281 el = document.createElement('span'); | |
| 282 el.className = 'item link window'; | |
| 283 el.tabItems = data.tabs; | |
| 284 el.tabIndex = 0; | |
| 285 el.textContent = formatTabsText(data.tabs.length); | |
| 286 } else { | |
| 287 el = document.createElement('a'); | |
| 288 el.className = 'item'; | |
| 289 el.href = data.url; | |
| 290 el.ping = getAppPingUrl( | |
| 291 'PING_BY_URL', data.url, 'NTP_RECENTLY_CLOSED'); | |
| 292 el.style.backgroundImage = url('chrome://favicon/' + data.url); | |
| 293 el.dir = data.direction; | |
| 294 el.textContent = data.title; | |
| 295 } | |
| 296 el.sessionId = data.sessionId; | |
| 297 el.xtitle = data.title; | |
| 298 el.sessionTag = data.sessionTag; | |
| 299 var wrapperEl = document.createElement('span'); | |
| 300 wrapperEl.appendChild(el); | |
| 301 return wrapperEl; | |
| 302 } | |
| 303 | |
| 304 function addRecentMenuItem(menu, data) { | |
| 305 var isWindow = data.type == 'window'; | |
| 306 var a = document.createElement('a'); | |
| 307 if (isWindow) { | |
| 308 a.textContent = formatTabsText(data.tabs.length); | |
| 309 a.className = 'window'; // To get the icon from the CSS .window rule. | |
| 310 a.href = ''; // To make underline show up. | |
| 311 } else { | |
| 312 a.href = data.url; | |
| 313 a.ping = getAppPingUrl( | |
| 314 'PING_BY_URL', data.url, 'NTP_RECENTLY_CLOSED'); | |
| 315 a.style.backgroundImage = 'url(chrome://favicon/' + data.url + ')'; | |
| 316 a.textContent = data.title; | |
| 317 } | |
| 318 function clickHandler(e) { | |
| 319 chrome.send('reopenTab', [String(data.sessionId)]); | |
| 320 e.preventDefault(); | |
| 321 } | |
| 322 a.addEventListener('click', clickHandler); | |
| 323 addClosedMenuEntryWithLink(menu, a); | |
| 324 } | |
| 325 | |
| 326 function saveShownSections() { | |
| 327 chrome.send('setShownSections', [shownSections]); | |
| 328 } | |
| 329 | |
| 330 var LayoutMode = { | |
| 331 SMALL: 1, | |
| 332 NORMAL: 2 | |
| 333 }; | |
| 334 | |
| 335 var layoutMode = useSmallGrid() ? LayoutMode.SMALL : LayoutMode.NORMAL; | |
| 336 | |
| 337 function handleWindowResize() { | |
| 338 if (window.innerWidth < 10) { | |
| 339 // We're probably a background tab, so don't do anything. | |
| 340 return; | |
| 341 } | |
| 342 | |
| 343 // TODO(jstritar): Remove the small-layout class and revert back to the | |
| 344 // @media (max-width) directive once http://crbug.com/70930 is fixed. | |
| 345 var oldLayoutMode = layoutMode; | |
| 346 var b = useSmallGrid(); | |
| 347 if (b) { | |
| 348 layoutMode = LayoutMode.SMALL; | |
| 349 document.body.classList.add('small-layout'); | |
| 350 } else { | |
| 351 layoutMode = LayoutMode.NORMAL; | |
| 352 document.body.classList.remove('small-layout'); | |
| 353 } | |
| 354 | |
| 355 if (layoutMode != oldLayoutMode){ | |
| 356 mostVisited.useSmallGrid = b; | |
| 357 mostVisited.layout(); | |
| 358 apps.layout({force:true}); | |
| 359 renderRecentlyClosed(); | |
| 360 renderForeignSessions(); | |
| 361 updateAllMiniviewClippings(); | |
| 362 } | |
| 363 | |
| 364 layoutSections(); | |
| 365 } | |
| 366 | |
| 367 // Stores some information about each section necessary to layout. A new | |
| 368 // instance is constructed for each section on each layout. | |
| 369 function SectionLayoutInfo(section) { | |
| 370 this.section = section; | |
| 371 this.header = section.querySelector('h2'); | |
| 372 this.miniview = section.querySelector('.miniview'); | |
| 373 this.maxiview = getSectionMaxiview(section); | |
| 374 this.expanded = this.maxiview && !section.classList.contains('collapsed'); | |
| 375 this.fixedHeight = this.section.offsetHeight; | |
| 376 this.scrollingHeight = 0; | |
| 377 | |
| 378 if (this.expanded) | |
| 379 this.scrollingHeight = this.maxiview.offsetHeight; | |
| 380 } | |
| 381 | |
| 382 // Get all sections to be layed out. | |
| 383 SectionLayoutInfo.getAll = function() { | |
| 384 var sections = document.querySelectorAll( | |
| 385 '.section:not(.disabled):not(.menu)'); | |
| 386 var result = []; | |
| 387 for (var i = 0, section; section = sections[i]; i++) { | |
| 388 result.push(new SectionLayoutInfo(section)); | |
| 389 } | |
| 390 return result; | |
| 391 }; | |
| 392 | |
| 393 // Ensure the miniview sections don't have any clipped items. | |
| 394 function updateMiniviewClipping(miniview) { | |
| 395 var clipped = false; | |
| 396 for (var j = 0, item; item = miniview.children[j]; j++) { | |
| 397 item.style.display = ''; | |
| 398 if (clipped || | |
| 399 (item.offsetLeft + item.offsetWidth) > miniview.offsetWidth) { | |
| 400 item.style.display = 'none'; | |
| 401 clipped = true; | |
| 402 } else { | |
| 403 item.style.display = ''; | |
| 404 } | |
| 405 } | |
| 406 } | |
| 407 | |
| 408 // Ensure none of the miniviews have any clipped items. | |
| 409 function updateAllMiniviewClippings() { | |
| 410 var miniviews = document.querySelectorAll('.section.collapsed .miniview'); | |
| 411 for (var i = 0, miniview; miniview = miniviews[i]; i++) { | |
| 412 updateMiniviewClipping(miniview); | |
| 413 } | |
| 414 } | |
| 415 | |
| 416 // Returns whether or not vertical scrollbars are present. | |
| 417 function hasScrollBars() { | |
| 418 return window.innerHeight != document.body.clientHeight; | |
| 419 } | |
| 420 | |
| 421 // Enables scrollbars (they will only show up if needed). | |
| 422 function showScrollBars() { | |
| 423 document.body.classList.remove('noscroll'); | |
| 424 } | |
| 425 | |
| 426 // Hides all scrollbars. | |
| 427 function hideScrollBars() { | |
| 428 document.body.classList.add('noscroll'); | |
| 429 } | |
| 430 | |
| 431 // Returns whether or not the sections are currently animating due to a | |
| 432 // section transition. | |
| 433 function isAnimating() { | |
| 434 var de = document.documentElement; | |
| 435 return de.getAttribute('enable-section-animations') == 'true'; | |
| 436 } | |
| 437 | |
| 438 // Layout the sections in a modified accordion. The header and miniview, if | |
| 439 // visible are fixed within the viewport. If there is an expanded section, its | |
| 440 // it scrolls. | |
| 441 // | |
| 442 // ============================= | |
| 443 // | collapsed section | <- Any collapsed sections are fixed position. | |
| 444 // | and miniview | | |
| 445 // |---------------------------| | |
| 446 // | expanded section | | |
| 447 // | | <- There can be one expanded section and it | |
| 448 // | and maxiview | is absolutely positioned so that it can | |
| 449 // | | scroll "underneath" the fixed elements. | |
| 450 // | | | |
| 451 // |---------------------------| | |
| 452 // | another collapsed section | | |
| 453 // |---------------------------| | |
| 454 // | |
| 455 // We want the main frame scrollbar to be the one that scrolls the expanded | |
| 456 // region. To get this effect, we make the fixed elements position:fixed and the | |
| 457 // scrollable element position:absolute. We also artificially increase the | |
| 458 // height of the document so that it is possible to scroll down enough to | |
| 459 // display the end of the document, even with any fixed elements at the bottom | |
| 460 // of the viewport. | |
| 461 // | |
| 462 // There is a final twist: If the intrinsic height of the expanded section is | |
| 463 // less than the available height (because the window is tall), any collapsed | |
| 464 // sections sinch up and sit below the expanded section. This is so that we | |
| 465 // don't have a bunch of dead whitespace in the case of expanded sections that | |
| 466 // aren't very tall. | |
| 467 function layoutSections() { | |
| 468 // While transitioning sections, we only want scrollbars to appear if they're | |
| 469 // already present or the window is being resized (so there's no animation). | |
| 470 if (!hasScrollBars() && isAnimating()) | |
| 471 hideScrollBars(); | |
| 472 | |
| 473 var sections = SectionLayoutInfo.getAll(); | |
| 474 var expandedSection = null; | |
| 475 var headerHeight = LAYOUT_SPACING_TOP; | |
| 476 var footerHeight = 0; | |
| 477 | |
| 478 // Calculate the height of the fixed elements above the expanded section. Also | |
| 479 // take note of the expanded section, if there is one. | |
| 480 var i; | |
| 481 var section; | |
| 482 for (i = 0; section = sections[i]; i++) { | |
| 483 headerHeight += section.fixedHeight; | |
| 484 if (section.expanded) { | |
| 485 expandedSection = section; | |
| 486 i++; | |
| 487 break; | |
| 488 } | |
| 489 } | |
| 490 | |
| 491 // Include the height of the sync promo bar. | |
| 492 var sync_promo_height = $('sync-promo').offsetHeight; | |
| 493 headerHeight += sync_promo_height; | |
| 494 | |
| 495 // Calculate the height of the fixed elements below the expanded section, if | |
| 496 // any. | |
| 497 for (; section = sections[i]; i++) { | |
| 498 footerHeight += section.fixedHeight; | |
| 499 } | |
| 500 // Leave room for bottom bar if it's visible. | |
| 501 footerHeight += $('closed-sections-bar').offsetHeight; | |
| 502 | |
| 503 | |
| 504 // Determine the height to use for the expanded section. If there isn't enough | |
| 505 // space to show the expanded section completely, this will be the available | |
| 506 // height. Otherwise, we use the intrinsic height of the expanded section. | |
| 507 var expandedSectionHeight; | |
| 508 var expandedSectionIsClipped = false; | |
| 509 if (expandedSection) { | |
| 510 var flexHeight = window.innerHeight - headerHeight - footerHeight; | |
| 511 if (flexHeight < expandedSection.scrollingHeight) { | |
| 512 expandedSectionHeight = flexHeight; | |
| 513 | |
| 514 // Also, artificially expand the height of the document so that we can see | |
| 515 // the entire expanded section. | |
| 516 // | |
| 517 // TODO(aa): Where does this come from? It is the difference between what | |
| 518 // we set document.body.style.height to and what | |
| 519 // document.body.scrollHeight measures afterward. I expect them to be the | |
| 520 // same if document.body has no margins. | |
| 521 var fudge = 44; | |
| 522 document.body.style.height = | |
| 523 headerHeight + | |
| 524 expandedSection.scrollingHeight + | |
| 525 footerHeight + | |
| 526 fudge + | |
| 527 'px'; | |
| 528 expandedSectionIsClipped = true; | |
| 529 } else { | |
| 530 expandedSectionHeight = expandedSection.scrollingHeight; | |
| 531 document.body.style.height = ''; | |
| 532 } | |
| 533 } else { | |
| 534 // We only set the document height when a section is expanded. If | |
| 535 // all sections are collapsed, then get rid of the previous height. | |
| 536 document.body.style.height = ''; | |
| 537 } | |
| 538 | |
| 539 maxiviewVisibleHeight = expandedSectionHeight; | |
| 540 | |
| 541 // Now position all the elements. | |
| 542 var y = LAYOUT_SPACING_TOP + sync_promo_height; | |
| 543 for (i = 0, section; section = sections[i]; i++) { | |
| 544 section.section.style.top = y + 'px'; | |
| 545 y += section.fixedHeight; | |
| 546 | |
| 547 if (section.maxiview) { | |
| 548 if (section == expandedSection) { | |
| 549 section.maxiview.style.top = y + 'px'; | |
| 550 } else { | |
| 551 // The miniviews fade out gradually, so it may have height at this | |
| 552 // point. We position the maxiview as if the miniview was not displayed | |
| 553 // by subtracting off the miniview's total height (height + margin). | |
| 554 var miniviewFudge = 40; // miniview margin-bottom + margin-top | |
| 555 var miniviewHeight = section.miniview.offsetHeight + miniviewFudge; | |
| 556 section.maxiview.style.top = y - miniviewHeight + 'px'; | |
| 557 } | |
| 558 } | |
| 559 | |
| 560 if (section.maxiview && section == expandedSection) | |
| 561 updateMask( | |
| 562 section.maxiview, expandedSectionHeight, expandedSectionIsClipped); | |
| 563 | |
| 564 if (section == expandedSection) | |
| 565 y += expandedSectionHeight; | |
| 566 } | |
| 567 if (cr.isChromeOS) | |
| 568 $('closed-sections-bar').style.top = y + 'px'; | |
| 569 | |
| 570 // Position the notification container below the sync promo. | |
| 571 $('notification-container').style.top = sync_promo_height + 'px'; | |
| 572 | |
| 573 updateMenuSections(); | |
| 574 updateAttributionDisplay(y); | |
| 575 } | |
| 576 | |
| 577 function updateMask(maxiview, visibleHeightPx, isClipped) { | |
| 578 // If the section isn't actually clipped, then we don't want to use a mask at | |
| 579 // all, since enabling one turns off subpixel anti-aliasing. | |
| 580 if (!isClipped) { | |
| 581 maxiview.style.WebkitMaskImage = 'none'; | |
| 582 return; | |
| 583 } | |
| 584 | |
| 585 // We want to end up with 10px gradients at the top and bottom of | |
| 586 // visibleHeight, but webkit-mask only supports expression in terms of | |
| 587 // percentages. | |
| 588 | |
| 589 // We might not have enough room to do 10px gradients on each side. To get the | |
| 590 // right effect, we don't want to make the gradients smaller, but make them | |
| 591 // appear to mush into each other. | |
| 592 var gradientHeightPx = Math.min(10, Math.floor(visibleHeightPx / 2)); | |
| 593 var gradientDestination = 'rgba(0,0,0,' + (gradientHeightPx / 10) + ')'; | |
| 594 | |
| 595 var bottomSpacing = 15; | |
| 596 var first = parseFloat(maxiview.style.top) / window.innerHeight; | |
| 597 var second = first + gradientHeightPx / window.innerHeight; | |
| 598 var fourth = first + (visibleHeightPx - bottomSpacing) / window.innerHeight; | |
| 599 var third = fourth - gradientHeightPx / window.innerHeight; | |
| 600 | |
| 601 var gradientArguments = [ | |
| 602 'transparent', | |
| 603 getColorStopString(first, 'transparent'), | |
| 604 getColorStopString(second, gradientDestination), | |
| 605 getColorStopString(third, gradientDestination), | |
| 606 getColorStopString(fourth, 'transparent'), | |
| 607 'transparent' | |
| 608 ]; | |
| 609 | |
| 610 var gradient = '-webkit-linear-gradient(' + gradientArguments.join(',') + ')'; | |
| 611 maxiview.style.WebkitMaskImage = gradient; | |
| 612 } | |
| 613 | |
| 614 function getColorStopString(height, color) { | |
| 615 // TODO(arv): The CSS3 gradient syntax allows px units so we should simplify | |
| 616 // this to use pixels instead. | |
| 617 return color + ' ' + height * 100 + '%'; | |
| 618 } | |
| 619 | |
| 620 // Updates the visibility of the menu buttons for each section, based on | |
| 621 // whether they are currently enabled and in menu mode. | |
| 622 function updateMenuSections() { | |
| 623 var elms = document.getElementsByClassName('section'); | |
| 624 for (var i = 0, elm; elm = elms[i]; i++) { | |
| 625 var button = getSectionMenuButton(elm.id); | |
| 626 if (!button) | |
| 627 continue; | |
| 628 | |
| 629 if (!elm.classList.contains('disabled') && | |
| 630 elm.classList.contains('menu')) { | |
| 631 button.style.display = 'inline-block'; | |
| 632 } else { | |
| 633 button.style.display = 'none'; | |
| 634 } | |
| 635 } | |
| 636 } | |
| 637 | |
| 638 window.addEventListener('resize', handleWindowResize); | |
| 639 | |
| 640 var sectionToElementMap; | |
| 641 function getSectionElement(section) { | |
| 642 if (!sectionToElementMap) { | |
| 643 sectionToElementMap = {}; | |
| 644 for (var key in Section) { | |
| 645 sectionToElementMap[Section[key]] = | |
| 646 document.querySelector('.section[section=' + key + ']'); | |
| 647 } | |
| 648 } | |
| 649 return sectionToElementMap[section]; | |
| 650 } | |
| 651 | |
| 652 function getSectionMaxiview(section) { | |
| 653 return $(section.id + '-maxiview'); | |
| 654 } | |
| 655 | |
| 656 function getSectionMiniview(section) { | |
| 657 return section.querySelector('.miniview'); | |
| 658 } | |
| 659 | |
| 660 // You usually want to call |showOnlySection()| instead of this. | |
| 661 function showSection(section) { | |
| 662 if (!(section & shownSections)) { | |
| 663 shownSections |= section; | |
| 664 var el = getSectionElement(section); | |
| 665 if (el) { | |
| 666 el.classList.remove('collapsed'); | |
| 667 | |
| 668 var maxiview = getSectionMaxiview(el); | |
| 669 if (maxiview) { | |
| 670 maxiview.classList.remove('collapsing'); | |
| 671 maxiview.classList.remove('collapsed'); | |
| 672 // The opacity won't transition if you toggle the display property | |
| 673 // at the same time. To get a fade effect, we set the opacity | |
| 674 // asynchronously from another function, after the display is toggled. | |
| 675 // 1) 'collapsed' (display: none, opacity: 0) | |
| 676 // 2) none (display: block, opacity: 0) | |
| 677 // 3) 'opaque' (display: block, opacity: 1) | |
| 678 setTimeout(function () { | |
| 679 maxiview.classList.add('opaque'); | |
| 680 }, 0); | |
| 681 } | |
| 682 | |
| 683 var miniview = getSectionMiniview(el); | |
| 684 if (miniview) { | |
| 685 // The miniview is hidden immediately (no need to set this async). | |
| 686 miniview.classList.remove('opaque'); | |
| 687 } | |
| 688 } | |
| 689 | |
| 690 switch (section) { | |
| 691 case Section.THUMB: | |
| 692 mostVisited.visible = true; | |
| 693 mostVisited.layout(); | |
| 694 break; | |
| 695 case Section.APPS: | |
| 696 apps.visible = true; | |
| 697 apps.layout({disableAnimations:true}); | |
| 698 break; | |
| 699 } | |
| 700 } | |
| 701 } | |
| 702 | |
| 703 // Show this section and hide all other sections - at most one section can | |
| 704 // be open at one time. | |
| 705 function showOnlySection(section) { | |
| 706 for (var p in Section) { | |
| 707 if (p == section) | |
| 708 showSection(Section[p]); | |
| 709 else | |
| 710 hideSection(Section[p]); | |
| 711 } | |
| 712 } | |
| 713 | |
| 714 function hideSection(section) { | |
| 715 if (section & shownSections) { | |
| 716 shownSections &= ~section; | |
| 717 | |
| 718 switch (section) { | |
| 719 case Section.THUMB: | |
| 720 mostVisited.visible = false; | |
| 721 mostVisited.layout(); | |
| 722 break; | |
| 723 case Section.APPS: | |
| 724 apps.visible = false; | |
| 725 apps.layout(); | |
| 726 break; | |
| 727 } | |
| 728 | |
| 729 var el = getSectionElement(section); | |
| 730 if (el) { | |
| 731 el.classList.add('collapsed'); | |
| 732 | |
| 733 var maxiview = getSectionMaxiview(el); | |
| 734 if (maxiview) { | |
| 735 maxiview.classList.add((isDoneLoading() && isAnimating()) ? | |
| 736 'collapsing' : 'collapsed'); | |
| 737 maxiview.classList.remove('opaque'); | |
| 738 } | |
| 739 | |
| 740 var miniview = getSectionMiniview(el); | |
| 741 if (miniview) { | |
| 742 // We need to set this asynchronously to properly get the fade effect. | |
| 743 setTimeout(function() { | |
| 744 miniview.classList.add('opaque'); | |
| 745 }, 0); | |
| 746 updateMiniviewClipping(miniview); | |
| 747 } | |
| 748 } | |
| 749 } | |
| 750 } | |
| 751 | |
| 752 window.addEventListener('webkitTransitionEnd', function(e) { | |
| 753 if (e.target.classList.contains('collapsing')) { | |
| 754 e.target.classList.add('collapsed'); | |
| 755 e.target.classList.remove('collapsing'); | |
| 756 } | |
| 757 | |
| 758 if (e.target.classList.contains('maxiview') || | |
| 759 e.target.classList.contains('miniview')) { | |
| 760 document.documentElement.removeAttribute('enable-section-animations'); | |
| 761 showScrollBars(); | |
| 762 } | |
| 763 }); | |
| 764 | |
| 765 /** | |
| 766 * Callback when the shown sections changes in another NTP. | |
| 767 * @param {number} newShownSections Bitmask of the shown sections. | |
| 768 */ | |
| 769 function setShownSections(newShownSections) { | |
| 770 for (var key in Section) { | |
| 771 if (newShownSections & Section[key]) | |
| 772 showSection(Section[key]); | |
| 773 else | |
| 774 hideSection(Section[key]); | |
| 775 } | |
| 776 setSectionMenuMode('apps', Section.APPS, newShownSections & MENU_APPS, | |
| 777 MENU_APPS); | |
| 778 setSectionMenuMode('most-visited', Section.THUMB, | |
| 779 newShownSections & MENU_THUMB, MENU_THUMB); | |
| 780 setSectionMenuMode('recently-closed', undefined, | |
| 781 newShownSections & MENU_RECENT, MENU_RECENT); | |
| 782 layoutSections(); | |
| 783 } | |
| 784 | |
| 785 // Recently closed | |
| 786 | |
| 787 function layoutRecentlyClosed() { | |
| 788 var recentElement = $('recently-closed'); | |
| 789 var miniview = getSectionMiniview(recentElement); | |
| 790 | |
| 791 updateMiniviewClipping(miniview); | |
| 792 | |
| 793 if (miniview.hasChildNodes()) { | |
| 794 recentElement.classList.remove('disabled'); | |
| 795 miniview.classList.add('opaque'); | |
| 796 } else { | |
| 797 recentElement.classList.add('disabled'); | |
| 798 miniview.classList.remove('opaque'); | |
| 799 } | |
| 800 | |
| 801 layoutSections(); | |
| 802 } | |
| 803 | |
| 804 /** | |
| 805 * This function is called by the backend whenever the sync status section | |
| 806 * needs to be updated to reflect recent sync state changes. The backend passes | |
| 807 * the new status information in the newMessage parameter. The state includes | |
| 808 * the following: | |
| 809 * | |
| 810 * syncsectionisvisible: true if the sync section needs to show up on the new | |
| 811 * tab page and false otherwise. | |
| 812 * title: the header for the sync status section. | |
| 813 * msg: the actual message (e.g. "Synced to foo@gmail.com"). | |
| 814 * linkisvisible: true if the link element should be visible within the sync | |
| 815 * section and false otherwise. | |
| 816 * linktext: the text to display as the link in the sync status (only used if | |
| 817 * linkisvisible is true). | |
| 818 * linkurlisset: true if an URL should be set as the href for the link and false | |
| 819 * otherwise. If this field is false, then clicking on the link | |
| 820 * will result in sending a message to the backend (see | |
| 821 * 'SyncLinkClicked'). | |
| 822 * linkurl: the URL to use as the element's href (only used if linkurlisset is | |
| 823 * true). | |
| 824 */ | |
| 825 function syncMessageChanged(newMessage) { | |
| 826 var syncStatusElement = $('sync-status'); | |
| 827 | |
| 828 // Hide the section if the message is emtpy. | |
| 829 if (!newMessage['syncsectionisvisible']) { | |
| 830 syncStatusElement.classList.add('disabled'); | |
| 831 return; | |
| 832 } | |
| 833 | |
| 834 syncStatusElement.classList.remove('disabled'); | |
| 835 | |
| 836 var content = syncStatusElement.children[0]; | |
| 837 | |
| 838 // Set the sync section background color based on the state. | |
| 839 if (newMessage.msgtype == 'error') { | |
| 840 content.style.backgroundColor = 'tomato'; | |
| 841 } else { | |
| 842 content.style.backgroundColor = ''; | |
| 843 } | |
| 844 | |
| 845 // Set the text for the header and sync message. | |
| 846 var titleElement = content.firstElementChild; | |
| 847 titleElement.textContent = newMessage.title; | |
| 848 var messageElement = titleElement.nextElementSibling; | |
| 849 messageElement.textContent = newMessage.msg; | |
| 850 | |
| 851 // Remove what comes after the message | |
| 852 while (messageElement.nextSibling) { | |
| 853 content.removeChild(messageElement.nextSibling); | |
| 854 } | |
| 855 | |
| 856 if (newMessage.linkisvisible) { | |
| 857 var el; | |
| 858 if (newMessage.linkurlisset) { | |
| 859 // Use a link | |
| 860 el = document.createElement('a'); | |
| 861 el.href = newMessage.linkurl; | |
| 862 } else { | |
| 863 el = document.createElement('button'); | |
| 864 el.className = 'link'; | |
| 865 el.addEventListener('click', syncSectionLinkClicked); | |
| 866 } | |
| 867 el.textContent = newMessage.linktext; | |
| 868 content.appendChild(el); | |
| 869 fixLinkUnderline(el); | |
| 870 } | |
| 871 | |
| 872 layoutSections(); | |
| 873 } | |
| 874 | |
| 875 /** | |
| 876 * Invoked when the link in the sync promo or sync status section is clicked. | |
| 877 */ | |
| 878 function syncSectionLinkClicked(e) { | |
| 879 chrome.send('SyncLinkClicked'); | |
| 880 e.preventDefault(); | |
| 881 } | |
| 882 | |
| 883 /** | |
| 884 * Invoked when link to start sync in the promo message is clicked, and Chrome | |
| 885 * has already been synced to an account. | |
| 886 */ | |
| 887 function syncAlreadyEnabled(message) { | |
| 888 showNotification(message.syncEnabledMessage); | |
| 889 } | |
| 890 | |
| 891 /** | |
| 892 * Returns the text used for a recently closed window. | |
| 893 * @param {number} numTabs Number of tabs in the window. | |
| 894 * @return {string} The text to use. | |
| 895 */ | |
| 896 function formatTabsText(numTabs) { | |
| 897 if (numTabs == 1) | |
| 898 return localStrings.getString('closedwindowsingle'); | |
| 899 return localStrings.getStringF('closedwindowmultiple', numTabs); | |
| 900 } | |
| 901 | |
| 902 // Theme related | |
| 903 | |
| 904 function themeChanged(hasAttribution) { | |
| 905 document.documentElement.setAttribute('hasattribution', hasAttribution); | |
| 906 $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); | |
| 907 updateAttribution(); | |
| 908 } | |
| 909 | |
| 910 function updateAttribution() { | |
| 911 // Default value for standard NTP with no theme attribution or custom logo. | |
| 912 logEvent('updateAttribution called'); | |
| 913 var imageId = 'IDR_PRODUCT_LOGO'; | |
| 914 // Theme attribution always overrides custom logos. | |
| 915 if (document.documentElement.getAttribute('hasattribution') == 'true') { | |
| 916 logEvent('updateAttribution called with THEME ATTR'); | |
| 917 imageId = 'IDR_THEME_NTP_ATTRIBUTION'; | |
| 918 } else if (document.documentElement.getAttribute('customlogo') == 'true') { | |
| 919 logEvent('updateAttribution with CUSTOMLOGO'); | |
| 920 imageId = 'IDR_CUSTOM_PRODUCT_LOGO'; | |
| 921 } | |
| 922 | |
| 923 $('attribution-img').src = 'chrome://theme/' + imageId + '?' + Date.now(); | |
| 924 } | |
| 925 | |
| 926 // If the content overlaps with the attribution, we bump its opacity down. | |
| 927 function updateAttributionDisplay(contentBottom) { | |
| 928 var attribution = $('attribution'); | |
| 929 var main = $('main'); | |
| 930 var rtl = document.documentElement.dir == 'rtl'; | |
| 931 var contentRect = main.getBoundingClientRect(); | |
| 932 var attributionRect = attribution.getBoundingClientRect(); | |
| 933 | |
| 934 // Hack. See comments for '.haslayout' in new_tab.css. | |
| 935 if (attributionRect.width == 0) | |
| 936 return; | |
| 937 else | |
| 938 attribution.classList.remove('nolayout'); | |
| 939 | |
| 940 if (contentBottom > attribution.offsetTop) { | |
| 941 if ((!rtl && contentRect.right > attributionRect.left) || | |
| 942 (rtl && attributionRect.right > contentRect.left)) { | |
| 943 attribution.classList.add('obscured'); | |
| 944 return; | |
| 945 } | |
| 946 } | |
| 947 | |
| 948 attribution.classList.remove('obscured'); | |
| 949 } | |
| 950 | |
| 951 function bookmarkBarAttached() { | |
| 952 document.documentElement.setAttribute('bookmarkbarattached', 'true'); | |
| 953 } | |
| 954 | |
| 955 function bookmarkBarDetached() { | |
| 956 document.documentElement.setAttribute('bookmarkbarattached', 'false'); | |
| 957 } | |
| 958 | |
| 959 function viewLog() { | |
| 960 var lines = []; | |
| 961 var start = log[0][1]; | |
| 962 | |
| 963 for (var i = 0; i < log.length; i++) { | |
| 964 lines.push((log[i][1] - start) + ': ' + log[i][0]); | |
| 965 } | |
| 966 | |
| 967 console.log(lines.join('\n')); | |
| 968 } | |
| 969 | |
| 970 // We apply the size class here so that we don't trigger layout animations | |
| 971 // onload. | |
| 972 | |
| 973 handleWindowResize(); | |
| 974 | |
| 975 var localStrings = new LocalStrings(); | |
| 976 | |
| 977 /////////////////////////////////////////////////////////////////////////////// | |
| 978 // Things we know are not needed at startup go below here | |
| 979 | |
| 980 function afterTransition(f) { | |
| 981 if (!isDoneLoading()) { | |
| 982 // Make sure we do not use a timer during load since it slows down the UI. | |
| 983 f(); | |
| 984 } else { | |
| 985 // The duration of all transitions are .15s | |
| 986 window.setTimeout(f, 150); | |
| 987 } | |
| 988 } | |
| 989 | |
| 990 // Notification | |
| 991 | |
| 992 | |
| 993 var notificationTimeout; | |
| 994 | |
| 995 /* | |
| 996 * Displays a message (either a string or a document fragment) in the | |
| 997 * notification slot at the top of the NTP. A close button ("x") will be | |
| 998 * inserted at the end of the message. | |
| 999 * @param {string|Node} message String or node to use as message. | |
| 1000 * @param {string} actionText The text to show as a link next to the message. | |
| 1001 * @param {function=} opt_f Function to call when the user clicks the action | |
| 1002 * link. | |
| 1003 * @param {number=} opt_delay The time in milliseconds before hiding the | |
| 1004 * notification. | |
| 1005 */ | |
| 1006 function showNotification(message, actionText, opt_f, opt_delay) { | |
| 1007 // TODO(arv): Create a notification component. | |
| 1008 var notificationElement = $('notification'); | |
| 1009 var f = opt_f || function() {}; | |
| 1010 var delay = opt_delay || 10000; | |
| 1011 | |
| 1012 function show() { | |
| 1013 window.clearTimeout(notificationTimeout); | |
| 1014 notificationElement.classList.add('show'); | |
| 1015 document.body.classList.add('notification-shown'); | |
| 1016 } | |
| 1017 | |
| 1018 function delayedHide() { | |
| 1019 notificationTimeout = window.setTimeout(hideNotification, delay); | |
| 1020 } | |
| 1021 | |
| 1022 function doAction() { | |
| 1023 f(); | |
| 1024 closeNotification(); | |
| 1025 } | |
| 1026 | |
| 1027 function closeNotification() { | |
| 1028 if (notification.classList.contains('promo')) | |
| 1029 chrome.send('closePromo'); | |
| 1030 hideNotification(); | |
| 1031 } | |
| 1032 | |
| 1033 // Remove classList entries from previous notifications. | |
| 1034 notification.classList.remove('first-run'); | |
| 1035 notification.classList.remove('promo'); | |
| 1036 | |
| 1037 var messageContainer = notificationElement.firstElementChild; | |
| 1038 var actionLink = notificationElement.querySelector('#action-link'); | |
| 1039 var closeButton = notificationElement.querySelector('#notification-close'); | |
| 1040 | |
| 1041 // Remove any previous actionLink entry. | |
| 1042 actionLink.textContent = ''; | |
| 1043 | |
| 1044 $('notification-close').onclick = closeNotification; | |
| 1045 | |
| 1046 if (typeof message == 'string') { | |
| 1047 messageContainer.textContent = message; | |
| 1048 } else { | |
| 1049 messageContainer.textContent = ''; // Remove all children. | |
| 1050 messageContainer.appendChild(message); | |
| 1051 } | |
| 1052 | |
| 1053 if (actionText) { | |
| 1054 actionLink.style.display = ''; | |
| 1055 actionLink.textContent = actionText; | |
| 1056 } else { | |
| 1057 actionLink.style.display = 'none'; | |
| 1058 } | |
| 1059 | |
| 1060 actionLink.onclick = doAction; | |
| 1061 actionLink.onkeydown = handleIfEnterKey(doAction); | |
| 1062 notificationElement.onmouseover = show; | |
| 1063 notificationElement.onmouseout = delayedHide; | |
| 1064 actionLink.onfocus = show; | |
| 1065 actionLink.onblur = delayedHide; | |
| 1066 // Enable tabbing to the link now that it is shown. | |
| 1067 actionLink.tabIndex = 0; | |
| 1068 | |
| 1069 show(); | |
| 1070 delayedHide(); | |
| 1071 } | |
| 1072 | |
| 1073 /** | |
| 1074 * Hides the notifier. | |
| 1075 */ | |
| 1076 function hideNotification() { | |
| 1077 var notificationElement = $('notification'); | |
| 1078 notificationElement.classList.remove('show'); | |
| 1079 document.body.classList.remove('notification-shown'); | |
| 1080 var actionLink = notificationElement.querySelector('#actionlink'); | |
| 1081 var closeButton = notificationElement.querySelector('#notification-close'); | |
| 1082 // Prevent tabbing to the hidden link. | |
| 1083 // Setting tabIndex to -1 only prevents future tabbing to it. If, however, the | |
| 1084 // user switches window or a tab and then moves back to this tab the element | |
| 1085 // may gain focus. We therefore make sure that we blur the element so that the | |
| 1086 // element focus is not restored when coming back to this window. | |
| 1087 if (actionLink) { | |
| 1088 actionLink.tabIndex = -1; | |
| 1089 actionLink.blur(); | |
| 1090 } | |
| 1091 if (closeButton) { | |
| 1092 closeButton.tabIndex = -1; | |
| 1093 closeButton.blur(); | |
| 1094 } | |
| 1095 } | |
| 1096 | |
| 1097 function showPromoNotification() { | |
| 1098 showNotification(parseHtmlSubset(localStrings.getString('serverpromo')), | |
| 1099 localStrings.getString('syncpromotext'), | |
| 1100 function () { chrome.send('SyncLinkClicked'); }, | |
| 1101 60000); | |
| 1102 var notificationElement = $('notification'); | |
| 1103 notification.classList.add('promo'); | |
| 1104 } | |
| 1105 | |
| 1106 $('main').addEventListener('click', function(e) { | |
| 1107 var p = e.target; | |
| 1108 while (p && p.tagName != 'H2') { | |
| 1109 // In case the user clicks on a button we do not want to expand/collapse a | |
| 1110 // section. | |
| 1111 if (p.tagName == 'BUTTON') | |
| 1112 return; | |
| 1113 p = p.parentNode; | |
| 1114 } | |
| 1115 | |
| 1116 if (!p) | |
| 1117 return; | |
| 1118 | |
| 1119 p = p.parentNode; | |
| 1120 if (!getSectionMaxiview(p)) | |
| 1121 return; | |
| 1122 | |
| 1123 toggleSectionVisibilityAndAnimate(p.getAttribute('section')); | |
| 1124 }); | |
| 1125 | |
| 1126 $('most-visited-settings').addEventListener('click', function() { | |
| 1127 $('clear-all-blacklisted').execute(); | |
| 1128 }); | |
| 1129 | |
| 1130 function toggleSectionVisibilityAndAnimate(section) { | |
| 1131 if (!section) | |
| 1132 return; | |
| 1133 | |
| 1134 // It looks better to return the scroll to the top when toggling sections. | |
| 1135 document.body.scrollTop = 0; | |
| 1136 | |
| 1137 // We set it back in webkitTransitionEnd. | |
| 1138 document.documentElement.setAttribute('enable-section-animations', 'true'); | |
| 1139 if (shownSections & Section[section]) { | |
| 1140 hideSection(Section[section]); | |
| 1141 } else { | |
| 1142 showOnlySection(section); | |
| 1143 } | |
| 1144 layoutSections(); | |
| 1145 saveShownSections(); | |
| 1146 } | |
| 1147 | |
| 1148 function handleIfEnterKey(f) { | |
| 1149 return function(e) { | |
| 1150 if (e.keyIdentifier == 'Enter') | |
| 1151 f(e); | |
| 1152 }; | |
| 1153 } | |
| 1154 | |
| 1155 function maybeReopenTab(e) { | |
| 1156 var el = findAncestor(e.target, function(el) { | |
| 1157 return el.sessionId !== undefined; | |
| 1158 }); | |
| 1159 if (el) { | |
| 1160 chrome.send('reopenTab', [String(el.sessionId)]); | |
| 1161 e.preventDefault(); | |
| 1162 | |
| 1163 setWindowTooltipTimeout(); | |
| 1164 } | |
| 1165 } | |
| 1166 | |
| 1167 // Note that the openForeignSession calls can fail, resulting this method to | |
| 1168 // not have any action (hence the maybe). | |
| 1169 function maybeOpenForeignSession(e) { | |
| 1170 var el = findAncestor(e.target, function(el) { | |
| 1171 return el.sessionTag !== undefined; | |
| 1172 }); | |
| 1173 if (el) { | |
| 1174 chrome.send('openForeignSession', [String(el.sessionTag)]); | |
| 1175 e.stopPropagation(); | |
| 1176 e.preventDefault(); | |
| 1177 setWindowTooltipTimeout(); | |
| 1178 } | |
| 1179 } | |
| 1180 | |
| 1181 function maybeOpenForeignWindow(e) { | |
| 1182 var el = findAncestor(e.target, function(el) { | |
| 1183 return el.winNum !== undefined; | |
| 1184 }); | |
| 1185 if (el) { | |
| 1186 chrome.send('openForeignSession', [String(el.sessionTag), | |
| 1187 String(el.winNum)]); | |
| 1188 e.stopPropagation(); | |
| 1189 e.preventDefault(); | |
| 1190 setWindowTooltipTimeout(); | |
| 1191 } | |
| 1192 } | |
| 1193 | |
| 1194 function maybeOpenForeignTab(e) { | |
| 1195 var el = findAncestor(e.target, function(el) { | |
| 1196 return el.sessionId !== undefined; | |
| 1197 }); | |
| 1198 if (el) { | |
| 1199 chrome.send('openForeignSession', [String(el.sessionTag), String(el.winNum), | |
| 1200 String(el.sessionId)]); | |
| 1201 e.stopPropagation(); | |
| 1202 e.preventDefault(); | |
| 1203 setWindowTooltipTimeout(); | |
| 1204 } | |
| 1205 } | |
| 1206 | |
| 1207 // HACK(arv): After the window onblur event happens we get a mouseover event | |
| 1208 // on the next item and we want to make sure that we do not show a tooltip | |
| 1209 // for that. | |
| 1210 function setWindowTooltipTimeout(e) { | |
| 1211 window.setTimeout(function() { | |
| 1212 windowTooltip.hide(); | |
| 1213 }, 2 * WindowTooltip.DELAY); | |
| 1214 } | |
| 1215 | |
| 1216 function maybeShowWindowTooltip(e) { | |
| 1217 var f = function(el) { | |
| 1218 return el.tabItems !== undefined; | |
| 1219 }; | |
| 1220 var el = findAncestor(e.target, f); | |
| 1221 var relatedEl = findAncestor(e.relatedTarget, f); | |
| 1222 if (el && el != relatedEl) { | |
| 1223 windowTooltip.handleMouseOver(e, el, el.tabItems); | |
| 1224 } | |
| 1225 } | |
| 1226 | |
| 1227 | |
| 1228 var recentlyClosedElement = $('recently-closed'); | |
| 1229 | |
| 1230 recentlyClosedElement.addEventListener('click', maybeReopenTab); | |
| 1231 recentlyClosedElement.addEventListener('keydown', | |
| 1232 handleIfEnterKey(maybeReopenTab)); | |
| 1233 | |
| 1234 recentlyClosedElement.addEventListener('mouseover', maybeShowWindowTooltip); | |
| 1235 recentlyClosedElement.addEventListener('focus', maybeShowWindowTooltip, true); | |
| 1236 | |
| 1237 var foreignSessionElement = $('foreign-sessions'); | |
| 1238 | |
| 1239 foreignSessionElement.addEventListener('click', maybeOpenForeignSession); | |
| 1240 foreignSessionElement.addEventListener('keydown', | |
| 1241 handleIfEnterKey( | |
| 1242 maybeOpenForeignSession)); | |
| 1243 | |
| 1244 foreignSessionElement.addEventListener('mouseover', maybeShowWindowTooltip); | |
| 1245 foreignSessionElement.addEventListener('focus', maybeShowWindowTooltip, true); | |
| 1246 | |
| 1247 /** | |
| 1248 * This object represents a tooltip representing a closed window. It is | |
| 1249 * shown when hovering over a closed window item or when the item is focused. It | |
| 1250 * gets hidden when blurred or when mousing out of the menu or the item. | |
| 1251 * @param {Element} tooltipEl The element to use as the tooltip. | |
| 1252 * @constructor | |
| 1253 */ | |
| 1254 function WindowTooltip(tooltipEl) { | |
| 1255 this.tooltipEl = tooltipEl; | |
| 1256 this.boundHide_ = this.hide.bind(this); | |
| 1257 this.boundHandleMouseOut_ = this.handleMouseOut.bind(this); | |
| 1258 } | |
| 1259 | |
| 1260 WindowTooltip.trackMouseMove_ = function(e) { | |
| 1261 WindowTooltip.clientX = e.clientX; | |
| 1262 WindowTooltip.clientY = e.clientY; | |
| 1263 }; | |
| 1264 | |
| 1265 /** | |
| 1266 * Time in ms to delay before the tooltip is shown. | |
| 1267 * @type {number} | |
| 1268 */ | |
| 1269 WindowTooltip.DELAY = 300; | |
| 1270 | |
| 1271 WindowTooltip.prototype = { | |
| 1272 timer: 0, | |
| 1273 handleMouseOver: function(e, linkEl, tabs) { | |
| 1274 this.linkEl_ = linkEl; | |
| 1275 if (e.type == 'mouseover') { | |
| 1276 this.linkEl_.addEventListener('mousemove', WindowTooltip.trackMouseMove_); | |
| 1277 this.linkEl_.addEventListener('mouseout', this.boundHandleMouseOut_); | |
| 1278 } else { // focus | |
| 1279 this.linkEl_.addEventListener('blur', this.boundHide_); | |
| 1280 } | |
| 1281 this.timer = window.setTimeout(this.show.bind(this, e.type, linkEl, tabs), | |
| 1282 WindowTooltip.DELAY); | |
| 1283 }, | |
| 1284 show: function(type, linkEl, tabs) { | |
| 1285 window.addEventListener('blur', this.boundHide_); | |
| 1286 this.linkEl_.removeEventListener('mousemove', | |
| 1287 WindowTooltip.trackMouseMove_); | |
| 1288 window.clearTimeout(this.timer); | |
| 1289 | |
| 1290 this.renderItems(tabs); | |
| 1291 var rect = linkEl.getBoundingClientRect(); | |
| 1292 var bodyRect = document.body.getBoundingClientRect(); | |
| 1293 var rtl = document.documentElement.dir == 'rtl'; | |
| 1294 | |
| 1295 this.tooltipEl.style.display = 'block'; | |
| 1296 var tooltipRect = this.tooltipEl.getBoundingClientRect(); | |
| 1297 var x, y; | |
| 1298 | |
| 1299 // When focused show below, like a drop down menu. | |
| 1300 if (type == 'focus') { | |
| 1301 x = rtl ? | |
| 1302 rect.left + bodyRect.left + rect.width - this.tooltipEl.offsetWidth : | |
| 1303 rect.left + bodyRect.left; | |
| 1304 y = rect.top + bodyRect.top + rect.height; | |
| 1305 } else { | |
| 1306 x = bodyRect.left + (rtl ? | |
| 1307 WindowTooltip.clientX - this.tooltipEl.offsetWidth : | |
| 1308 WindowTooltip.clientX); | |
| 1309 // Offset like a tooltip | |
| 1310 y = 20 + WindowTooltip.clientY + bodyRect.top; | |
| 1311 } | |
| 1312 | |
| 1313 // We need to ensure that the tooltip is inside the window viewport. | |
| 1314 x = Math.min(x, bodyRect.width - tooltipRect.width); | |
| 1315 x = Math.max(x, 0); | |
| 1316 y = Math.min(y, bodyRect.height - tooltipRect.height); | |
| 1317 y = Math.max(y, 0); | |
| 1318 | |
| 1319 this.tooltipEl.style.left = x + 'px'; | |
| 1320 this.tooltipEl.style.top = y + 'px'; | |
| 1321 }, | |
| 1322 handleMouseOut: function(e) { | |
| 1323 // Don't hide when move to another item in the link. | |
| 1324 var f = function(el) { | |
| 1325 return el.tabItems !== undefined; | |
| 1326 }; | |
| 1327 var el = findAncestor(e.target, f); | |
| 1328 var relatedEl = findAncestor(e.relatedTarget, f); | |
| 1329 if (el && el != relatedEl) { | |
| 1330 this.hide(); | |
| 1331 } | |
| 1332 }, | |
| 1333 hide: function() { | |
| 1334 window.clearTimeout(this.timer); | |
| 1335 window.removeEventListener('blur', this.boundHide_); | |
| 1336 this.linkEl_.removeEventListener('mousemove', | |
| 1337 WindowTooltip.trackMouseMove_); | |
| 1338 this.linkEl_.removeEventListener('mouseout', this.boundHandleMouseOut_); | |
| 1339 this.linkEl_.removeEventListener('blur', this.boundHide_); | |
| 1340 this.linkEl_ = null; | |
| 1341 | |
| 1342 this.tooltipEl.style.display = 'none'; | |
| 1343 }, | |
| 1344 renderItems: function(tabs) { | |
| 1345 var tooltip = this.tooltipEl; | |
| 1346 tooltip.textContent = ''; | |
| 1347 | |
| 1348 tabs.forEach(function(tab) { | |
| 1349 var span = document.createElement('span'); | |
| 1350 span.className = 'item'; | |
| 1351 span.style.backgroundImage = url('chrome://favicon/' + tab.url); | |
| 1352 span.dir = tab.direction; | |
| 1353 span.textContent = tab.title; | |
| 1354 tooltip.appendChild(span); | |
| 1355 }); | |
| 1356 } | |
| 1357 }; | |
| 1358 | |
| 1359 var windowTooltip = new WindowTooltip($('window-tooltip')); | |
| 1360 | |
| 1361 window.addEventListener('load', | |
| 1362 logEvent.bind(global, 'Tab.NewTabOnload', true)); | |
| 1363 | |
| 1364 window.addEventListener('resize', handleWindowResize); | |
| 1365 document.addEventListener('DOMContentLoaded', | |
| 1366 logEvent.bind(global, 'Tab.NewTabDOMContentLoaded', true)); | |
| 1367 | |
| 1368 // Whether or not we should send the initial 'GetSyncMessage' to the backend | |
| 1369 // depends on the value of the attribue 'syncispresent' which the backend sets | |
| 1370 // to indicate if there is code in the backend which is capable of processing | |
| 1371 // this message. This attribute is loaded by the JSTemplate and therefore we | |
| 1372 // must make sure we check the attribute after the DOM is loaded. | |
| 1373 document.addEventListener('DOMContentLoaded', | |
| 1374 callGetSyncMessageIfSyncIsPresent); | |
| 1375 | |
| 1376 /** | |
| 1377 * The sync code is not yet built by default on all platforms so we have to | |
| 1378 * make sure we don't send the initial sync message to the backend unless the | |
| 1379 * backend told us that the sync code is present. | |
| 1380 */ | |
| 1381 function callGetSyncMessageIfSyncIsPresent() { | |
| 1382 if (document.documentElement.getAttribute('syncispresent') == 'true') { | |
| 1383 chrome.send('GetSyncMessage'); | |
| 1384 } | |
| 1385 } | |
| 1386 | |
| 1387 // Tooltip for elements that have text that overflows. | |
| 1388 document.addEventListener('mouseover', function(e) { | |
| 1389 // We don't want to do this while we are dragging because it makes things very | |
| 1390 // janky | |
| 1391 if (mostVisited.isDragging()) { | |
| 1392 return; | |
| 1393 } | |
| 1394 | |
| 1395 var el = findAncestor(e.target, function(el) { | |
| 1396 return el.xtitle; | |
| 1397 }); | |
| 1398 if (el && el.xtitle != el.title) { | |
| 1399 if (el.scrollWidth > el.clientWidth) { | |
| 1400 el.title = el.xtitle; | |
| 1401 } else { | |
| 1402 el.title = ''; | |
| 1403 } | |
| 1404 } | |
| 1405 }); | |
| 1406 | |
| 1407 /** | |
| 1408 * Makes links and buttons support a different underline color. | |
| 1409 * @param {Node} node The node to search for links and buttons in. | |
| 1410 */ | |
| 1411 function fixLinkUnderlines(node) { | |
| 1412 var elements = node.querySelectorAll('a,button'); | |
| 1413 Array.prototype.forEach.call(elements, fixLinkUnderline); | |
| 1414 } | |
| 1415 | |
| 1416 /** | |
| 1417 * Wraps the content of an element in a a link-color span. | |
| 1418 * @param {Element} el The element to wrap. | |
| 1419 */ | |
| 1420 function fixLinkUnderline(el) { | |
| 1421 var span = document.createElement('span'); | |
| 1422 span.className = 'link-color'; | |
| 1423 while (el.hasChildNodes()) { | |
| 1424 span.appendChild(el.firstChild); | |
| 1425 } | |
| 1426 el.appendChild(span); | |
| 1427 } | |
| 1428 | |
| 1429 updateAttribution(); | |
| 1430 | |
| 1431 function initializeLogin() { | |
| 1432 chrome.send('initializeLogin', []); | |
| 1433 } | |
| 1434 | |
| 1435 function updateLogin(login) { | |
| 1436 $('login-container').style.display = login ? 'block' : ''; | |
| 1437 if (login) | |
| 1438 $('login-username').textContent = login; | |
| 1439 } | |
| 1440 | |
| 1441 var mostVisited = new MostVisited( | |
| 1442 $('most-visited-maxiview'), | |
| 1443 document.querySelector('#most-visited .miniview'), | |
| 1444 $('most-visited-menu'), | |
| 1445 useSmallGrid(), | |
| 1446 shownSections & Section.THUMB); | |
| 1447 | |
| 1448 function setMostVisitedPages(data, hasBlacklistedUrls) { | |
| 1449 logEvent('received most visited pages'); | |
| 1450 | |
| 1451 mostVisited.updateSettingsLink(hasBlacklistedUrls); | |
| 1452 mostVisited.data = data; | |
| 1453 mostVisited.layout(); | |
| 1454 layoutSections(); | |
| 1455 | |
| 1456 // Remove class name in a timeout so that changes done in this JS thread are | |
| 1457 // not animated. | |
| 1458 window.setTimeout(function() { | |
| 1459 mostVisited.ensureSmallGridCorrect(); | |
| 1460 maybeDoneLoading(); | |
| 1461 }, 1); | |
| 1462 | |
| 1463 if (localStrings.getString('serverpromo')) { | |
| 1464 showPromoNotification(); | |
| 1465 } | |
| 1466 | |
| 1467 } | |
| 1468 | |
| 1469 function maybeDoneLoading() { | |
| 1470 if (mostVisited.data && apps.loaded) | |
| 1471 document.body.classList.remove('loading'); | |
| 1472 } | |
| 1473 | |
| 1474 function isDoneLoading() { | |
| 1475 return !document.body.classList.contains('loading'); | |
| 1476 } | |
| 1477 | |
| 1478 document.addEventListener('DOMContentLoaded', function() { | |
| 1479 cr.enablePlatformSpecificCSSRules(); | |
| 1480 | |
| 1481 // Initialize the listener for the "hide this" link on the apps promo. We do | |
| 1482 // this outside of getAppsCallback because it only needs to be done once per | |
| 1483 // NTP load. | |
| 1484 $('apps-promo-hide').addEventListener('click', function() { | |
| 1485 chrome.send('hideAppsPromo', []); | |
| 1486 document.documentElement.classList.remove('apps-promo-visible'); | |
| 1487 layoutSections(); | |
| 1488 }); | |
| 1489 }); | |
| OLD | NEW |