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 // File Description: 6 // Contains all the necessary functions for rendering the NTP on mobile 7 // devices. 8 9 /** 10 * The event type used to determine when a touch starts. 11 * @type {string} 12 */ 13 var PRESS_START_EVT = 'touchstart'; 14 15 /** 16 * The event type used to determine when a touch finishes. 17 * @type {string} 18 */ 19 var PRESS_STOP_EVT = 'touchend'; 20 21 /** 22 * The event type used to determine when a touch moves. 23 * @type {string} 24 */ 25 var PRESS_MOVE_EVT = 'touchmove'; 26 27 cr.define('ntp', function() { 28 /** 29 * Constant for the localStorage key used to specify the default bookmark 30 * folder to be selected when navigating to the bookmark tab for the first 31 * time of a new NTP instance. 32 * @type {string} 33 */ 34 var DEFAULT_BOOKMARK_FOLDER_KEY = 'defaultBookmarkFolder'; 35 36 /** 37 * Constant for the localStorage key used to store whether or not sync was 38 * enabled on the last call to syncEnabled(). 39 * @type {string} 40 */ 41 var SYNC_ENABLED_KEY = 'syncEnabled'; 42 43 /** 44 * The time before and item gets marked as active (in milliseconds). This 45 * prevents an item from being marked as active when the user is scrolling 46 * the page. 47 * @type {number} 48 */ 49 var ACTIVE_ITEM_DELAY_MS = 100; 50 51 /** 52 * The CSS class identifier for grid layouts. 53 * @type {string} 54 */ 55 var GRID_CSS_CLASS = 'icon-grid'; 56 57 /** 58 * The element to center when centering a GRID_CSS_CLASS. 59 */ 60 var GRID_CENTER_CSS_CLASS = 'center-icon-grid'; 61 62 /** 63 * Attribute used to specify the number of columns to use in a grid. If 64 * left unspecified, the grid will fill the container. 65 */ 66 var GRID_COLUMNS = 'grid-columns'; 67 68 /** 69 * Attribute used to specify whether the top margin should be set to match 70 * the left margin of the grid. 71 */ 72 var GRID_SET_TOP_MARGIN_CLASS = 'grid-set-top-margin'; 73 74 /** 75 * Attribute used to specify whether the margins of individual items within 76 * the grid should be adjusted to better fill the space. 77 */ 78 var GRID_SET_ITEM_MARGINS = 'grid-set-item-margins'; 79 80 /** 81 * The CSS class identifier for centered empty section containers. 82 */ 83 var CENTER_EMPTY_CONTAINER_CSS_CLASS = 'center-empty-container'; 84 85 /** 86 * The CSS class identifier for marking list items as active. 87 * @type {string} 88 */ 89 var ACTIVE_LIST_ITEM_CSS_CLASS = 'list-item-active'; 90 91 /** 92 * Attributes set on elements representing data in a section, specifying 93 * which section that element belongs to. Used for context menus. 94 * @type {string} 95 */ 96 var SECTION_KEY = 'sectionType'; 97 98 /** 99 * Attribute set on an element that has a context menu. Specifies the URL for 100 * which the context menu action should apply. 101 * @type {string} 102 */ 103 var CONTEXT_MENU_URL_KEY = 'url'; 104 105 /** 106 * The list of main section panes added. 107 * @type {Array.<Element>} 108 */ 109 var panes = []; 110 111 /** 112 * The list of section prefixes, which are used to append to the hash of the 113 * page to allow the native toolbar to see url changes when the pane is 114 * switched. 115 */ 116 var sectionPrefixes = []; 117 118 /** 119 * The next available index for new favicons. Users must increment this 120 * value once assigning this index to a favicon. 121 * @type {number} 122 */ 123 var faviconIndex = 0; 124 125 /** 126 * The currently selected pane DOM element. 127 * @type {Element} 128 */ 129 var currentPane = null; 130 131 /** 132 * The index of the currently selected top level pane. The index corresponds 133 * to the elements defined in {@see #panes}. 134 * @type {number} 135 */ 136 var currentPaneIndex; 137 138 /** 139 * The ID of the bookmark folder currently selected. 140 * @type {string|number} 141 */ 142 var bookmarkFolderId = null; 143 144 /** 145 * The current element active item. 146 * @type {?Element} 147 */ 148 var activeItem; 149 150 /** 151 * The element to be marked as active if no actions cancel it. 152 * @type {?Element} 153 */ 154 var pendingActiveItem; 155 156 /** 157 * The timer ID to mark an element as active. 158 * @type {number} 159 */ 160 var activeItemDelayTimerId; 161 162 /** 163 * Enum for the different load states based on the initialization of the NTP. 164 * @enum {number} 165 */ 166 var LoadStatusType = { 167 LOAD_NOT_DONE: 0, 168 LOAD_IMAGES_COMPLETE: 1, 169 LOAD_BOOKMARKS_FINISHED: 2, 170 LOAD_COMPLETE: 3 // An OR'd combination of all necessary states. 171 }; 172 173 /** 174 * The current loading status for the NTP. 175 * @type {LoadStatusType} 176 */ 177 var loadStatus_ = LoadStatusType.LOAD_NOT_DONE; 178 179 /** 180 * Whether the loading complete notification has been sent. 181 * @type {boolean} 182 */ 183 var finishedLoadingNotificationSent_ = false; 184 185 /** 186 * Whether the page title has been loaded. 187 * @type {boolean} 188 */ 189 var titleLoadedStatus_ = false; 190 191 /** 192 * Whether the NTP is in incognito mode or not. 193 * @type {boolean} 194 */ 195 var isIncognito = false; 196 197 /** 198 * Whether incognito mode is enabled. (It can be blocked e.g. with a policy.) 199 * @type {boolean} 200 */ 201 var isIncognitoEnabled = true; 202 203 /** 204 * Whether the initial history state has been replaced. The state will be 205 * replaced once the bookmark data has loaded to ensure the proper folder 206 * id is persisted. 207 * @type {boolean} 208 */ 209 var replacedInitialState = false; 210 211 /** 212 * Stores number of most visited pages. 213 * @type {number} 214 */ 215 var numberOfMostVisitedPages = 0; 216 217 /** 218 * Whether there are any recently closed tabs. 219 * @type {boolean} 220 */ 221 var hasRecentlyClosedTabs = false; 222 223 /** 224 * Whether promo is not allowed or not (external to NTP). 225 * @type {boolean} 226 */ 227 var promoIsAllowed = false; 228 229 /** 230 * Whether promo should be shown on Most Visited page (externally set). 231 * @type {boolean} 232 */ 233 var promoIsAllowedOnMostVisited = false; 234 235 /** 236 * Whether promo should be shown on Open Tabs page (externally set). 237 * @type {boolean} 238 */ 239 var promoIsAllowedOnOpenTabs = false; 240 241 /** 242 * Whether promo should show a virtual computer on Open Tabs (externally set). 243 * @type {boolean} 244 */ 245 var promoIsAllowedAsVirtualComputer = false; 246 247 /** 248 * Promo-injected title of a virtual computer on an open tabs pane. 249 * @type {string} 250 */ 251 var promoInjectedComputerTitleText = ''; 252 253 /** 254 * Promo-injected last synced text of a virtual computer on an open tabs pane. 255 * @type {string} 256 */ 257 var promoInjectedComputerLastSyncedText = ''; 258 259 /** 260 * The different sections that are displayed. 261 * @enum {number} 262 */ 263 var SectionType = { 264 BOOKMARKS: 'bookmarks', 265 FOREIGN_SESSION: 'foreign_session', 266 FOREIGN_SESSION_HEADER: 'foreign_session_header', 267 MOST_VISITED: 'most_visited', 268 PROMO_VC_SESSION_HEADER: 'promo_vc_session_header', 269 RECENTLY_CLOSED: 'recently_closed', 270 SNAPSHOTS: 'snapshots', 271 UNKNOWN: 'unknown', 272 }; 273 274 /** 275 * The different ids used of our custom context menu. Sent to the ChromeView 276 * and sent back when a menu is selected. 277 * @enum {number} 278 */ 279 var ContextMenuItemIds = { 280 BOOKMARK_EDIT: 0, 281 BOOKMARK_DELETE: 1, 282 BOOKMARK_OPEN_IN_NEW_TAB: 2, 283 BOOKMARK_OPEN_IN_INCOGNITO_TAB: 3, 284 BOOKMARK_SHORTCUT: 4, 285 286 MOST_VISITED_OPEN_IN_NEW_TAB: 10, 287 MOST_VISITED_OPEN_IN_INCOGNITO_TAB: 11, 288 MOST_VISITED_REMOVE: 12, 289 290 RECENTLY_CLOSED_OPEN_IN_NEW_TAB: 20, 291 RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB: 21, 292 RECENTLY_CLOSED_REMOVE: 22, 293 294 FOREIGN_SESSIONS_REMOVE: 30, 295 296 PROMO_VC_SESSION_REMOVE: 40, 297 }; 298 299 /** 300 * The URL of the element for the context menu. 301 * @type {string} 302 */ 303 var contextMenuUrl = null; 304 305 var contextMenuItem = null; 306 307 var currentSnapshots = null; 308 309 var currentSessions = null; 310 311 /** 312 * The possible states of the sync section 313 * @enum {number} 314 */ 315 var SyncState = { 316 INITIAL: 0, 317 WAITING_FOR_DATA: 1, 318 DISPLAYING_LOADING: 2, 319 DISPLAYED_LOADING: 3, 320 LOADED: 4, 321 }; 322 323 /** 324 * The current state of the sync section. 325 */ 326 var syncState = SyncState.INITIAL; 327 328 /** 329 * Whether or not sync is enabled. It will be undefined until 330 * setSyncEnabled() is called. 331 * @type {?boolean} 332 */ 333 var syncEnabled = undefined; 334 335 /** 336 * The current most visited data being displayed. 337 * @type {Array.<Object>} 338 */ 339 var mostVisitedData_ = []; 340 341 /** 342 * The current bookmark data being displayed. Keep a reference to this data 343 * in case the sync enabled state changes. In this case, the bookmark data 344 * will need to be refiltered. 345 * @type {?Object} 346 */ 347 var bookmarkData; 348 349 /** 350 * Keep track of any outstanding timers related to updating the sync section. 351 */ 352 var syncTimerId = -1; 353 354 /** 355 * The minimum amount of time that 'Loading...' can be displayed. This is to 356 * prevent flashing. 357 */ 358 var SYNC_LOADING_TIMEOUT = 1000; 359 360 /** 361 * How long to wait for sync data to load before displaying the 'Loading...' 362 * text to the user. 363 */ 364 var SYNC_INITIAL_LOAD_TIMEOUT = 1000; 365 366 /** 367 * An array of images that are currently in loading state. Once an image 368 * loads it is removed from this array. 369 */ 370 var imagesBeingLoaded = new Array(); 371 372 /** 373 * Flag indicating if we are on bookmark shortcut mode. 374 * In this mode, only the bookmark section is available and selecting 375 * a non-folder bookmark adds it to the home screen. 376 * Context menu is disabled. 377 */ 378 var bookmarkShortcutMode = false; 379 380 function setIncognitoMode(incognito) { 381 isIncognito = incognito; 382 if (!isIncognito) { 383 chrome.send('getMostVisited'); 384 chrome.send('getRecentlyClosedTabs'); 385 chrome.send('getForeignSessions'); 386 chrome.send('getPromotions'); 387 chrome.send('getIncognitoDisabled'); 388 } 389 } 390 391 function setIncognitoEnabled(item) { 392 isIncognitoEnabled = item.incognitoEnabled; 393 } 394 395 /** 396 * Flag set to true when the page is loading its initial set of images. This 397 * is set to false after all the initial images have loaded. 398 */ 399 function onInitialImageLoaded(event) { 400 var url = event.target.src; 401 for (var i = 0; i < imagesBeingLoaded.length; ++i) { 402 if (imagesBeingLoaded[i].src == url) { 403 imagesBeingLoaded.splice(i, 1); 404 if (imagesBeingLoaded.length == 0) { 405 // To send out the NTP loading complete notification. 406 loadStatus_ |= LoadStatusType.LOAD_IMAGES_COMPLETE; 407 sendNTPNotification(); 408 } 409 } 410 } 411 } 412 413 /** 414 * Marks the given image as currently being loaded. Once all such images load 415 * we inform the browser via a hash change. 416 */ 417 function trackImageLoad(url) { 418 if (finishedLoadingNotificationSent_) 419 return; 420 421 for (var i = 0; i < imagesBeingLoaded.length; ++i) { 422 if (imagesBeingLoaded[i].src == url) 423 return; 424 } 425 426 loadStatus_ &= (~LoadStatusType.LOAD_IMAGES_COMPLETE); 427 428 var image = new Image(); 429 image.onload = onInitialImageLoaded; 430 image.onerror = onInitialImageLoaded; 431 image.src = url; 432 imagesBeingLoaded.push(image); 433 } 434 435 /** 436 * Initializes all the UI once the page has loaded. 437 */ 438 function init() { 439 // Special case to handle NTP caching. 440 if (window.location.hash == '#cached_ntp') 441 document.location.hash = '#most_visited'; 442 // Special case to show a specific bookmarks folder. 443 // Used to show the mobile bookmarks folder after importing. 444 var bookmarkIdMatch = window.location.hash.match(/#bookmarks:(\d+)/); 445 if (bookmarkIdMatch && bookmarkIdMatch.length == 2) { 446 localStorage.setItem(DEFAULT_BOOKMARK_FOLDER_KEY, bookmarkIdMatch[1]); 447 document.location.hash = '#bookmarks'; 448 } 449 // Special case to choose a bookmark for adding a shortcut. 450 // See the doc of bookmarkShortcutMode for details. 451 if (window.location.hash == '#bookmark_shortcut') 452 bookmarkShortcutMode = true; 453 // Make sure a valid section is always displayed. Both normal and 454 // incognito NTPs have a bookmarks section. 455 if (getPaneIndexFromHash() < 0) 456 document.location.hash = '#bookmarks'; 457 458 // Initialize common widgets. 459 var titleScrollers = 460 document.getElementsByClassName('section-title-wrapper'); 461 for (var i = 0, len = titleScrollers.length; i < len; i++) 462 initializeTitleScroller(titleScrollers[i]); 463 464 // Initialize virtual computers for the sync promo. 465 createPromoVirtualComputers(); 466 467 setCurrentBookmarkFolderData( 468 localStorage.getItem(DEFAULT_BOOKMARK_FOLDER_KEY)); 469 470 addMainSection('incognito'); 471 addMainSection('most_visited'); 472 addMainSection('bookmarks'); 473 addMainSection('open_tabs'); 474 475 computeDynamicLayout(); 476 477 scrollToPane(getPaneIndexFromHash()); 478 updateSyncEmptyState(); 479 480 window.onpopstate = onPopStateHandler; 481 window.addEventListener('hashchange', updatePaneOnHash); 482 window.addEventListener('resize', windowResizeHandler); 483 484 if (!bookmarkShortcutMode) 485 window.addEventListener('contextmenu', contextMenuHandler); 486 } 487 488 function sendNTPTitleLoadedNotification() { 489 if (!titleLoadedStatus_) { 490 titleLoadedStatus_ = true; 491 chrome.send('notifyNTPTitleLoaded'); 492 } 493 } 494 495 /** 496 * Notifies the chrome process of the status of the NTP. 497 */ 498 function sendNTPNotification() { 499 if (loadStatus_ != LoadStatusType.LOAD_COMPLETE) 500 return; 501 502 if (!finishedLoadingNotificationSent_) { 503 finishedLoadingNotificationSent_ = true; 504 chrome.send('notifyNTPReady'); 505 } else { 506 // Navigating after the loading complete notification has been sent 507 // might break tests. 508 chrome.send('NTPUnexpectedNavigation'); 509 } 510 } 511 512 /** 513 * The default click handler for created item shortcuts. 514 * 515 * @param {Object} item The item specification. 516 * @param {function} evt The browser click event triggered. 517 */ 518 function itemShortcutClickHandler(item, evt) { 519 // Handle the touch callback 520 if (item['folder']) { 521 browseToBookmarkFolder(item.id); 522 } else { 523 if (bookmarkShortcutMode) { 524 chrome.send('createHomeScreenBookmarkShortcut', [item.id]); 525 } else if (!!item.url) { 526 window.location = item.url; 527 } 528 } 529 } 530 531 /** 532 * Opens a recently closed tab. 533 * 534 * @param {Object} item An object containing the necessary information to 535 * reopen a tab. 536 */ 537 function openRecentlyClosedTab(item, evt) { 538 chrome.send('openedRecentlyClosed'); 539 chrome.send('reopenTab', [item.sessionId]); 540 } 541 542 /** 543 * Creates a 'div' DOM element. 544 * 545 * @param {string} className The CSS class name for the DIV. 546 * @param {string=} opt_backgroundUrl The background URL to be applied to the 547 * DIV if required. 548 * @return {Element} The newly created DIV element. 549 */ 550 function createDiv(className, opt_backgroundUrl) { 551 var div = document.createElement('div'); 552 div.className = className; 553 if (opt_backgroundUrl) 554 div.style.backgroundImage = 'url(' + opt_backgroundUrl + ')'; 555 return div; 556 } 557 558 /** 559 * Helper for creating new DOM elements. 560 * 561 * @param {string} type The type of Element to be created (i.e. 'div', 562 * 'span'). 563 * @param {Object} params A mapping of element attribute key and values that 564 * should be applied to the new element. 565 * @return {Element} The newly created DOM element. 566 */ 567 function createElement(type, params) { 568 var el = document.createElement(type); 569 if (typeof params === 'string') { 570 el.className = params; 571 } else { 572 for (attr in params) { 573 el[attr] = params[attr]; 574 } 575 } 576 return el; 577 } 578 579 /** 580 * Adds a click listener to a specified element with the ability to override 581 * the default value of itemShortcutClickHandler. 582 * 583 * @param {Element} el The element the click listener should be added to. 584 * @param {Object} item The item data represented by the element. 585 * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The 586 * click callback to be triggered upon selection. 587 */ 588 function wrapClickHandler(el, item, opt_clickCallback) { 589 el.addEventListener('click', function(evt) { 590 var clickCallback = 591 opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler; 592 clickCallback(item, evt); 593 }); 594 } 595 596 /** 597 * Create a DOM element to contain a recently closed item for a tablet 598 * device. 599 * 600 * @param {Object} item The data of the item used to generate the shortcut. 601 * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The 602 * click callback to be triggered upon selection (if not provided it will 603 * use the default -- itemShortcutClickHandler). 604 * @return {Element} The shortcut element created. 605 */ 606 function makeRecentlyClosedTabletItem(item, opt_clickCallback) { 607 var cell = createDiv('cell'); 608 609 cell.setAttribute(CONTEXT_MENU_URL_KEY, item.url); 610 611 var iconUrl = item.icon; 612 if (!iconUrl) { 613 iconUrl = 'chrome://touch-icon/size/16@' + window.devicePixelRatio + 614 'x/' + item.url; 615 } 616 var icon = createDiv('icon', iconUrl); 617 trackImageLoad(iconUrl); 618 cell.appendChild(icon); 619 620 var title = createDiv('title'); 621 title.textContent = item.title; 622 cell.appendChild(title); 623 624 wrapClickHandler(cell, item, opt_clickCallback); 625 626 return cell; 627 } 628 629 /** 630 * Creates a shortcut DOM element based on the item specified item 631 * configuration using the thumbnail layout used for most visited. Other 632 * data types should not use this as they won't have a thumbnail. 633 * 634 * @param {Object} item The data of the item used to generate the shortcut. 635 * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The 636 * click callback to be triggered upon selection (if not provided it will 637 * use the default -- itemShortcutClickHandler). 638 * @return {Element} The shortcut element created. 639 */ 640 function makeMostVisitedItem(item, opt_clickCallback) { 641 // thumbnail-cell -- main outer container 642 // thumbnail-container -- container for the thumbnail 643 // thumbnail -- the actual thumbnail image; outer border 644 // inner-border -- inner border 645 // title -- container for the title 646 // img -- hack align title text baseline with bottom 647 // title text -- the actual text of the title 648 var thumbnailCell = createDiv('thumbnail-cell'); 649 var thumbnailContainer = createDiv('thumbnail-container'); 650 var backgroundUrl = item.thumbnailUrl || 'chrome://thumb/' + item.url; 651 if (backgroundUrl == 'chrome://thumb/chrome://welcome/') { 652 // Ideally, it would be nice to use the URL as is. However, as of now 653 // theme support has been removed from Chrome. Instead, load the image 654 // URL from a style and use it. Don't just use the style because 655 // trackImageLoad(...) must be called with the background URL. 656 var welcomeStyle = findCssRule('.welcome-to-chrome').style; 657 var backgroundImage = welcomeStyle.backgroundImage; 658 // trim the "url(" prefix and ")" suffix 659 backgroundUrl = backgroundImage.substring(4, backgroundImage.length - 1); 660 } 661 trackImageLoad(backgroundUrl); 662 var thumbnail = createDiv('thumbnail'); 663 // Use an Image object to ensure the thumbnail image actually exists. If 664 // not, this will allow the default to show instead. 665 var thumbnailImg = new Image(); 666 thumbnailImg.onload = function() { 667 thumbnail.style.backgroundImage = 'url(' + backgroundUrl + ')'; 668 }; 669 thumbnailImg.src = backgroundUrl; 670 671 thumbnailContainer.appendChild(thumbnail); 672 var innerBorder = createDiv('inner-border'); 673 thumbnailContainer.appendChild(innerBorder); 674 thumbnailCell.appendChild(thumbnailContainer); 675 var title = createDiv('title'); 676 title.textContent = item.title; 677 var spacerImg = createElement('img', 'title-spacer'); 678 spacerImg.alt = ''; 679 title.insertBefore(spacerImg, title.firstChild); 680 thumbnailCell.appendChild(title); 681 682 var shade = createDiv('thumbnail-cell-shade'); 683 thumbnailContainer.appendChild(shade); 684 addActiveTouchListener(shade, 'thumbnail-cell-shade-active'); 685 686 wrapClickHandler(thumbnailCell, item, opt_clickCallback); 687 688 thumbnailCell.setAttribute(CONTEXT_MENU_URL_KEY, item.url); 689 thumbnailCell.contextMenuItem = item; 690 return thumbnailCell; 691 } 692 693 /** 694 * Creates a shortcut DOM element based on the item specified item 695 * configuration using the favicon layout used for bookmarks. 696 * 697 * @param {Object} item The data of the item used to generate the shortcut. 698 * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The 699 * click callback to be triggered upon selection (if not provided it will 700 * use the default -- itemShortcutClickHandler). 701 * @return {Element} The shortcut element created. 702 */ 703 function makeBookmarkItem(item, opt_clickCallback) { 704 var holder = createDiv('favicon-cell'); 705 addActiveTouchListener(holder, 'favicon-cell-active'); 706 707 holder.setAttribute(CONTEXT_MENU_URL_KEY, item.url); 708 holder.contextMenuItem = item; 709 var faviconBox = createDiv('favicon-box'); 710 if (item.folder) { 711 faviconBox.classList.add('folder'); 712 } else { 713 var iconUrl = item.icon || 'chrome://touch-icon/largest/' + item.url; 714 var faviconIcon = createDiv('favicon-icon'); 715 faviconIcon.style.backgroundImage = 'url(' + iconUrl + ')'; 716 trackImageLoad(iconUrl); 717 718 var image = new Image(); 719 image.src = iconUrl; 720 image.onload = function() { 721 var w = image.width; 722 var h = image.height; 723 if (Math.floor(w) <= 16 || Math.floor(h) <= 16) { 724 // it's a standard favicon (or at least it's small). 725 faviconBox.classList.add('document'); 726 727 faviconBox.appendChild( 728 createDiv('color-strip colorstrip-' + faviconIndex)); 729 faviconBox.appendChild(createDiv('bookmark-border')); 730 var foldDiv = createDiv('fold'); 731 foldDiv.id = 'fold_' + faviconIndex; 732 foldDiv.style['background'] = 733 '-webkit-canvas(fold_' + faviconIndex + ')'; 734 735 // Use a container so that the fold it self can be zoomed without 736 // changing the positioning of the fold. 737 var foldContainer = createDiv('fold-container'); 738 foldContainer.appendChild(foldDiv); 739 faviconBox.appendChild(foldContainer); 740 741 // FaviconWebUIHandler::HandleGetFaviconDominantColor expects 742 // an URL that starts with chrome://favicon/size/. 743 // The handler always loads 16x16 1x favicon and assumes that 744 // the dominant color for all scale factors is the same. 745 chrome.send('getFaviconDominantColor', 746 [('chrome://favicon/size/16@1x/' + item.url), '' + faviconIndex]); 747 faviconIndex++; 748 } else if ((w == 57 && h == 57) || (w == 114 && h == 114)) { 749 // it's a touch icon for 1x or 2x. 750 faviconIcon.classList.add('touch-icon'); 751 } else { 752 // It's an html5 icon (or at least it's larger). 753 // Rescale it to be no bigger than 64x64 dip. 754 var max = 64; 755 if (w > max || h > max) { 756 var scale = (w > h) ? (max / w) : (max / h); 757 w *= scale; 758 h *= scale; 759 } 760 faviconIcon.style.backgroundSize = w + 'px ' + h + 'px'; 761 } 762 }; 763 faviconBox.appendChild(faviconIcon); 764 } 765 holder.appendChild(faviconBox); 766 767 var title = createDiv('title'); 768 title.textContent = item.title; 769 holder.appendChild(title); 770 771 wrapClickHandler(holder, item, opt_clickCallback); 772 773 return holder; 774 } 775 776 /** 777 * Adds touch listeners to the specified element to apply a class when it is 778 * selected (removing the class when no longer pressed). 779 * 780 * @param {Element} el The element to apply the class to when touched. 781 * @param {string} activeClass The CSS class name to be applied when active. 782 */ 783 function addActiveTouchListener(el, activeClass) { 784 if (!window.touchCancelListener) { 785 window.touchCancelListener = function(evt) { 786 if (activeItemDelayTimerId) { 787 clearTimeout(activeItemDelayTimerId); 788 activeItemDelayTimerId = undefined; 789 } 790 if (!activeItem) { 791 return; 792 } 793 activeItem.classList.remove(activeItem.dataset.activeClass); 794 activeItem = null; 795 }; 796 document.addEventListener('touchcancel', window.touchCancelListener); 797 } 798 el.dataset.activeClass = activeClass; 799 el.addEventListener(PRESS_START_EVT, function(evt) { 800 if (activeItemDelayTimerId) { 801 clearTimeout(activeItemDelayTimerId); 802 activeItemDelayTimerId = undefined; 803 } 804 activeItemDelayTimerId = setTimeout(function() { 805 el.classList.add(activeClass); 806 activeItem = el; 807 }, ACTIVE_ITEM_DELAY_MS); 808 }); 809 el.addEventListener(PRESS_STOP_EVT, function(evt) { 810 if (activeItemDelayTimerId) { 811 clearTimeout(activeItemDelayTimerId); 812 activeItemDelayTimerId = undefined; 813 } 814 // Add the active class to ensure the pressed state is visible when 815 // quickly tapping, which can happen if the start and stop events are 816 // received before the active item delay timer has been executed. 817 el.classList.add(activeClass); 818 el.classList.add('no-active-delay'); 819 setTimeout(function() { 820 el.classList.remove(activeClass); 821 el.classList.remove('no-active-delay'); 822 }, 0); 823 activeItem = null; 824 }); 825 } 826 827 /** 828 * Creates a shortcut DOM element based on the item specified in the list 829 * format. 830 * 831 * @param {Object} item The data of the item used to generate the shortcut. 832 * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The 833 * click callback to be triggered upon selection (if not provided it will 834 * use the default -- itemShortcutClickHandler). 835 * @return {Element} The shortcut element created. 836 */ 837 function makeListEntryItem(item, opt_clickCallback) { 838 var listItem = createDiv('list-item'); 839 addActiveTouchListener(listItem, ACTIVE_LIST_ITEM_CSS_CLASS); 840 listItem.setAttribute(CONTEXT_MENU_URL_KEY, item.url); 841 var iconSize = item.iconSize || 64; 842 var iconUrl = item.icon || 843 'chrome://touch-icon/size/' + iconSize + '@1x/' + item.url; 844 listItem.appendChild(createDiv('icon', iconUrl)); 845 trackImageLoad(iconUrl); 846 var title = createElement('div', { 847 textContent: item.title, 848 className: 'title session_title' 849 }); 850 listItem.appendChild(title); 851 852 listItem.addEventListener('click', function(evt) { 853 var clickCallback = 854 opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler; 855 clickCallback(item, evt); 856 }); 857 if (item.divider == 'section') { 858 // Add a child div because the section divider has a gradient and 859 // webkit doesn't seem to currently support borders with gradients. 860 listItem.appendChild(createDiv('section-divider')); 861 } else { 862 listItem.classList.add('standard-divider'); 863 } 864 return listItem; 865 } 866 867 /** 868 * Creates a DOM list entry for a remote session or tab. 869 * 870 * @param {Object} item The data of the item used to generate the shortcut. 871 * @param {function(Object, string, BrowserEvent)=} opt_clickCallback The 872 * click callback to be triggered upon selection (if not provided it will 873 * use the default -- itemShortcutClickHandler). 874 * @return {Element} The shortcut element created. 875 */ 876 function makeForeignSessionListEntry(item, opt_clickCallback) { 877 // Session item 878 var sessionOuterDiv = createDiv('list-item standard-divider'); 879 addActiveTouchListener(sessionOuterDiv, ACTIVE_LIST_ITEM_CSS_CLASS); 880 sessionOuterDiv.contextMenuItem = item; 881 882 var icon = createDiv('session-icon ' + item.iconStyle); 883 sessionOuterDiv.appendChild(icon); 884 885 var titleContainer = createElement('div', 'title'); 886 sessionOuterDiv.appendChild(titleContainer); 887 888 // Extra container to allow title & last-sync time to stack vertically. 889 var sessionInnerDiv = createDiv('session_container'); 890 titleContainer.appendChild(sessionInnerDiv); 891 892 var title = createDiv('session-name'); 893 title.textContent = item.title; 894 title.id = item.titleId || ''; 895 sessionInnerDiv.appendChild(title); 896 897 var lastSynced = createDiv('session-last-synced'); 898 lastSynced.textContent = 899 templateData.opentabslastsynced + ': ' + item.userVisibleTimestamp; 900 lastSynced.id = item.userVisibleTimestampId || ''; 901 sessionInnerDiv.appendChild(lastSynced); 902 903 sessionOuterDiv.addEventListener('click', function(evt) { 904 var clickCallback = 905 opt_clickCallback ? opt_clickCallback : itemShortcutClickHandler; 906 clickCallback(item, evt); 907 }); 908 return sessionOuterDiv; 909 } 910 911 /** 912 * Saves the number of most visited pages and updates promo visibility. 913 * @param {number} n Number of most visited pages. 914 */ 915 function setNumberOfMostVisitedPages(n) { 916 numberOfMostVisitedPages = n; 917 updatePromoVisibility(); 918 } 919 920 /** 921 * Saves the recently closed tabs flag and updates promo visibility. 922 * @param {boolean} anyTabs Whether there are any recently closed tabs. 923 */ 924 function setHasRecentlyClosedTabs(anyTabs) { 925 hasRecentlyClosedTabs = anyTabs; 926 updatePromoVisibility(); 927 } 928 929 /** 930 * Updates the most visited pages. 931 * 932 * @param {Array.<Object>} List of data for displaying the list of most 933 * visited pages (see C++ handler for model description). 934 * @param {boolean} hasBlacklistedUrls Whether any blacklisted URLs are 935 * present. 936 */ 937 function setMostVisitedPages(data, hasBlacklistedUrls) { 938 setNumberOfMostVisitedPages(data.length); 939 // limit the number of most visited items to display 940 if (isPhone() && data.length > 6) { 941 data.splice(6, data.length - 6); 942 } else if (isTablet() && data.length > 8) { 943 data.splice(8, data.length - 8); 944 } 945 946 data.forEach(function(item, index) { 947 item.mostVisitedIndex = index; 948 }); 949 950 if (equals(data, mostVisitedData_)) 951 return; 952 953 var clickFunction = function(item) { 954 chrome.send('openedMostVisited'); 955 chrome.send('metricsHandler:recordInHistogram', 956 ['NewTabPage.MostVisited', item.mostVisitedIndex, 8]); 957 window.location = item.url; 958 }; 959 populateData(findList('most_visited'), SectionType.MOST_VISITED, data, 960 makeMostVisitedItem, clickFunction); 961 computeDynamicLayout(); 962 963 mostVisitedData_ = data; 964 } 965 966 /** 967 * Updates the recently closed tabs. 968 * 969 * @param {Array.<Object>} List of data for displaying the list of recently 970 * closed tabs (see C++ handler for model description). 971 */ 972 function setRecentlyClosedTabs(data) { 973 var container = $('recently_closed_container'); 974 if (!data || data.length == 0) { 975 // hide the recently closed section if it is empty. 976 container.style.display = 'none'; 977 setHasRecentlyClosedTabs(false); 978 } else { 979 container.style.display = 'block'; 980 setHasRecentlyClosedTabs(true); 981 var decoratorFunc = isPhone() ? makeListEntryItem : 982 makeRecentlyClosedTabletItem; 983 populateData(findList('recently_closed'), SectionType.RECENTLY_CLOSED, 984 data, decoratorFunc, openRecentlyClosedTab); 985 } 986 computeDynamicLayout(); 987 } 988 989 /** 990 * Updates the bookmarks. 991 * 992 * @param {Array.<Object>} List of data for displaying the bookmarks (see 993 * C++ handler for model description). 994 */ 995 function bookmarks(data) { 996 bookmarkFolderId = data.id; 997 if (!replacedInitialState) { 998 history.replaceState( 999 {folderId: bookmarkFolderId, selectedPaneIndex: currentPaneIndex}, 1000 null, null); 1001 replacedInitialState = true; 1002 } 1003 if (syncEnabled == undefined) { 1004 // Wait till we know whether or not sync is enabled before displaying any 1005 // bookmarks (since they may need to be filtered below) 1006 bookmarkData = data; 1007 return; 1008 } 1009 1010 var titleWrapper = $('bookmarks_title_wrapper'); 1011 setBookmarkTitleHierarchy( 1012 titleWrapper, data, data['hierarchy']); 1013 1014 var filteredBookmarks = data.bookmarks; 1015 if (!syncEnabled) { 1016 filteredBookmarks = filteredBookmarks.filter(function(val) { 1017 return (val.type != 'BOOKMARK_BAR' && val.type != 'OTHER_NODE'); 1018 }); 1019 } 1020 if (bookmarkShortcutMode) { 1021 populateData(findList('bookmarks'), SectionType.BOOKMARKS, 1022 filteredBookmarks, makeBookmarkItem); 1023 } else { 1024 var clickFunction = function(item) { 1025 if (item['folder']) { 1026 browseToBookmarkFolder(item.id); 1027 } else if (!!item.url) { 1028 chrome.send('openedBookmark'); 1029 window.location = item.url; 1030 } 1031 }; 1032 populateData(findList('bookmarks'), SectionType.BOOKMARKS, 1033 filteredBookmarks, makeBookmarkItem, clickFunction); 1034 } 1035 1036 var bookmarkContainer = $('bookmarks_container'); 1037 1038 // update the shadows on the breadcrumb bar 1039 computeDynamicLayout(); 1040 1041 if ((loadStatus_ & LoadStatusType.LOAD_BOOKMARKS_FINISHED) != 1042 LoadStatusType.LOAD_BOOKMARKS_FINISHED) { 1043 loadStatus_ |= LoadStatusType.LOAD_BOOKMARKS_FINISHED; 1044 sendNTPNotification(); 1045 } 1046 } 1047 1048 /** 1049 * Checks if promo is allowed and MostVisited requirements are satisfied. 1050 * @return {boolean} Whether the promo should be shown on most_visited. 1051 */ 1052 function shouldPromoBeShownOnMostVisited() { 1053 return promoIsAllowed && promoIsAllowedOnMostVisited && 1054 numberOfMostVisitedPages >= 2 && !hasRecentlyClosedTabs; 1055 } 1056 1057 /** 1058 * Checks if promo is allowed and OpenTabs requirements are satisfied. 1059 * @return {boolean} Whether the promo should be shown on open_tabs. 1060 */ 1061 function shouldPromoBeShownOnOpenTabs() { 1062 var snapshotsCount = 1063 currentSnapshots == null ? 0 : currentSnapshots.length; 1064 var sessionsCount = currentSessions == null ? 0 : currentSessions.length; 1065 return promoIsAllowed && promoIsAllowedOnOpenTabs && 1066 (snapshotsCount + sessionsCount != 0); 1067 } 1068 1069 /** 1070 * Checks if promo is allowed and SyncPromo requirements are satisfied. 1071 * @return {boolean} Whether the promo should be shown on sync_promo. 1072 */ 1073 function shouldPromoBeShownOnSync() { 1074 var snapshotsCount = 1075 currentSnapshots == null ? 0 : currentSnapshots.length; 1076 var sessionsCount = currentSessions == null ? 0 : currentSessions.length; 1077 return promoIsAllowed && promoIsAllowedOnOpenTabs && 1078 (snapshotsCount + sessionsCount == 0); 1079 } 1080 1081 /** 1082 * Records a promo impression on a given section if necessary. 1083 * @param {string} section Active section name to check. 1084 */ 1085 function promoUpdateImpressions(section) { 1086 if (section == 'most_visited' && shouldPromoBeShownOnMostVisited()) 1087 chrome.send('recordImpression', ['most_visited']); 1088 else if (section == 'open_tabs' && shouldPromoBeShownOnOpenTabs()) 1089 chrome.send('recordImpression', ['open_tabs']); 1090 else if (section == 'open_tabs' && shouldPromoBeShownOnSync()) 1091 chrome.send('recordImpression', ['sync_promo']); 1092 } 1093 1094 /** 1095 * Updates the visibility on all promo-related items as necessary. 1096 */ 1097 function updatePromoVisibility() { 1098 var mostVisitedEl = $('promo_message_on_most_visited'); 1099 var openTabsVCEl = $('promo_vc_list'); 1100 var syncPromoLegacyEl = $('promo_message_on_sync_promo_legacy'); 1101 var syncPromoReceivedEl = $('promo_message_on_sync_promo_received'); 1102 mostVisitedEl.style.display = 1103 shouldPromoBeShownOnMostVisited() ? 'block' : 'none'; 1104 syncPromoReceivedEl.style.display = 1105 shouldPromoBeShownOnSync() ? 'block' : 'none'; 1106 syncPromoLegacyEl.style.display = 1107 shouldPromoBeShownOnSync() ? 'none' : 'block'; 1108 openTabsVCEl.style.display = 1109 (shouldPromoBeShownOnOpenTabs() && promoIsAllowedAsVirtualComputer) ? 1110 'block' : 'none'; 1111 } 1112 1113 /** 1114 * Called from native. 1115 * Clears the promotion. 1116 */ 1117 function clearPromotions() { 1118 setPromotions({}); 1119 } 1120 1121 /** 1122 * Set the element to a parsed and sanitized promotion HTML string. 1123 * @param {Element} el The element to set the promotion string to. 1124 * @param {string} html The promotion HTML string. 1125 * @throws {Error} In case of non supported markup. 1126 */ 1127 function setPromotionHtml(el, html) { 1128 if (!el) return; 1129 el.innerHTML = ''; 1130 if (!html) return; 1131 var tags = ['BR', 'DIV', 'BUTTON', 'SPAN']; 1132 var attrs = { 1133 class: function(node, value) { return true; }, 1134 style: function(node, value) { return true; }, 1135 }; 1136 try { 1137 var fragment = parseHtmlSubset(html, tags, attrs); 1138 el.appendChild(fragment); 1139 } catch (err) { 1140 console.error(err.toString()); 1141 // Ignore all errors while parsing or setting the element. 1142 } 1143 } 1144 1145 /** 1146 * Called from native. 1147 * Sets the text for all promo-related items, updates 1148 * promo-send-email-target items to send email on click and 1149 * updates the visibility of items. 1150 * @param {Object} promotions Dictionary used to fill-in the text. 1151 */ 1152 function setPromotions(promotions) { 1153 var mostVisitedEl = $('promo_message_on_most_visited'); 1154 var openTabsEl = $('promo_message_on_open_tabs'); 1155 var syncPromoReceivedEl = $('promo_message_on_sync_promo_received'); 1156 1157 promoIsAllowed = !!promotions.promoIsAllowed; 1158 promoIsAllowedOnMostVisited = !!promotions.promoIsAllowedOnMostVisited; 1159 promoIsAllowedOnOpenTabs = !!promotions.promoIsAllowedOnOpenTabs; 1160 promoIsAllowedAsVirtualComputer = !!promotions.promoIsAllowedAsVC; 1161 1162 setPromotionHtml(mostVisitedEl, promotions.promoMessage); 1163 setPromotionHtml(openTabsEl, promotions.promoMessage); 1164 setPromotionHtml(syncPromoReceivedEl, promotions.promoMessageLong); 1165 1166 promoInjectedComputerTitleText = promotions.promoVCTitle || ''; 1167 promoInjectedComputerLastSyncedText = promotions.promoVCLastSynced || ''; 1168 var openTabsVCTitleEl = $('promo_vc_title'); 1169 if (openTabsVCTitleEl) 1170 openTabsVCTitleEl.textContent = promoInjectedComputerTitleText; 1171 var openTabsVCLastSyncEl = $('promo_vc_lastsync'); 1172 if (openTabsVCLastSyncEl) 1173 openTabsVCLastSyncEl.textContent = promoInjectedComputerLastSyncedText; 1174 1175 if (promoIsAllowed) { 1176 var promoButtonEls = 1177 document.getElementsByClassName('promo-button'); 1178 for (var i = 0, len = promoButtonEls.length; i < len; i++) { 1179 promoButtonEls[i].onclick = executePromoAction; 1180 addActiveTouchListener(promoButtonEls[i], 'promo-button-active'); 1181 } 1182 } 1183 updatePromoVisibility(); 1184 } 1185 1186 /** 1187 * On-click handler for promo email targets. 1188 * Performs the promo action "send email". 1189 * @param {Object} evt User interface event that triggered the action. 1190 */ 1191 function executePromoAction(evt) { 1192 evt.preventDefault(); 1193 chrome.send('promoActionTriggered'); 1194 } 1195 1196 /** 1197 * Called by the browser when a context menu has been selected. 1198 * 1199 * @param {number} itemId The id of the item that was selected, as specified 1200 * when chrome.send('showContextMenu') was called. 1201 */ 1202 function onCustomMenuSelected(itemId) { 1203 if (contextMenuUrl != null) { 1204 switch (itemId) { 1205 case ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB: 1206 case ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB: 1207 chrome.send('openedBookmark'); 1208 break; 1209 1210 case ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB: 1211 case ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB: 1212 chrome.send('openedMostVisited'); 1213 if (contextMenuItem) { 1214 chrome.send('metricsHandler:recordInHistogram', 1215 ['NewTabPage.MostVisited', 1216 contextMenuItem.mostVisitedIndex, 1217 8]); 1218 } 1219 break; 1220 1221 case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB: 1222 case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB: 1223 chrome.send('openedRecentlyClosed'); 1224 break; 1225 } 1226 } 1227 1228 switch (itemId) { 1229 case ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB: 1230 case ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB: 1231 case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB: 1232 if (contextMenuUrl != null) 1233 chrome.send('openInNewTab', [contextMenuUrl]); 1234 break; 1235 1236 case ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB: 1237 case ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB: 1238 case ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB: 1239 if (contextMenuUrl != null) 1240 chrome.send('openInIncognitoTab', [contextMenuUrl]); 1241 break; 1242 1243 case ContextMenuItemIds.BOOKMARK_EDIT: 1244 if (contextMenuItem != null) 1245 chrome.send('editBookmark', [contextMenuItem.id]); 1246 break; 1247 1248 case ContextMenuItemIds.BOOKMARK_DELETE: 1249 if (contextMenuUrl != null) 1250 chrome.send('deleteBookmark', [contextMenuItem.id]); 1251 break; 1252 1253 case ContextMenuItemIds.MOST_VISITED_REMOVE: 1254 if (contextMenuUrl != null) 1255 chrome.send('blacklistURLFromMostVisited', [contextMenuUrl]); 1256 break; 1257 1258 case ContextMenuItemIds.BOOKMARK_SHORTCUT: 1259 if (contextMenuUrl != null) 1260 chrome.send('createHomeScreenBookmarkShortcut', [contextMenuItem.id]); 1261 break; 1262 1263 case ContextMenuItemIds.RECENTLY_CLOSED_REMOVE: 1264 chrome.send('clearRecentlyClosed'); 1265 break; 1266 1267 case ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE: 1268 if (contextMenuItem != null) { 1269 chrome.send( 1270 'deleteForeignSession', [contextMenuItem.sessionTag]); 1271 chrome.send('getForeignSessions'); 1272 } 1273 break; 1274 1275 case ContextMenuItemIds.PROMO_VC_SESSION_REMOVE: 1276 chrome.send('promoDisabled'); 1277 break; 1278 1279 default: 1280 log.error('Unknown context menu selected id=' + itemId); 1281 break; 1282 } 1283 } 1284 1285 /** 1286 * Generates the full bookmark folder hierarchy and populates the scrollable 1287 * title element. 1288 * 1289 * @param {Element} wrapperEl The wrapper element containing the scrollable 1290 * title. 1291 * @param {string} data The current bookmark folder node. 1292 * @param {Array.<Object>=} opt_ancestry The folder ancestry of the current 1293 * bookmark folder. The list is ordered in order of closest descendant 1294 * (the root will always be the last node). The definition of each 1295 * element is: 1296 * - id {number}: Unique ID of the folder (N/A for root node). 1297 * - name {string}: Name of the folder (N/A for root node). 1298 * - root {boolean}: Whether this is the root node. 1299 */ 1300 function setBookmarkTitleHierarchy(wrapperEl, data, opt_ancestry) { 1301 var title = wrapperEl.getElementsByClassName('section-title')[0]; 1302 title.innerHTML = ''; 1303 if (opt_ancestry) { 1304 for (var i = opt_ancestry.length - 1; i >= 0; i--) { 1305 var titleCrumb = createBookmarkTitleCrumb_(opt_ancestry[i]); 1306 title.appendChild(titleCrumb); 1307 title.appendChild(createDiv('bookmark-separator')); 1308 } 1309 } 1310 var titleCrumb = createBookmarkTitleCrumb_(data); 1311 titleCrumb.classList.add('title-crumb-active'); 1312 title.appendChild(titleCrumb); 1313 1314 // Ensure the last crumb is as visible as possible. 1315 var windowWidth = 1316 wrapperEl.getElementsByClassName('section-title-mask')[0].offsetWidth; 1317 var crumbWidth = titleCrumb.offsetWidth; 1318 var leftOffset = titleCrumb.offsetLeft; 1319 1320 var shiftLeft = windowWidth - crumbWidth - leftOffset; 1321 if (shiftLeft < 0) { 1322 if (crumbWidth > windowWidth) 1323 shifLeft = -leftOffset; 1324 1325 // Queue up the scrolling initially to allow for the mask element to 1326 // be placed into the dom and it's size correctly calculated. 1327 setTimeout(function() { 1328 handleTitleScroll(wrapperEl, shiftLeft); 1329 }, 0); 1330 } else { 1331 handleTitleScroll(wrapperEl, 0); 1332 } 1333 } 1334 1335 /** 1336 * Creates a clickable bookmark title crumb. 1337 * @param {Object} data The crumb data (see setBookmarkTitleHierarchy for 1338 * definition of the data object). 1339 * @return {Element} The clickable title crumb element. 1340 * @private 1341 */ 1342 function createBookmarkTitleCrumb_(data) { 1343 var titleCrumb = createDiv('title-crumb'); 1344 if (data.root) { 1345 titleCrumb.innerText = templateData.bookmarkstitle; 1346 } else { 1347 titleCrumb.innerText = data.title; 1348 } 1349 titleCrumb.addEventListener('click', function(evt) { 1350 browseToBookmarkFolder(data.root ? '0' : data.id); 1351 }); 1352 return titleCrumb; 1353 } 1354 1355 /** 1356 * Handles scrolling a title element. 1357 * @param {Element} wrapperEl The wrapper element containing the scrollable 1358 * title. 1359 * @param {number} scrollPosition The position to be scrolled to. 1360 */ 1361 function handleTitleScroll(wrapperEl, scrollPosition) { 1362 var overflowLeftMask = 1363 wrapperEl.getElementsByClassName('overflow-left-mask')[0]; 1364 var overflowRightMask = 1365 wrapperEl.getElementsByClassName('overflow-right-mask')[0]; 1366 var title = wrapperEl.getElementsByClassName('section-title')[0]; 1367 var titleMask = wrapperEl.getElementsByClassName('section-title-mask')[0]; 1368 var titleWidth = title.scrollWidth; 1369 var containerWidth = titleMask.offsetWidth; 1370 1371 var maxRightScroll = containerWidth - titleWidth; 1372 var boundedScrollPosition = 1373 Math.max(maxRightScroll, Math.min(scrollPosition, 0)); 1374 1375 overflowLeftMask.style.opacity = 1376 Math.min( 1377 1, 1378 (Math.max(0, -boundedScrollPosition)) + 10 / 30); 1379 1380 overflowRightMask.style.opacity = 1381 Math.min( 1382 1, 1383 (Math.max(0, boundedScrollPosition - maxRightScroll) + 10) / 30); 1384 1385 // Set the position of the title. 1386 if (titleWidth < containerWidth) { 1387 // left-align on LTR and right-align on RTL. 1388 title.style.left = ''; 1389 } else { 1390 title.style.left = boundedScrollPosition + 'px'; 1391 } 1392 } 1393 1394 /** 1395 * Initializes a scrolling title element. 1396 * @param {Element} wrapperEl The wrapper element of the scrolling title. 1397 */ 1398 function initializeTitleScroller(wrapperEl) { 1399 var title = wrapperEl.getElementsByClassName('section-title')[0]; 1400 1401 var inTitleScroll = false; 1402 var startingScrollPosition; 1403 var startingOffset; 1404 wrapperEl.addEventListener(PRESS_START_EVT, function(evt) { 1405 inTitleScroll = true; 1406 startingScrollPosition = getTouchEventX(evt); 1407 startingOffset = title.offsetLeft; 1408 }); 1409 document.body.addEventListener(PRESS_STOP_EVT, function(evt) { 1410 if (!inTitleScroll) 1411 return; 1412 inTitleScroll = false; 1413 }); 1414 document.body.addEventListener(PRESS_MOVE_EVT, function(evt) { 1415 if (!inTitleScroll) 1416 return; 1417 handleTitleScroll( 1418 wrapperEl, 1419 startingOffset - (startingScrollPosition - getTouchEventX(evt))); 1420 evt.stopPropagation(); 1421 }); 1422 } 1423 1424 /** 1425 * Handles updates from the underlying bookmark model (calls originate 1426 * in the WebUI handler for bookmarks). 1427 * 1428 * @param {Object} status Describes the type of change that occurred. Can 1429 * contain the following fields: 1430 * - parent_id {string}: Unique id of the parent that was affected by 1431 * the change. If the parent is the bookmark 1432 * bar, then the ID will be 'root'. 1433 * - node_id {string}: The unique ID of the node that was affected. 1434 */ 1435 function bookmarkChanged(status) { 1436 if (status) { 1437 var affectedParentNode = status['parent_id']; 1438 var affectedNodeId = status['node_id']; 1439 var shouldUpdate = (bookmarkFolderId == affectedParentNode || 1440 bookmarkFolderId == affectedNodeId); 1441 if (shouldUpdate) 1442 setCurrentBookmarkFolderData(bookmarkFolderId); 1443 } else { 1444 // This typically happens when extensive changes could have happened to 1445 // the model, such as initial load, import and sync. 1446 setCurrentBookmarkFolderData(bookmarkFolderId); 1447 } 1448 } 1449 1450 /** 1451 * Loads the bookarks data for a given folder. 1452 * 1453 * @param {string|number} folderId The ID of the folder to load (or null if 1454 * it should load the root folder). 1455 */ 1456 function setCurrentBookmarkFolderData(folderId) { 1457 if (folderId != null) { 1458 chrome.send('getBookmarks', [folderId]); 1459 } else { 1460 chrome.send('getBookmarks'); 1461 } 1462 try { 1463 if (folderId == null) { 1464 localStorage.removeItem(DEFAULT_BOOKMARK_FOLDER_KEY); 1465 } else { 1466 localStorage.setItem(DEFAULT_BOOKMARK_FOLDER_KEY, folderId); 1467 } 1468 } catch (e) {} 1469 } 1470 1471 /** 1472 * Navigates to the specified folder and handles loading the required data. 1473 * Ensures the current folder can be navigated back to using the browser 1474 * controls. 1475 * 1476 * @param {string|number} folderId The ID of the folder to navigate to. 1477 */ 1478 function browseToBookmarkFolder(folderId) { 1479 history.pushState( 1480 {folderId: folderId, selectedPaneIndex: currentPaneIndex}, 1481 null, null); 1482 setCurrentBookmarkFolderData(folderId); 1483 } 1484 1485 /** 1486 * Called to inform the page of the current sync status. If the state has 1487 * changed from disabled to enabled, it changes the current and default 1488 * bookmark section to the root directory. This makes desktop bookmarks are 1489 * visible. 1490 */ 1491 function setSyncEnabled(enabled) { 1492 try { 1493 if (syncEnabled != undefined && syncEnabled == enabled) { 1494 // The value didn't change 1495 return; 1496 } 1497 syncEnabled = enabled; 1498 1499 if (enabled) { 1500 if (!localStorage.getItem(SYNC_ENABLED_KEY)) { 1501 localStorage.setItem(SYNC_ENABLED_KEY, 'true'); 1502 setCurrentBookmarkFolderData('0'); 1503 } 1504 } else { 1505 localStorage.removeItem(SYNC_ENABLED_KEY); 1506 } 1507 updatePromoVisibility(); 1508 1509 if (bookmarkData) { 1510 // Bookmark data can now be displayed (or needs to be refiltered) 1511 bookmarks(bookmarkData); 1512 } 1513 1514 updateSyncEmptyState(); 1515 } catch (e) {} 1516 } 1517 1518 /** 1519 * Handles adding or removing the 'nothing to see here' text from the session 1520 * list depending on the state of snapshots and sessions. 1521 * 1522 * @param {boolean} Whether the call is occuring because of a schedule 1523 * timeout. 1524 */ 1525 function updateSyncEmptyState(timeout) { 1526 if (syncState == SyncState.DISPLAYING_LOADING && !timeout) { 1527 // Make sure 'Loading...' is displayed long enough 1528 return; 1529 } 1530 1531 var openTabsList = findList('open_tabs'); 1532 var snapshotsList = findList('snapshots'); 1533 var syncPromo = $('sync_promo'); 1534 var syncLoading = $('sync_loading'); 1535 var syncEnableSync = $('sync_enable_sync'); 1536 1537 if (syncEnabled == undefined || 1538 currentSnapshots == null || 1539 currentSessions == null) { 1540 if (syncState == SyncState.INITIAL) { 1541 // Wait one second for sync data to come in before displaying loading 1542 // text. 1543 syncState = SyncState.WAITING_FOR_DATA; 1544 syncTimerId = setTimeout(function() { updateSyncEmptyState(true); }, 1545 SYNC_INITIAL_LOAD_TIMEOUT); 1546 } else if (syncState == SyncState.WAITING_FOR_DATA && timeout) { 1547 // We've waited for the initial info timeout to pass and still don't 1548 // have data. So, display loading text so the user knows something is 1549 // happening. 1550 syncState = SyncState.DISPLAYING_LOADING; 1551 syncLoading.style.display = '-webkit-box'; 1552 centerEmptySections(syncLoading); 1553 syncTimerId = setTimeout(function() { updateSyncEmptyState(true); }, 1554 SYNC_LOADING_TIMEOUT); 1555 } else if (syncState == SyncState.DISPLAYING_LOADING) { 1556 // Allow the Loading... text to go away once data comes in 1557 syncState = SyncState.DISPLAYED_LOADING; 1558 } 1559 return; 1560 } 1561 1562 if (syncTimerId != -1) { 1563 clearTimeout(syncTimerId); 1564 syncTimerId = -1; 1565 } 1566 syncState = SyncState.LOADED; 1567 1568 // Hide everything by default, display selectively below 1569 syncEnableSync.style.display = 'none'; 1570 syncLoading.style.display = 'none'; 1571 syncPromo.style.display = 'none'; 1572 1573 var snapshotsCount = 1574 currentSnapshots == null ? 0 : currentSnapshots.length; 1575 var sessionsCount = currentSessions == null ? 0 : currentSessions.length; 1576 1577 if (!syncEnabled) { 1578 syncEnableSync.style.display = '-webkit-box'; 1579 centerEmptySections(syncEnableSync); 1580 } else if (sessionsCount + snapshotsCount == 0) { 1581 syncPromo.style.display = '-webkit-box'; 1582 centerEmptySections(syncPromo); 1583 } else { 1584 openTabsList.style.display = sessionsCount == 0 ? 'none' : 'block'; 1585 snapshotsList.style.display = snapshotsCount == 0 ? 'none' : 'block'; 1586 } 1587 updatePromoVisibility(); 1588 } 1589 1590 /** 1591 * Called externally when updated snapshot data is available. 1592 * 1593 * @param {Object} data The snapshot data 1594 */ 1595 function snapshots(data) { 1596 var list = findList('snapshots'); 1597 list.innerHTML = ''; 1598 1599 currentSnapshots = data; 1600 updateSyncEmptyState(); 1601 1602 if (!data || data.length == 0) 1603 return; 1604 1605 data.sort(function(a, b) { 1606 return b.createTime - a.createTime; 1607 }); 1608 1609 // Create the main container 1610 var snapshotsEl = createElement('div'); 1611 list.appendChild(snapshotsEl); 1612 1613 // Create the header container 1614 var headerEl = createDiv('session-header'); 1615 snapshotsEl.appendChild(headerEl); 1616 1617 // Create the documents container 1618 var docsEl = createDiv('session-children-container'); 1619 snapshotsEl.appendChild(docsEl); 1620 1621 // Create the container for the title & icon 1622 var headerInnerEl = createDiv('list-item standard-divider'); 1623 addActiveTouchListener(headerInnerEl, ACTIVE_LIST_ITEM_CSS_CLASS); 1624 headerEl.appendChild(headerInnerEl); 1625 1626 // Create the header icon 1627 headerInnerEl.appendChild(createDiv('session-icon documents')); 1628 1629 // Create the header title 1630 var titleContainer = createElement('span', 'title'); 1631 headerInnerEl.appendChild(titleContainer); 1632 var title = createDiv('session-name'); 1633 title.textContent = templateData.receivedDocuments; 1634 titleContainer.appendChild(title); 1635 1636 // Add support for expanding and collapsing the children 1637 var expando = createDiv(); 1638 var expandoFunction = createExpandoFunction(expando, docsEl); 1639 headerInnerEl.addEventListener('click', expandoFunction); 1640 headerEl.appendChild(expando); 1641 1642 // Support for actually opening the document 1643 var snapshotClickCallback = function(item) { 1644 if (!item) 1645 return; 1646 if (item.snapshotId) { 1647 window.location = 'chrome://snapshot/' + item.snapshotId; 1648 } else if (item.printJobId) { 1649 window.location = 'chrome://printjob/' + item.printJobId; 1650 } else { 1651 window.location = item.url; 1652 } 1653 } 1654 1655 // Finally, add the list of documents 1656 populateData(docsEl, SectionType.SNAPSHOTS, data, 1657 makeListEntryItem, snapshotClickCallback); 1658 } 1659 1660 /** 1661 * Create a function to handle expanding and collapsing a section 1662 * 1663 * @param {Element} expando The expando div 1664 * @param {Element} element The element to expand and collapse 1665 * @return {function()} A callback function that should be invoked when the 1666 * expando is clicked 1667 */ 1668 function createExpandoFunction(expando, element) { 1669 expando.className = 'expando open'; 1670 return function() { 1671 if (element.style.height != '0px') { 1672 // It seems that '-webkit-transition' only works when explicit pixel 1673 // values are used. 1674 setTimeout(function() { 1675 // If this is the first time to collapse the list, store off the 1676 // expanded height and also set the height explicitly on the style. 1677 if (!element.expandedHeight) { 1678 element.expandedHeight = 1679 element.clientHeight + 'px'; 1680 element.style.height = element.expandedHeight; 1681 } 1682 // Now set the height to 0. Note, this is also done in a callback to 1683 // give the layout engine a chance to run after possibly setting the 1684 // height above. 1685 setTimeout(function() { 1686 element.style.height = '0px'; 1687 }, 0); 1688 }, 0); 1689 expando.className = 'expando closed'; 1690 } else { 1691 element.style.height = element.expandedHeight; 1692 expando.className = 'expando open'; 1693 } 1694 } 1695 } 1696 1697 /** 1698 * Initializes the promo_vc_list div to look like a foreign session 1699 * with a desktop. 1700 */ 1701 function createPromoVirtualComputers() { 1702 var list = findList('promo_vc'); 1703 list.innerHTML = ''; 1704 1705 // Set up the container and the "virtual computer" session header. 1706 var sessionEl = createDiv(); 1707 list.appendChild(sessionEl); 1708 var sessionHeader = createDiv('session-header'); 1709 sessionEl.appendChild(sessionHeader); 1710 1711 // Set up the session children container and the promo as a child. 1712 var sessionChildren = createDiv('session-children-container'); 1713 var promoMessage = createDiv('promo-message'); 1714 promoMessage.id = 'promo_message_on_open_tabs'; 1715 sessionChildren.appendChild(promoMessage); 1716 sessionEl.appendChild(sessionChildren); 1717 1718 // Add support for expanding and collapsing the children. 1719 var expando = createDiv(); 1720 var expandoFunction = createExpandoFunction(expando, sessionChildren); 1721 1722 // Fill-in the contents of the "virtual computer" session header. 1723 var headerList = [{ 1724 'title': promoInjectedComputerTitleText, 1725 'titleId': 'promo_vc_title', 1726 'userVisibleTimestamp': promoInjectedComputerLastSyncedText, 1727 'userVisibleTimestampId': 'promo_vc_lastsync', 1728 'iconStyle': 'laptop' 1729 }]; 1730 1731 populateData(sessionHeader, SectionType.PROMO_VC_SESSION_HEADER, headerList, 1732 makeForeignSessionListEntry, expandoFunction); 1733 sessionHeader.appendChild(expando); 1734 } 1735 1736 /** 1737 * Called externally when updated synced sessions data is available. 1738 * 1739 * @param {Object} data The snapshot data 1740 */ 1741 function setForeignSessions(data, tabSyncEnabled) { 1742 var list = findList('open_tabs'); 1743 list.innerHTML = ''; 1744 1745 currentSessions = data; 1746 updateSyncEmptyState(); 1747 1748 // Sort the windows within each client such that more recently 1749 // modified windows appear first. 1750 data.forEach(function(client) { 1751 if (client.windows != null) { 1752 client.windows.sort(function(a, b) { 1753 if (b.timestamp == null) { 1754 return -1; 1755 } else if (a.timestamp == null) { 1756 return 1; 1757 } else { 1758 return b.timestamp - a.timestamp; 1759 } 1760 }); 1761 } 1762 }); 1763 1764 // Sort so more recently modified clients appear first. 1765 data.sort(function(aClient, bClient) { 1766 var aWindows = aClient.windows; 1767 var bWindows = bClient.windows; 1768 if (bWindows == null || bWindows.length == 0 || 1769 bWindows[0].timestamp == null) { 1770 return -1; 1771 } else if (aWindows == null || aWindows.length == 0 || 1772 aWindows[0].timestamp == null) { 1773 return 1; 1774 } else { 1775 return bWindows[0].timestamp - aWindows[0].timestamp; 1776 } 1777 }); 1778 1779 data.forEach(function(client, clientNum) { 1780 1781 var windows = client.windows; 1782 if (windows == null || windows.length == 0) 1783 return; 1784 1785 // Set up the container for the session header 1786 var sessionEl = createElement('div'); 1787 list.appendChild(sessionEl); 1788 var sessionHeader = createDiv('session-header'); 1789 sessionEl.appendChild(sessionHeader); 1790 1791 // Set up the container for the session children 1792 var sessionChildren = createDiv('session-children-container'); 1793 sessionEl.appendChild(sessionChildren); 1794 1795 var clientName = 'Client ' + clientNum; 1796 if (client.name) 1797 clientName = client.name; 1798 1799 var iconStyle; 1800 var deviceType = client.deviceType; 1801 if (deviceType == 'win' || 1802 deviceType == 'macosx' || 1803 deviceType == 'linux' || 1804 deviceType == 'chromeos' || 1805 deviceType == 'other') { 1806 iconStyle = 'laptop'; 1807 } else if (deviceType == 'phone') { 1808 iconStyle = 'phone'; 1809 } else if (deviceType == 'tablet') { 1810 iconStyle = 'tablet'; 1811 } else { 1812 console.error('Unknown sync device type found: ', deviceType); 1813 iconStyle = 'laptop'; 1814 } 1815 var headerList = [{ 1816 'title': clientName, 1817 'userVisibleTimestamp': windows[0].userVisibleTimestamp, 1818 'iconStyle': iconStyle, 1819 'sessionTag': client.tag, 1820 }]; 1821 1822 var expando = createDiv(); 1823 var expandoFunction = createExpandoFunction(expando, sessionChildren); 1824 populateData(sessionHeader, SectionType.FOREIGN_SESSION_HEADER, 1825 headerList, makeForeignSessionListEntry, expandoFunction); 1826 sessionHeader.appendChild(expando); 1827 1828 // Populate the session children container 1829 var openTabsList = new Array(); 1830 for (var winNum = 0; winNum < windows.length; winNum++) { 1831 win = windows[winNum]; 1832 var tabs = win.tabs; 1833 for (var tabNum = 0; tabNum < tabs.length; tabNum++) { 1834 var tab = tabs[tabNum]; 1835 // If this is the last tab in the window and there are more windows, 1836 // use a section divider. 1837 var needSectionDivider = 1838 (tabNum + 1 == tabs.length) && (winNum + 1 < windows.length); 1839 tab.icon = tab.icon || 'chrome://favicon/size/16@1x/' + tab.url; 1840 1841 openTabsList.push({ 1842 timestamp: tab.timestamp, 1843 title: tab.title, 1844 url: tab.url, 1845 sessionTag: client.tag, 1846 winNum: winNum, 1847 sessionId: tab.sessionId, 1848 icon: tab.icon, 1849 iconSize: 16, 1850 divider: needSectionDivider ? 'section' : 'standard', 1851 }); 1852 } 1853 } 1854 var tabCallback = function(item, evt) { 1855 var buttonIndex = 0; 1856 var altKeyPressed = false; 1857 var ctrlKeyPressed = false; 1858 var metaKeyPressed = false; 1859 var shiftKeyPressed = false; 1860 if (evt instanceof MouseEvent) { 1861 buttonIndex = evt.button; 1862 altKeyPressed = evt.altKey; 1863 ctrlKeyPressed = evt.ctrlKey; 1864 metaKeyPressed = evt.metaKey; 1865 shiftKeyPressed = evt.shiftKey; 1866 } 1867 chrome.send('openedForeignSession'); 1868 chrome.send('openForeignSession', [String(item.sessionTag), 1869 String(item.winNum), String(item.sessionId), buttonIndex, 1870 altKeyPressed, ctrlKeyPressed, metaKeyPressed, shiftKeyPressed]); 1871 }; 1872 populateData(sessionChildren, SectionType.FOREIGN_SESSION, openTabsList, 1873 makeListEntryItem, tabCallback); 1874 }); 1875 } 1876 1877 /** 1878 * Updates the dominant favicon color for a given index. 1879 * 1880 * @param {number} index The index of the favicon whose dominant color is 1881 * being specified. 1882 * @param {string} color The string encoded color. 1883 */ 1884 function setFaviconDominantColor(index, color) { 1885 var colorstrips = document.getElementsByClassName('colorstrip-' + index); 1886 for (var i = 0; i < colorstrips.length; i++) 1887 colorstrips[i].style.background = color; 1888 1889 var id = 'fold_' + index; 1890 var fold = $(id); 1891 if (!fold) 1892 return; 1893 var zoom = window.getComputedStyle(fold).zoom; 1894 var scale = 1 / window.getComputedStyle(fold).zoom; 1895 1896 // The width/height of the canvas. Set to 24 so it looks good across all 1897 // resolutions. 1898 var cw = 24; 1899 var ch = 24; 1900 1901 // Get the fold canvas and create a path for the fold shape 1902 var ctx = document.getCSSCanvasContext( 1903 '2d', 'fold_' + index, cw * scale, ch * scale); 1904 ctx.beginPath(); 1905 ctx.moveTo(0, 0); 1906 ctx.lineTo(0, ch * 0.75 * scale); 1907 ctx.quadraticCurveTo( 1908 0, ch * scale, 1909 cw * .25 * scale, ch * scale); 1910 ctx.lineTo(cw * scale, ch * scale); 1911 ctx.closePath(); 1912 1913 // Create a gradient for the fold and fill it 1914 var gradient = ctx.createLinearGradient(cw * scale, 0, 0, ch * scale); 1915 if (color.indexOf('#') == 0) { 1916 var r = parseInt(color.substring(1, 3), 16); 1917 var g = parseInt(color.substring(3, 5), 16); 1918 var b = parseInt(color.substring(5, 7), 16); 1919 gradient.addColorStop(0, 'rgba(' + r + ', ' + g + ', ' + b + ', 0.6)'); 1920 } else { 1921 // assume the color is in the 'rgb(#, #, #)' format 1922 var rgbBase = color.substring(4, color.length - 1); 1923 gradient.addColorStop(0, 'rgba(' + rgbBase + ', 0.6)'); 1924 } 1925 gradient.addColorStop(1, color); 1926 ctx.fillStyle = gradient; 1927 ctx.fill(); 1928 1929 // Stroke the fold 1930 ctx.lineWidth = Math.floor(scale); 1931 ctx.strokeStyle = color; 1932 ctx.stroke(); 1933 ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'; 1934 ctx.stroke(); 1935 1936 } 1937 1938 /** 1939 * Finds the list element corresponding to the given name. 1940 * @param {string} name The name prefix of the DOM element (<prefix>_list). 1941 * @return {Element} The list element corresponding with the name. 1942 */ 1943 function findList(name) { 1944 return $(name + '_list'); 1945 } 1946 1947 /** 1948 * Render the given data into the given list, and hide or show the entire 1949 * container based on whether there are any elements. The decorator function 1950 * is used to create the element to be inserted based on the given data 1951 * object. 1952 * 1953 * @param {holder} The dom element that the generated list items will be put 1954 * into. 1955 * @param {SectionType} section The section that data is for. 1956 * @param {Object} data The data to be populated. 1957 * @param {function(Object, boolean)} decorator The function that will 1958 * handle decorating each item in the data. 1959 * @param {function(Object, Object)} opt_clickCallback The function that is 1960 * called when the item is clicked. 1961 */ 1962 function populateData(holder, section, data, decorator, 1963 opt_clickCallback) { 1964 // Empty other items in the list, if present. 1965 holder.innerHTML = ''; 1966 var fragment = document.createDocumentFragment(); 1967 if (!data || data.length == 0) { 1968 fragment.innerHTML = ''; 1969 } else { 1970 data.forEach(function(item) { 1971 var el = decorator(item, opt_clickCallback); 1972 el.setAttribute(SECTION_KEY, section); 1973 el.id = section + fragment.childNodes.length; 1974 fragment.appendChild(el); 1975 }); 1976 } 1977 holder.appendChild(fragment); 1978 if (holder.classList.contains(GRID_CSS_CLASS)) 1979 centerGrid(holder); 1980 centerEmptySections(holder); 1981 } 1982 1983 /** 1984 * Given an element containing a list of child nodes arranged in 1985 * a grid, this will center the grid in the window based on the 1986 * remaining space. 1987 * @param {Element} el Container holding the grid cell items. 1988 */ 1989 function centerGrid(el) { 1990 var childEl = el.firstChild; 1991 if (!childEl) 1992 return; 1993 1994 // Find the element to actually set the margins on. 1995 var toCenter = el; 1996 var curEl = toCenter; 1997 while (curEl && curEl.classList) { 1998 if (curEl.classList.contains(GRID_CENTER_CSS_CLASS)) { 1999 toCenter = curEl; 2000 break; 2001 } 2002 curEl = curEl.parentNode; 2003 } 2004 var setItemMargins = el.classList.contains(GRID_SET_ITEM_MARGINS); 2005 var itemWidth = getItemWidth(childEl, setItemMargins); 2006 var windowWidth = document.documentElement.offsetWidth; 2007 if (itemWidth >= windowWidth) { 2008 toCenter.style.paddingLeft = '0'; 2009 toCenter.style.paddingRight = '0'; 2010 } else { 2011 var numColumns = el.getAttribute(GRID_COLUMNS); 2012 if (numColumns) { 2013 numColumns = parseInt(numColumns); 2014 } else { 2015 numColumns = Math.floor(windowWidth / itemWidth); 2016 } 2017 2018 if (setItemMargins) { 2019 // In this case, try to size each item to fill as much space as 2020 // possible. 2021 var gutterSize = 2022 (windowWidth - itemWidth * numColumns) / (numColumns + 1); 2023 var childLeftMargin = Math.round(gutterSize / 2); 2024 var childRightMargin = Math.floor(gutterSize - childLeftMargin); 2025 var children = el.childNodes; 2026 for (var i = 0; i < children.length; i++) { 2027 children[i].style.marginLeft = childLeftMargin + 'px'; 2028 children[i].style.marginRight = childRightMargin + 'px'; 2029 } 2030 itemWidth += childLeftMargin + childRightMargin; 2031 } 2032 2033 var remainder = windowWidth - itemWidth * numColumns; 2034 var leftPadding = Math.round(remainder / 2); 2035 var rightPadding = Math.floor(remainder - leftPadding); 2036 toCenter.style.paddingLeft = leftPadding + 'px'; 2037 toCenter.style.paddingRight = rightPadding + 'px'; 2038 2039 if (toCenter.classList.contains(GRID_SET_TOP_MARGIN_CLASS)) { 2040 var childStyle = window.getComputedStyle(childEl); 2041 var childLeftPadding = parseInt( 2042 childStyle.getPropertyValue('padding-left')); 2043 toCenter.style.paddingTop = 2044 (childLeftMargin + childLeftPadding + leftPadding) + 'px'; 2045 } 2046 } 2047 } 2048 2049 /** 2050 * Finds and centers all child grid elements for a given node (the grids 2051 * do not need to be direct descendants and can reside anywhere in the node 2052 * hierarchy). 2053 * @param {Element} el The node containing the grid child nodes. 2054 */ 2055 function centerChildGrids(el) { 2056 var grids = el.getElementsByClassName(GRID_CSS_CLASS); 2057 for (var i = 0; i < grids.length; i++) 2058 centerGrid(grids[i]); 2059 } 2060 2061 /** 2062 * Finds and vertically centers all 'empty' elements for a given node (the 2063 * 'empty' elements do not need to be direct descendants and can reside 2064 * anywhere in the node hierarchy). 2065 * @param {Element} el The node containing the 'empty' child nodes. 2066 */ 2067 function centerEmptySections(el) { 2068 if (el.classList && 2069 el.classList.contains(CENTER_EMPTY_CONTAINER_CSS_CLASS)) { 2070 centerEmptySection(el); 2071 } 2072 var empties = el.getElementsByClassName(CENTER_EMPTY_CONTAINER_CSS_CLASS); 2073 for (var i = 0; i < empties.length; i++) { 2074 centerEmptySection(empties[i]); 2075 } 2076 } 2077 2078 /** 2079 * Set the top of the given element to the top of the parent and set the 2080 * height to (bottom of document - top). 2081 * 2082 * @param {Element} el Container holding the centered content. 2083 */ 2084 function centerEmptySection(el) { 2085 var parent = el.parentNode; 2086 var top = parent.offsetTop; 2087 var bottom = ( 2088 document.documentElement.offsetHeight - getButtonBarPadding()); 2089 el.style.height = (bottom - top) + 'px'; 2090 el.style.top = top + 'px'; 2091 } 2092 2093 /** 2094 * Finds the index of the panel specified by its prefix. 2095 * @param {string} The string prefix for the panel. 2096 * @return {number} The index of the panel. 2097 */ 2098 function getPaneIndex(panePrefix) { 2099 var pane = $(panePrefix + '_container'); 2100 2101 if (pane != null) { 2102 var index = panes.indexOf(pane); 2103 2104 if (index >= 0) 2105 return index; 2106 } 2107 return 0; 2108 } 2109 2110 /** 2111 * Finds the index of the panel specified by location hash. 2112 * @return {number} The index of the panel. 2113 */ 2114 function getPaneIndexFromHash() { 2115 var paneIndex; 2116 if (window.location.hash == '#bookmarks') { 2117 paneIndex = getPaneIndex('bookmarks'); 2118 } else if (window.location.hash == '#bookmark_shortcut') { 2119 paneIndex = getPaneIndex('bookmarks'); 2120 } else if (window.location.hash == '#most_visited') { 2121 paneIndex = getPaneIndex('most_visited'); 2122 } else if (window.location.hash == '#open_tabs') { 2123 paneIndex = getPaneIndex('open_tabs'); 2124 } else if (window.location.hash == '#incognito') { 2125 paneIndex = getPaneIndex('incognito'); 2126 } else { 2127 // Couldn't find a good section 2128 paneIndex = -1; 2129 } 2130 return paneIndex; 2131 } 2132 2133 /** 2134 * Selects a pane from the top level list (Most Visited, Bookmarks, etc...). 2135 * @param {number} paneIndex The index of the pane to be selected. 2136 * @return {boolean} Whether the selected pane has changed. 2137 */ 2138 function scrollToPane(paneIndex) { 2139 var pane = panes[paneIndex]; 2140 2141 if (pane == currentPane) 2142 return false; 2143 2144 var newHash = '#' + sectionPrefixes[paneIndex]; 2145 // If updated hash matches the current one in the URL, we need to call 2146 // updatePaneOnHash directly as updating the hash to the same value will 2147 // not trigger the 'hashchange' event. 2148 if (bookmarkShortcutMode || newHash == document.location.hash) 2149 updatePaneOnHash(); 2150 computeDynamicLayout(); 2151 promoUpdateImpressions(sectionPrefixes[paneIndex]); 2152 return true; 2153 } 2154 2155 /** 2156 * Updates the pane based on the current hash. 2157 */ 2158 function updatePaneOnHash() { 2159 var paneIndex = getPaneIndexFromHash(); 2160 var pane = panes[paneIndex]; 2161 2162 if (currentPane) 2163 currentPane.classList.remove('selected'); 2164 pane.classList.add('selected'); 2165 currentPane = pane; 2166 currentPaneIndex = paneIndex; 2167 2168 setScrollTopForDocument(document, 0); 2169 2170 var panelPrefix = sectionPrefixes[paneIndex]; 2171 var title = templateData[panelPrefix + '_document_title']; 2172 if (!title) 2173 title = templateData['title']; 2174 document.title = title; 2175 2176 sendNTPTitleLoadedNotification(); 2177 2178 // TODO (dtrainor): Could potentially add logic to reset the bookmark state 2179 // if they are moving to that pane. This logic was in there before, but 2180 // was removed due to the fact that we have to go to this pane as part of 2181 // the history navigation. 2182 } 2183 2184 /** 2185 * Adds a top level section to the NTP. 2186 * @param {string} panelPrefix The prefix of the element IDs corresponding 2187 * to the container of the content. 2188 * @param {boolean=} opt_canBeDefault Whether this section can be marked as 2189 * the default starting point for subsequent instances of the NTP. The 2190 * default value for this is true. 2191 */ 2192 function addMainSection(panelPrefix) { 2193 var paneEl = $(panelPrefix + '_container'); 2194 var paneIndex = panes.push(paneEl) - 1; 2195 sectionPrefixes.push(panelPrefix); 2196 } 2197 2198 /** 2199 * Handles the dynamic layout of the components on the new tab page. Only 2200 * layouts that require calculation based on the screen size should go in 2201 * this function as it will be called during all resize changes 2202 * (orientation, keyword being displayed). 2203 */ 2204 function computeDynamicLayout() { 2205 // Update the scrolling titles to ensure they are not in a now invalid 2206 // scroll position. 2207 var titleScrollers = 2208 document.getElementsByClassName('section-title-wrapper'); 2209 for (var i = 0, len = titleScrollers.length; i < len; i++) { 2210 var titleEl = 2211 titleScrollers[i].getElementsByClassName('section-title')[0]; 2212 handleTitleScroll( 2213 titleScrollers[i], 2214 titleEl.offsetLeft); 2215 } 2216 2217 updateMostVisitedStyle(); 2218 updateMostVisitedHeight(); 2219 } 2220 2221 /** 2222 * The centering of the 'recently closed' section is different depending on 2223 * the orientation of the device. In landscape, it should be left-aligned 2224 * with the 'most used' section. In portrait, it should be centered in the 2225 * screen. 2226 */ 2227 function updateMostVisitedStyle() { 2228 if (isTablet()) { 2229 updateMostVisitedStyleTablet(); 2230 } else { 2231 updateMostVisitedStylePhone(); 2232 } 2233 } 2234 2235 /** 2236 * Updates the style of the most visited pane for the phone. 2237 */ 2238 function updateMostVisitedStylePhone() { 2239 var mostVisitedList = $('most_visited_list'); 2240 var childEl = mostVisitedList.firstChild; 2241 if (!childEl) 2242 return; 2243 2244 // 'natural' height and width of the thumbnail 2245 var thumbHeight = 72; 2246 var thumbWidth = 108; 2247 var labelHeight = 25; 2248 var labelWidth = thumbWidth + 20; 2249 var labelLeft = (thumbWidth - labelWidth) / 2; 2250 var itemHeight = thumbHeight + labelHeight; 2251 2252 // default vertical margin between items 2253 var itemMarginTop = 0; 2254 var itemMarginBottom = 0; 2255 var itemMarginLeft = 20; 2256 var itemMarginRight = 20; 2257 2258 var listHeight = 0; 2259 2260 var screenHeight = 2261 document.documentElement.offsetHeight - 2262 getButtonBarPadding(); 2263 2264 if (isPortrait()) { 2265 mostVisitedList.setAttribute(GRID_COLUMNS, '2'); 2266 listHeight = screenHeight * .85; 2267 // Ensure that listHeight is not too small and not too big. 2268 listHeight = Math.max(listHeight, (itemHeight * 3) + 20); 2269 listHeight = Math.min(listHeight, 420); 2270 // Size for 3 rows (4 gutters) 2271 itemMarginTop = (listHeight - (itemHeight * 3)) / 4; 2272 } else { 2273 mostVisitedList.setAttribute(GRID_COLUMNS, '3'); 2274 listHeight = screenHeight; 2275 2276 // If the screen height is less than targetHeight, scale the size of the 2277 // thumbnails such that the margin between the thumbnails remains 2278 // constant. 2279 var targetHeight = 220; 2280 if (screenHeight < targetHeight) { 2281 var targetRemainder = targetHeight - 2 * (thumbHeight + labelHeight); 2282 var scale = (screenHeight - 2 * labelHeight - 2283 targetRemainder) / (2 * thumbHeight); 2284 // update values based on scale 2285 thumbWidth = Math.round(thumbWidth * scale); 2286 thumbHeight = Math.round(thumbHeight * scale); 2287 labelWidth = thumbWidth + 20; 2288 itemHeight = thumbHeight + labelHeight; 2289 } 2290 2291 // scale the vertical margin such that the items fit perfectly on the 2292 // screen 2293 var remainder = screenHeight - (2 * itemHeight); 2294 var margin = (remainder / 2); 2295 margin = margin > 24 ? 24 : margin; 2296 itemMarginTop = Math.round(margin / 2); 2297 itemMarginBottom = Math.round(margin - itemMarginTop); 2298 } 2299 2300 mostVisitedList.style.minHeight = listHeight + 'px'; 2301 2302 modifyCssRule('body[device="phone"] .thumbnail-cell', 2303 'height', itemHeight + 'px'); 2304 modifyCssRule('body[device="phone"] #most_visited_list .thumbnail', 2305 'height', thumbHeight + 'px'); 2306 modifyCssRule('body[device="phone"] #most_visited_list .thumbnail', 2307 'width', thumbWidth + 'px'); 2308 modifyCssRule( 2309 'body[device="phone"] #most_visited_list .thumbnail-container', 2310 'height', thumbHeight + 'px'); 2311 modifyCssRule( 2312 'body[device="phone"] #most_visited_list .thumbnail-container', 2313 'width', thumbWidth + 'px'); 2314 modifyCssRule('body[device="phone"] #most_visited_list .title', 2315 'width', labelWidth + 'px'); 2316 modifyCssRule('body[device="phone"] #most_visited_list .title', 2317 'left', labelLeft + 'px'); 2318 modifyCssRule('body[device="phone"] #most_visited_list .inner-border', 2319 'height', thumbHeight - 2 + 'px'); 2320 modifyCssRule('body[device="phone"] #most_visited_list .inner-border', 2321 'width', thumbWidth - 2 + 'px'); 2322 2323 modifyCssRule('body[device="phone"] .thumbnail-cell', 2324 'margin-left', itemMarginLeft + 'px'); 2325 modifyCssRule('body[device="phone"] .thumbnail-cell', 2326 'margin-right', itemMarginRight + 'px'); 2327 modifyCssRule('body[device="phone"] .thumbnail-cell', 2328 'margin-top', itemMarginTop + 'px'); 2329 modifyCssRule('body[device="phone"] .thumbnail-cell', 2330 'margin-bottom', itemMarginBottom + 'px'); 2331 2332 centerChildGrids($('most_visited_container')); 2333 } 2334 2335 /** 2336 * Updates the style of the most visited pane for the tablet. 2337 */ 2338 function updateMostVisitedStyleTablet() { 2339 function setCenterIconGrid(el, set) { 2340 if (set) { 2341 el.classList.add(GRID_CENTER_CSS_CLASS); 2342 } else { 2343 el.classList.remove(GRID_CENTER_CSS_CLASS); 2344 el.style.paddingLeft = '0px'; 2345 el.style.paddingRight = '0px'; 2346 } 2347 } 2348 var isPortrait = document.documentElement.offsetWidth < 2349 document.documentElement.offsetHeight; 2350 var mostVisitedContainer = $('most_visited_container'); 2351 var mostVisitedList = $('most_visited_list'); 2352 var recentlyClosedContainer = $('recently_closed_container'); 2353 var recentlyClosedList = $('recently_closed_list'); 2354 2355 setCenterIconGrid(mostVisitedContainer, !isPortrait); 2356 setCenterIconGrid(mostVisitedList, isPortrait); 2357 setCenterIconGrid(recentlyClosedContainer, isPortrait); 2358 if (isPortrait) { 2359 recentlyClosedList.classList.add(GRID_CSS_CLASS); 2360 } else { 2361 recentlyClosedList.classList.remove(GRID_CSS_CLASS); 2362 } 2363 2364 // Make the recently closed list visually left align with the most recently 2365 // closed items in landscape mode. It will be reset by the grid centering 2366 // in portrait mode. 2367 if (!isPortrait) 2368 recentlyClosedContainer.style.paddingLeft = '14px'; 2369 } 2370 2371 /** 2372 * This handles updating some of the spacing to make the 'recently closed' 2373 * section appear at the bottom of the page. 2374 */ 2375 function updateMostVisitedHeight() { 2376 if (!isTablet()) 2377 return; 2378 // subtract away height of button bar 2379 var windowHeight = document.documentElement.offsetHeight; 2380 var padding = parseInt(window.getComputedStyle(document.body) 2381 .getPropertyValue('padding-bottom')); 2382 $('most_visited_container').style.minHeight = 2383 (windowHeight - padding) + 'px'; 2384 } 2385 2386 /** 2387 * Called by the native toolbar to open a different section. This handles 2388 * updating the hash url which in turns makes a history entry. 2389 * 2390 * @param {string} section The section to switch to. 2391 */ 2392 var openSection = function(section) { 2393 if (!scrollToPane(getPaneIndex(section))) 2394 return; 2395 // Update the url so the native toolbar knows the pane has changed and 2396 // to create a history entry. 2397 document.location.hash = '#' + section; 2398 } 2399 2400 ///////////////////////////////////////////////////////////////////////////// 2401 // NTP Scoped Window Event Listeners. 2402 ///////////////////////////////////////////////////////////////////////////// 2403 2404 /** 2405 * Handles history on pop state changes. 2406 */ 2407 function onPopStateHandler(event) { 2408 if (event.state != null) { 2409 var evtState = event.state; 2410 // Navigate back to the previously selected panel and ensure the same 2411 // bookmarks are loaded. 2412 var selectedPaneIndex = evtState.selectedPaneIndex == undefined ? 2413 0 : evtState.selectedPaneIndex; 2414 2415 scrollToPane(selectedPaneIndex); 2416 setCurrentBookmarkFolderData(evtState.folderId); 2417 } else { 2418 // When loading the page, replace the default state with one that 2419 // specifies the default panel loaded via localStorage as well as the 2420 // default bookmark folder. 2421 history.replaceState( 2422 {folderId: bookmarkFolderId, selectedPaneIndex: currentPaneIndex}, 2423 null, null); 2424 } 2425 } 2426 2427 /** 2428 * Handles window resize events. 2429 */ 2430 function windowResizeHandler() { 2431 // Scroll to the current pane to refactor all the margins and offset. 2432 scrollToPane(currentPaneIndex); 2433 computeDynamicLayout(); 2434 // Center the padding for each of the grid views. 2435 centerChildGrids(document); 2436 centerEmptySections(document); 2437 } 2438 2439 /* 2440 * We implement the context menu ourselves. 2441 */ 2442 function contextMenuHandler(evt) { 2443 var section = SectionType.UNKNOWN; 2444 contextMenuUrl = null; 2445 contextMenuItem = null; 2446 // The node with a menu have been tagged with their section and url. 2447 // Let's find these tags. 2448 var node = evt.target; 2449 while (node) { 2450 if (section == SectionType.UNKNOWN && 2451 node.getAttribute && 2452 node.getAttribute(SECTION_KEY) != null) { 2453 section = node.getAttribute(SECTION_KEY); 2454 if (contextMenuUrl != null) 2455 break; 2456 } 2457 if (contextMenuUrl == null) { 2458 contextMenuUrl = node.getAttribute(CONTEXT_MENU_URL_KEY); 2459 contextMenuItem = node.contextMenuItem; 2460 if (section != SectionType.UNKNOWN) 2461 break; 2462 } 2463 node = node.parentNode; 2464 } 2465 2466 var menuOptions; 2467 2468 if (section == SectionType.BOOKMARKS && 2469 !contextMenuItem.folder && !isIncognito) { 2470 menuOptions = [ 2471 [ 2472 ContextMenuItemIds.BOOKMARK_OPEN_IN_NEW_TAB, 2473 templateData.elementopeninnewtab 2474 ] 2475 ]; 2476 if (isIncognitoEnabled) { 2477 menuOptions.push([ 2478 ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB, 2479 templateData.elementopeninincognitotab 2480 ]); 2481 } 2482 if (contextMenuItem.editable) { 2483 menuOptions.push( 2484 [ContextMenuItemIds.BOOKMARK_EDIT, templateData.bookmarkedit], 2485 [ContextMenuItemIds.BOOKMARK_DELETE, templateData.bookmarkdelete]); 2486 } 2487 if (contextMenuUrl.search('chrome://') == -1 && 2488 contextMenuUrl.search('about://') == -1 && 2489 document.body.getAttribute('shortcut_item_enabled') == 'true') { 2490 menuOptions.push([ 2491 ContextMenuItemIds.BOOKMARK_SHORTCUT, 2492 templateData.bookmarkshortcut 2493 ]); 2494 } 2495 } else if (section == SectionType.BOOKMARKS && 2496 !contextMenuItem.folder && 2497 isIncognito) { 2498 menuOptions = [ 2499 [ 2500 ContextMenuItemIds.BOOKMARK_OPEN_IN_INCOGNITO_TAB, 2501 templateData.elementopeninincognitotab 2502 ] 2503 ]; 2504 } else if (section == SectionType.BOOKMARKS && 2505 contextMenuItem.folder && 2506 contextMenuItem.editable && 2507 !isIncognito) { 2508 menuOptions = [ 2509 [ContextMenuItemIds.BOOKMARK_EDIT, templateData.editfolder], 2510 [ContextMenuItemIds.BOOKMARK_DELETE, templateData.deletefolder] 2511 ]; 2512 } else if (section == SectionType.MOST_VISITED) { 2513 menuOptions = [ 2514 [ 2515 ContextMenuItemIds.MOST_VISITED_OPEN_IN_NEW_TAB, 2516 templateData.elementopeninnewtab 2517 ], 2518 ]; 2519 if (isIncognitoEnabled) { 2520 menuOptions.push([ 2521 ContextMenuItemIds.MOST_VISITED_OPEN_IN_INCOGNITO_TAB, 2522 templateData.elementopeninincognitotab 2523 ]); 2524 } 2525 menuOptions.push( 2526 [ContextMenuItemIds.MOST_VISITED_REMOVE, templateData.elementremove]); 2527 } else if (section == SectionType.RECENTLY_CLOSED) { 2528 menuOptions = [ 2529 [ 2530 ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_NEW_TAB, 2531 templateData.elementopeninnewtab 2532 ], 2533 ]; 2534 if (isIncognitoEnabled) { 2535 menuOptions.push([ 2536 ContextMenuItemIds.RECENTLY_CLOSED_OPEN_IN_INCOGNITO_TAB, 2537 templateData.elementopeninincognitotab 2538 ]); 2539 } 2540 menuOptions.push( 2541 [ContextMenuItemIds.RECENTLY_CLOSED_REMOVE, templateData.removeall]); 2542 } else if (section == SectionType.FOREIGN_SESSION_HEADER) { 2543 menuOptions = [ 2544 [ 2545 ContextMenuItemIds.FOREIGN_SESSIONS_REMOVE, 2546 templateData.elementremove 2547 ] 2548 ]; 2549 } else if (section == SectionType.PROMO_VC_SESSION_HEADER) { 2550 menuOptions = [ 2551 [ 2552 ContextMenuItemIds.PROMO_VC_SESSION_REMOVE, 2553 templateData.elementremove 2554 ] 2555 ]; 2556 } 2557 2558 if (menuOptions) 2559 chrome.send('showContextMenu', menuOptions); 2560 2561 return false; 2562 } 2563 2564 // Return an object with all the exports 2565 return { 2566 bookmarks: bookmarks, 2567 bookmarkChanged: bookmarkChanged, 2568 clearPromotions: clearPromotions, 2569 init: init, 2570 setIncognitoEnabled: setIncognitoEnabled, 2571 onCustomMenuSelected: onCustomMenuSelected, 2572 openSection: openSection, 2573 setFaviconDominantColor: setFaviconDominantColor, 2574 setForeignSessions: setForeignSessions, 2575 setIncognitoMode: setIncognitoMode, 2576 setMostVisitedPages: setMostVisitedPages, 2577 setPromotions: setPromotions, 2578 setRecentlyClosedTabs: setRecentlyClosedTabs, 2579 setSyncEnabled: setSyncEnabled, 2580 snapshots: snapshots 2581 }; 2582 }); 2583 2584 ///////////////////////////////////////////////////////////////////////////// 2585 //Utility Functions. 2586 ///////////////////////////////////////////////////////////////////////////// 2587 2588 /** 2589 * A best effort approach for checking simple data object equality. 2590 * @param {?} val1 The first value to check equality for. 2591 * @param {?} val2 The second value to check equality for. 2592 * @return {boolean} Whether the two objects are equal(ish). 2593 */ 2594 function equals(val1, val2) { 2595 if (typeof val1 != 'object' || typeof val2 != 'object') 2596 return val1 === val2; 2597 2598 // Object and array equality checks. 2599 var keyCountVal1 = 0; 2600 for (var key in val1) { 2601 if (!(key in val2) || !equals(val1[key], val2[key])) 2602 return false; 2603 keyCountVal1++; 2604 } 2605 var keyCountVal2 = 0; 2606 for (var key in val2) 2607 keyCountVal2++; 2608 if (keyCountVal1 != keyCountVal2) 2609 return false; 2610 return true; 2611 } 2612 2613 /** 2614 * Alias for document.getElementById. 2615 * @param {string} id The ID of the element to find. 2616 * @return {HTMLElement} The found element or null if not found. 2617 */ 2618 function $(id) { 2619 return document.getElementById(id); 2620 } 2621 2622 /** 2623 * @return {boolean} Whether the device is currently in portrait mode. 2624 */ 2625 function isPortrait() { 2626 return document.documentElement.offsetWidth < 2627 document.documentElement.offsetHeight; 2628 } 2629 2630 /** 2631 * Determine if the page should be formatted for tablets. 2632 * @return {boolean} true if the device is a tablet, false otherwise. 2633 */ 2634 function isTablet() { 2635 return document.body.getAttribute('device') == 'tablet'; 2636 } 2637 2638 /** 2639 * Determine if the page should be formatted for phones. 2640 * @return {boolean} true if the device is a phone, false otherwise. 2641 */ 2642 function isPhone() { 2643 return document.body.getAttribute('device') == 'phone'; 2644 } 2645 2646 /** 2647 * Get the page X coordinate of a touch event. 2648 * @param {TouchEvent} evt The touch event triggered by the browser. 2649 * @return {number} The page X coordinate of the touch event. 2650 */ 2651 function getTouchEventX(evt) { 2652 return (evt.touches[0] || e.changedTouches[0]).pageX; 2653 } 2654 2655 /** 2656 * Get the page Y coordinate of a touch event. 2657 * @param {TouchEvent} evt The touch event triggered by the browser. 2658 * @return {number} The page Y coordinate of the touch event. 2659 */ 2660 function getTouchEventY(evt) { 2661 return (evt.touches[0] || e.changedTouches[0]).pageY; 2662 } 2663 2664 /** 2665 * @param {Element} el The item to get the width of. 2666 * @param {boolean} excludeMargin If true, exclude the width of the margin. 2667 * @return {number} The total width of a given item. 2668 */ 2669 function getItemWidth(el, excludeMargin) { 2670 var elStyle = window.getComputedStyle(el); 2671 var width = el.offsetWidth; 2672 if (!width || width == 0) { 2673 width = parseInt(elStyle.getPropertyValue('width')); 2674 width += 2675 parseInt(elStyle.getPropertyValue('border-left-width')) + 2676 parseInt(elStyle.getPropertyValue('border-right-width')); 2677 width += 2678 parseInt(elStyle.getPropertyValue('padding-left')) + 2679 parseInt(elStyle.getPropertyValue('padding-right')); 2680 } 2681 if (!excludeMargin) { 2682 width += parseInt(elStyle.getPropertyValue('margin-left')) + 2683 parseInt(elStyle.getPropertyValue('margin-right')); 2684 } 2685 return width; 2686 } 2687 2688 /** 2689 * @return {number} The padding height of the body due to the button bar 2690 */ 2691 function getButtonBarPadding() { 2692 var body = document.getElementsByTagName('body')[0]; 2693 var style = window.getComputedStyle(body); 2694 return parseInt(style.getPropertyValue('padding-bottom')); 2695 } 2696 2697 /** 2698 * Modify a css rule 2699 * @param {string} selector The selector for the rule (passed to findCssRule()) 2700 * @param {string} property The property to update 2701 * @param {string} value The value to update the property to 2702 * @return {boolean} true if the rule was updated, false otherwise. 2703 */ 2704 function modifyCssRule(selector, property, value) { 2705 var rule = findCssRule(selector); 2706 if (!rule) 2707 return false; 2708 rule.style[property] = value; 2709 return true; 2710 } 2711 2712 /** 2713 * Find a particular CSS rule. The stylesheets attached to the document 2714 * are traversed in reverse order. The rules in each stylesheet are also 2715 * traversed in reverse order. The first rule found to match the selector 2716 * is returned. 2717 * @param {string} selector The selector for the rule. 2718 * @return {Object} The rule if one was found, null otherwise 2719 */ 2720 function findCssRule(selector) { 2721 var styleSheets = document.styleSheets; 2722 for (i = styleSheets.length - 1; i >= 0; i--) { 2723 var styleSheet = styleSheets[i]; 2724 var rules = styleSheet.cssRules; 2725 if (rules == null) 2726 continue; 2727 for (j = rules.length - 1; j >= 0; j--) { 2728 if (rules[j].selectorText == selector) 2729 return rules[j]; 2730 } 2731 } 2732 } 2733 2734 ///////////////////////////////////////////////////////////////////////////// 2735 // NTP Entry point. 2736 ///////////////////////////////////////////////////////////////////////////// 2737 2738 /* 2739 * Handles initializing the UI when the page has finished loading. 2740 */ 2741 window.addEventListener('DOMContentLoaded', function(evt) { 2742 ntp.init(); 2743 $('content-area').style.display = 'block'; 2744 }); 2745