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 cr.define('options', function() { 6 ///////////////////////////////////////////////////////////////////////////// 7 // OptionsPage class: 8 9 /** 10 * Base class for options page. 11 * @constructor 12 * @param {string} name Options page name, also defines id of the div element 13 * containing the options view and the name of options page navigation bar 14 * item as name+'PageNav'. 15 * @param {string} title Options page title, used for navigation bar 16 * @extends {EventTarget} 17 */ 18 function OptionsPage(name, title, pageDivName) { 19 this.name = name; 20 this.title = title; 21 this.pageDivName = pageDivName; 22 this.pageDiv = $(this.pageDivName); 23 this.tab = null; 24 this.managed = false; 25 } 26 27 const SUBPAGE_SHEET_COUNT = 2; 28 29 /** 30 * Main level option pages. 31 * @protected 32 */ 33 OptionsPage.registeredPages = {}; 34 35 /** 36 * Pages which are meant to behave like modal dialogs. 37 * @protected 38 */ 39 OptionsPage.registeredOverlayPages = {}; 40 41 /** 42 * Whether or not |initialize| has been called. 43 * @private 44 */ 45 OptionsPage.initialized_ = false; 46 47 /** 48 * Gets the default page (to be shown on initial load). 49 */ 50 OptionsPage.getDefaultPage = function() { 51 return BrowserOptions.getInstance(); 52 }; 53 54 /** 55 * Shows the default page. 56 */ 57 OptionsPage.showDefaultPage = function() { 58 this.navigateToPage(this.getDefaultPage().name); 59 }; 60 61 /** 62 * "Navigates" to a page, meaning that the page will be shown and the 63 * appropriate entry is placed in the history. 64 * @param {string} pageName Page name. 65 */ 66 OptionsPage.navigateToPage = function(pageName) { 67 this.showPageByName(pageName, true); 68 }; 69 70 /** 71 * Shows a registered page. This handles both top-level pages and sub-pages. 72 * @param {string} pageName Page name. 73 * @param {boolean} updateHistory True if we should update the history after 74 * showing the page. 75 * @private 76 */ 77 OptionsPage.showPageByName = function(pageName, updateHistory) { 78 // Find the currently visible root-level page. 79 var rootPage = null; 80 for (var name in this.registeredPages) { 81 var page = this.registeredPages[name]; 82 if (page.visible && !page.parentPage) { 83 rootPage = page; 84 break; 85 } 86 } 87 88 // Find the target page. 89 var targetPage = this.registeredPages[pageName]; 90 if (!targetPage || !targetPage.canShowPage()) { 91 // If it's not a page, try it as an overlay. 92 if (!targetPage && this.showOverlay_(pageName, rootPage)) { 93 if (updateHistory) 94 this.updateHistoryState_(); 95 return; 96 } else { 97 targetPage = this.getDefaultPage(); 98 } 99 } 100 101 pageName = targetPage.name; 102 103 // Determine if the root page is 'sticky', meaning that it 104 // shouldn't change when showing a sub-page. This can happen for special 105 // pages like Search. 106 var isRootPageLocked = 107 rootPage && rootPage.sticky && targetPage.parentPage; 108 109 // Notify pages if they will be hidden. 110 for (var name in this.registeredPages) { 111 var page = this.registeredPages[name]; 112 if (!page.parentPage && isRootPageLocked) 113 continue; 114 if (page.willHidePage && name != pageName && 115 !page.isAncestorOfPage(targetPage)) 116 page.willHidePage(); 117 } 118 119 // Update visibilities to show only the hierarchy of the target page. 120 for (var name in this.registeredPages) { 121 var page = this.registeredPages[name]; 122 if (!page.parentPage && isRootPageLocked) 123 continue; 124 page.visible = name == pageName || 125 (!document.documentElement.classList.contains('hide-menu') && 126 page.isAncestorOfPage(targetPage)); 127 } 128 129 // Update the history and current location. 130 if (updateHistory) 131 this.updateHistoryState_(); 132 133 // Always update the page title. 134 document.title = targetPage.title; 135 136 // Notify pages if they were shown. 137 for (var name in this.registeredPages) { 138 var page = this.registeredPages[name]; 139 if (!page.parentPage && isRootPageLocked) 140 continue; 141 if (page.didShowPage && (name == pageName || 142 page.isAncestorOfPage(targetPage))) 143 page.didShowPage(); 144 } 145 }; 146 147 /** 148 * Updates the visibility and stacking order of the subpage backdrop 149 * according to which subpage is topmost and visible. 150 * @private 151 */ 152 OptionsPage.updateSubpageBackdrop_ = function () { 153 var topmostPage = this.getTopmostVisibleNonOverlayPage_(); 154 var nestingLevel = topmostPage ? topmostPage.nestingLevel : 0; 155 156 var subpageBackdrop = $('subpage-backdrop'); 157 if (nestingLevel > 0) { 158 var container = $('subpage-sheet-container-' + nestingLevel); 159 subpageBackdrop.style.zIndex = 160 parseInt(window.getComputedStyle(container).zIndex) - 1; 161 subpageBackdrop.hidden = false; 162 } else { 163 subpageBackdrop.hidden = true; 164 } 165 }; 166 167 /** 168 * Pushes the current page onto the history stack, overriding the last page 169 * if it is the generic chrome://settings/. 170 * @private 171 */ 172 OptionsPage.updateHistoryState_ = function() { 173 var page = this.getTopmostVisiblePage(); 174 var path = location.pathname; 175 if (path) 176 path = path.slice(1); 177 // The page is already in history (the user may have clicked the same link 178 // twice). Do nothing. 179 if (path == page.name) 180 return; 181 182 // If there is no path, the current location is chrome://settings/. 183 // Override this with the new page. 184 var historyFunction = path ? window.history.pushState : 185 window.history.replaceState; 186 historyFunction.call(window.history, 187 {pageName: page.name}, 188 page.title, 189 '/' + page.name); 190 // Update tab title. 191 document.title = page.title; 192 }; 193 194 /** 195 * Shows a registered Overlay page. Does not update history. 196 * @param {string} overlayName Page name. 197 * @param {OptionPage} rootPage The currently visible root-level page. 198 * @return {boolean} whether we showed an overlay. 199 */ 200 OptionsPage.showOverlay_ = function(overlayName, rootPage) { 201 var overlay = this.registeredOverlayPages[overlayName]; 202 if (!overlay || !overlay.canShowPage()) 203 return false; 204 205 if ((!rootPage || !rootPage.sticky) && overlay.parentPage) 206 this.showPageByName(overlay.parentPage.name, false); 207 208 overlay.visible = true; 209 if (overlay.didShowPage) overlay.didShowPage(); 210 return true; 211 }; 212 213 /** 214 * Returns whether or not an overlay is visible. 215 * @return {boolean} True if an overlay is visible. 216 * @private 217 */ 218 OptionsPage.isOverlayVisible_ = function() { 219 return this.getVisibleOverlay_() != null; 220 }; 221 222 /** 223 * @return {boolean} True if the visible overlay should be closed. 224 * @private 225 */ 226 OptionsPage.shouldCloseOverlay_ = function() { 227 var overlay = this.getVisibleOverlay_(); 228 return overlay && overlay.shouldClose(); 229 }; 230 231 /** 232 * Returns the currently visible overlay, or null if no page is visible. 233 * @return {OptionPage} The visible overlay. 234 */ 235 OptionsPage.getVisibleOverlay_ = function() { 236 for (var name in this.registeredOverlayPages) { 237 var page = this.registeredOverlayPages[name]; 238 if (page.visible) 239 return page; 240 } 241 return null; 242 }; 243 244 /** 245 * Closes the visible overlay. Updates the history state after closing the 246 * overlay. 247 */ 248 OptionsPage.closeOverlay = function() { 249 var overlay = this.getVisibleOverlay_(); 250 if (!overlay) 251 return; 252 253 overlay.visible = false; 254 if (overlay.didClosePage) overlay.didClosePage(); 255 this.updateHistoryState_(); 256 }; 257 258 /** 259 * Hides the visible overlay. Does not affect the history state. 260 * @private 261 */ 262 OptionsPage.hideOverlay_ = function() { 263 var overlay = this.getVisibleOverlay_(); 264 if (overlay) 265 overlay.visible = false; 266 }; 267 268 /** 269 * Returns the topmost visible page (overlays excluded). 270 * @return {OptionPage} The topmost visible page aside any overlay. 271 * @private 272 */ 273 OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { 274 var topPage = null; 275 for (var name in this.registeredPages) { 276 var page = this.registeredPages[name]; 277 if (page.visible && 278 (!topPage || page.nestingLevel > topPage.nestingLevel)) 279 topPage = page; 280 } 281 282 return topPage; 283 }; 284 285 /** 286 * Returns the topmost visible page, or null if no page is visible. 287 * @return {OptionPage} The topmost visible page. 288 */ 289 OptionsPage.getTopmostVisiblePage = function() { 290 // Check overlays first since they're top-most if visible. 291 return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); 292 }; 293 294 /** 295 * Closes the topmost open subpage, if any. 296 * @private 297 */ 298 OptionsPage.closeTopSubPage_ = function() { 299 var topPage = this.getTopmostVisiblePage(); 300 if (topPage && !topPage.isOverlay && topPage.parentPage) 301 topPage.visible = false; 302 303 this.updateHistoryState_(); 304 }; 305 306 /** 307 * Closes all subpages below the given level. 308 * @param {number} level The nesting level to close below. 309 */ 310 OptionsPage.closeSubPagesToLevel = function(level) { 311 var topPage = this.getTopmostVisiblePage(); 312 while (topPage && topPage.nestingLevel > level) { 313 topPage.visible = false; 314 topPage = topPage.parentPage; 315 } 316 317 this.updateHistoryState_(); 318 }; 319 320 /** 321 * Updates managed banner visibility state based on the topmost page. 322 */ 323 OptionsPage.updateManagedBannerVisibility = function() { 324 var topPage = this.getTopmostVisiblePage(); 325 if (topPage) 326 topPage.updateManagedBannerVisibility(); 327 }; 328 329 /** 330 * Shows the tab contents for the given navigation tab. 331 * @param {!Element} tab The tab that the user clicked. 332 */ 333 OptionsPage.showTab = function(tab) { 334 // Search parents until we find a tab, or the nav bar itself. This allows 335 // tabs to have child nodes, e.g. labels in separately-styled spans. 336 while (tab && !tab.classList.contains('subpages-nav-tabs') && 337 !tab.classList.contains('tab')) { 338 tab = tab.parentNode; 339 } 340 if (!tab || !tab.classList.contains('tab')) 341 return; 342 343 if (this.activeNavTab != null) { 344 this.activeNavTab.classList.remove('active-tab'); 345 $(this.activeNavTab.getAttribute('tab-contents')).classList. 346 remove('active-tab-contents'); 347 } 348 349 tab.classList.add('active-tab'); 350 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); 351 this.activeNavTab = tab; 352 }; 353 354 /** 355 * Registers new options page. 356 * @param {OptionsPage} page Page to register. 357 */ 358 OptionsPage.register = function(page) { 359 this.registeredPages[page.name] = page; 360 // Create and add new page <li> element to navbar. 361 var pageNav = document.createElement('li'); 362 pageNav.id = page.name + 'PageNav'; 363 pageNav.className = 'navbar-item'; 364 pageNav.setAttribute('pageName', page.name); 365 pageNav.textContent = page.pageDiv.querySelector('h1').textContent; 366 pageNav.tabIndex = 0; 367 pageNav.onclick = function(event) { 368 OptionsPage.navigateToPage(this.getAttribute('pageName')); 369 }; 370 pageNav.onkeypress = function(event) { 371 // Enter or space 372 if (event.keyCode == 13 || event.keyCode == 32) { 373 OptionsPage.navigateToPage(this.getAttribute('pageName')); 374 } 375 }; 376 var navbar = $('navbar'); 377 navbar.appendChild(pageNav); 378 page.tab = pageNav; 379 page.initializePage(); 380 }; 381 382 /** 383 * Find an enclosing section for an element if it exists. 384 * @param {Element} element Element to search. 385 * @return {OptionPage} The section element, or null. 386 * @private 387 */ 388 OptionsPage.findSectionForNode_ = function(node) { 389 while (node = node.parentNode) { 390 if (node.nodeName == 'SECTION') 391 return node; 392 } 393 return null; 394 }; 395 396 /** 397 * Registers a new Sub-page. 398 * @param {OptionsPage} subPage Sub-page to register. 399 * @param {OptionsPage} parentPage Associated parent page for this page. 400 * @param {Array} associatedControls Array of control elements that lead to 401 * this sub-page. The first item is typically a button in a root-level 402 * page. There may be additional buttons for nested sub-pages. 403 */ 404 OptionsPage.registerSubPage = function(subPage, 405 parentPage, 406 associatedControls) { 407 this.registeredPages[subPage.name] = subPage; 408 subPage.parentPage = parentPage; 409 if (associatedControls) { 410 subPage.associatedControls = associatedControls; 411 if (associatedControls.length) { 412 subPage.associatedSection = 413 this.findSectionForNode_(associatedControls[0]); 414 } 415 } 416 subPage.tab = undefined; 417 subPage.initializePage(); 418 }; 419 420 /** 421 * Registers a new Overlay page. 422 * @param {OptionsPage} overlay Overlay to register. 423 * @param {OptionsPage} parentPage Associated parent page for this overlay. 424 * @param {Array} associatedControls Array of control elements associated with 425 * this page. 426 */ 427 OptionsPage.registerOverlay = function(overlay, 428 parentPage, 429 associatedControls) { 430 this.registeredOverlayPages[overlay.name] = overlay; 431 overlay.parentPage = parentPage; 432 if (associatedControls) { 433 overlay.associatedControls = associatedControls; 434 if (associatedControls.length) { 435 overlay.associatedSection = 436 this.findSectionForNode_(associatedControls[0]); 437 } 438 } 439 overlay.tab = undefined; 440 overlay.isOverlay = true; 441 overlay.initializePage(); 442 }; 443 444 /** 445 * Callback for window.onpopstate. 446 * @param {Object} data State data pushed into history. 447 */ 448 OptionsPage.setState = function(data) { 449 if (data && data.pageName) { 450 // It's possible an overlay may be the last top-level page shown. 451 if (this.isOverlayVisible_() && 452 this.registeredOverlayPages[data.pageName] == undefined) { 453 this.hideOverlay_(); 454 } 455 456 this.showPageByName(data.pageName, false); 457 } 458 }; 459 460 /** 461 * Callback for window.onbeforeunload. Used to notify overlays that they will 462 * be closed. 463 */ 464 OptionsPage.willClose = function() { 465 var overlay = this.getVisibleOverlay_(); 466 if (overlay && overlay.didClosePage) 467 overlay.didClosePage(); 468 }; 469 470 /** 471 * Freezes/unfreezes the scroll position of given level's page container. 472 * @param {boolean} freeze Whether the page should be frozen. 473 * @param {number} level The level to freeze/unfreeze. 474 * @private 475 */ 476 OptionsPage.setPageFrozenAtLevel_ = function(freeze, level) { 477 var container = level == 0 ? $('toplevel-page-container') 478 : $('subpage-sheet-container-' + level); 479 480 if (container.classList.contains('frozen') == freeze) 481 return; 482 483 if (freeze) { 484 var scrollPosition = document.body.scrollTop; 485 // Lock the width, since auto width computation may change. 486 container.style.width = window.getComputedStyle(container).width; 487 container.classList.add('frozen'); 488 container.style.top = -scrollPosition + 'px'; 489 this.updateFrozenElementHorizontalPosition_(container); 490 } else { 491 var scrollPosition = - parseInt(container.style.top, 10); 492 container.classList.remove('frozen'); 493 container.style.top = ''; 494 container.style.left = ''; 495 container.style.right = ''; 496 container.style.width = ''; 497 // Restore the scroll position. 498 if (!container.hidden) 499 window.scroll(document.body.scrollLeft, scrollPosition); 500 } 501 }; 502 503 /** 504 * Freezes/unfreezes the scroll position of visible pages based on the current 505 * page stack. 506 */ 507 OptionsPage.updatePageFreezeStates = function() { 508 var topPage = OptionsPage.getTopmostVisiblePage(); 509 if (!topPage) 510 return; 511 var nestingLevel = topPage.isOverlay ? 100 : topPage.nestingLevel; 512 for (var i = 0; i <= SUBPAGE_SHEET_COUNT; i++) { 513 this.setPageFrozenAtLevel_(i < nestingLevel, i); 514 } 515 }; 516 517 /** 518 * Initializes the complete options page. This will cause all C++ handlers to 519 * be invoked to do final setup. 520 */ 521 OptionsPage.initialize = function() { 522 chrome.send('coreOptionsInitialize'); 523 this.initialized_ = true; 524 525 document.addEventListener('scroll', this.handleScroll_.bind(this)); 526 window.addEventListener('resize', this.handleResize_.bind(this)); 527 528 if (!document.documentElement.classList.contains('hide-menu')) { 529 // Close subpages if the user clicks on the html body. Listen in the 530 // capturing phase so that we can stop the click from doing anything. 531 document.body.addEventListener('click', 532 this.bodyMouseEventHandler_.bind(this), 533 true); 534 // We also need to cancel mousedowns on non-subpage content. 535 document.body.addEventListener('mousedown', 536 this.bodyMouseEventHandler_.bind(this), 537 true); 538 539 var self = this; 540 // Hook up the close buttons. 541 subpageCloseButtons = document.querySelectorAll('.close-subpage'); 542 for (var i = 0; i < subpageCloseButtons.length; i++) { 543 subpageCloseButtons[i].onclick = function() { 544 self.closeTopSubPage_(); 545 }; 546 }; 547 548 // Install handler for key presses. 549 document.addEventListener('keydown', 550 this.keyDownEventHandler_.bind(this)); 551 552 document.addEventListener('focus', this.manageFocusChange_.bind(this), 553 true); 554 } 555 556 // Calculate and store the horizontal locations of elements that may be 557 // frozen later. 558 var sidebarWidth = 559 parseInt(window.getComputedStyle($('mainview')).webkitPaddingStart, 10); 560 $('toplevel-page-container').horizontalOffset = sidebarWidth + 561 parseInt(window.getComputedStyle( 562 $('mainview-content')).webkitPaddingStart, 10); 563 for (var level = 1; level <= SUBPAGE_SHEET_COUNT; level++) { 564 var containerId = 'subpage-sheet-container-' + level; 565 $(containerId).horizontalOffset = sidebarWidth; 566 } 567 $('subpage-backdrop').horizontalOffset = sidebarWidth; 568 // Trigger the resize handler manually to set the initial state. 569 this.handleResize_(null); 570 }; 571 572 /** 573 * Does a bounds check for the element on the given x, y client coordinates. 574 * @param {Element} e The DOM element. 575 * @param {number} x The client X to check. 576 * @param {number} y The client Y to check. 577 * @return {boolean} True if the point falls within the element's bounds. 578 * @private 579 */ 580 OptionsPage.elementContainsPoint_ = function(e, x, y) { 581 var clientRect = e.getBoundingClientRect(); 582 return x >= clientRect.left && x <= clientRect.right && 583 y >= clientRect.top && y <= clientRect.bottom; 584 }; 585 586 /** 587 * Called when focus changes; ensures that focus doesn't move outside 588 * the topmost subpage/overlay. 589 * @param {Event} e The focus change event. 590 * @private 591 */ 592 OptionsPage.manageFocusChange_ = function(e) { 593 var focusableItemsRoot; 594 var topPage = this.getTopmostVisiblePage(); 595 if (!topPage) 596 return; 597 598 if (topPage.isOverlay) { 599 // If an overlay is visible, that defines the tab loop. 600 focusableItemsRoot = topPage.pageDiv; 601 } else { 602 // If a subpage is visible, use its parent as the tab loop constraint. 603 // (The parent is used because it contains the close button.) 604 if (topPage.nestingLevel > 0) 605 focusableItemsRoot = topPage.pageDiv.parentNode; 606 } 607 608 if (focusableItemsRoot && !focusableItemsRoot.contains(e.target)) 609 topPage.focusFirstElement(); 610 }; 611 612 /** 613 * Called when the page is scrolled; moves elements that are position:fixed 614 * but should only behave as if they are fixed for vertical scrolling. 615 * @param {Event} e The scroll event. 616 * @private 617 */ 618 OptionsPage.handleScroll_ = function(e) { 619 var scrollHorizontalOffset = document.body.scrollLeft; 620 // position:fixed doesn't seem to work for horizontal scrolling in RTL mode, 621 // so only adjust in LTR mode (where scroll values will be positive). 622 if (scrollHorizontalOffset >= 0) { 623 $('navbar-container').style.left = -scrollHorizontalOffset + 'px'; 624 var subpageBackdrop = $('subpage-backdrop'); 625 subpageBackdrop.style.left = subpageBackdrop.horizontalOffset - 626 scrollHorizontalOffset + 'px'; 627 this.updateAllFrozenElementPositions_(); 628 } 629 }; 630 631 /** 632 * Updates all frozen pages to match the horizontal scroll position. 633 * @private 634 */ 635 OptionsPage.updateAllFrozenElementPositions_ = function() { 636 var frozenElements = document.querySelectorAll('.frozen'); 637 for (var i = 0; i < frozenElements.length; i++) { 638 this.updateFrozenElementHorizontalPosition_(frozenElements[i]); 639 } 640 }; 641 642 /** 643 * Updates the given frozen element to match the horizontal scroll position. 644 * @param {HTMLElement} e The frozen element to update 645 * @private 646 */ 647 OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { 648 if (document.documentElement.dir == 'rtl') 649 e.style.right = e.horizontalOffset + 'px'; 650 else 651 e.style.left = e.horizontalOffset - document.body.scrollLeft + 'px'; 652 }; 653 654 /** 655 * Called when the page is resized; adjusts the size of elements that depend 656 * on the veiwport. 657 * @param {Event} e The resize event. 658 * @private 659 */ 660 OptionsPage.handleResize_ = function(e) { 661 // Set an explicit height equal to the viewport on all the subpage 662 // containers shorter than the viewport. This is used instead of 663 // min-height: 100% so that there is an explicit height for the subpages' 664 // min-height: 100%. 665 var viewportHeight = document.documentElement.clientHeight; 666 var subpageContainers = 667 document.querySelectorAll('.subpage-sheet-container'); 668 for (var i = 0; i < subpageContainers.length; i++) { 669 if (subpageContainers[i].scrollHeight > viewportHeight) 670 subpageContainers[i].style.removeProperty('height'); 671 else 672 subpageContainers[i].style.height = viewportHeight + 'px'; 673 } 674 }; 675 676 /** 677 * A function to handle mouse events (mousedown or click) on the html body by 678 * closing subpages and/or stopping event propagation. 679 * @return {Event} a mousedown or click event. 680 * @private 681 */ 682 OptionsPage.bodyMouseEventHandler_ = function(event) { 683 // Do nothing if a subpage isn't showing. 684 var topPage = this.getTopmostVisiblePage(); 685 if (!topPage || topPage.isOverlay || !topPage.parentPage) 686 return; 687 688 // Do nothing if the client coordinates are not within the source element. 689 // This situation is indicative of a Webkit bug where clicking on a 690 // radio/checkbox label span will generate an event with client coordinates 691 // of (-scrollX, -scrollY). 692 // See https://bugs.webkit.org/show_bug.cgi?id=56606 693 if (event.clientX == -document.body.scrollLeft && 694 event.clientY == -document.body.scrollTop) { 695 return; 696 } 697 698 // Don't interfere with navbar clicks. 699 if ($('navbar').contains(event.target)) 700 return; 701 702 // Figure out which page the click happened in. 703 for (var level = topPage.nestingLevel; level >= 0; level--) { 704 var clickIsWithinLevel = level == 0 ? true : 705 OptionsPage.elementContainsPoint_( 706 $('subpage-sheet-' + level), event.clientX, event.clientY); 707 708 if (!clickIsWithinLevel) 709 continue; 710 711 // Event was within the topmost page; do nothing. 712 if (topPage.nestingLevel == level) 713 return; 714 715 // Block propgation of both clicks and mousedowns, but only close subpages 716 // on click. 717 if (event.type == 'click') 718 this.closeSubPagesToLevel(level); 719 event.stopPropagation(); 720 event.preventDefault(); 721 return; 722 } 723 }; 724 725 /** 726 * A function to handle key press events. 727 * @return {Event} a keydown event. 728 * @private 729 */ 730 OptionsPage.keyDownEventHandler_ = function(event) { 731 // Close the top overlay or sub-page on esc. 732 if (event.keyCode == 27) { // Esc 733 if (this.isOverlayVisible_()) { 734 if (this.shouldCloseOverlay_()) 735 this.closeOverlay(); 736 } else { 737 this.closeTopSubPage_(); 738 } 739 } 740 }; 741 742 OptionsPage.setClearPluginLSODataEnabled = function(enabled) { 743 if (enabled) { 744 document.documentElement.setAttribute( 745 'flashPluginSupportsClearSiteData', ''); 746 } else { 747 document.documentElement.removeAttribute( 748 'flashPluginSupportsClearSiteData'); 749 } 750 }; 751 752 /** 753 * Re-initializes the C++ handlers if necessary. This is called if the 754 * handlers are torn down and recreated but the DOM may not have been (in 755 * which case |initialize| won't be called again). If |initialize| hasn't been 756 * called, this does nothing (since it will be later, once the DOM has 757 * finished loading). 758 */ 759 OptionsPage.reinitializeCore = function() { 760 if (this.initialized_) 761 chrome.send('coreOptionsInitialize'); 762 } 763 764 OptionsPage.prototype = { 765 __proto__: cr.EventTarget.prototype, 766 767 /** 768 * The parent page of this option page, or null for top-level pages. 769 * @type {OptionsPage} 770 */ 771 parentPage: null, 772 773 /** 774 * The section on the parent page that is associated with this page. 775 * Can be null. 776 * @type {Element} 777 */ 778 associatedSection: null, 779 780 /** 781 * An array of controls that are associated with this page. The first 782 * control should be located on a top-level page. 783 * @type {OptionsPage} 784 */ 785 associatedControls: null, 786 787 /** 788 * Initializes page content. 789 */ 790 initializePage: function() {}, 791 792 /** 793 * Sets managed banner visibility state. 794 */ 795 setManagedBannerVisibility: function(visible) { 796 this.managed = visible; 797 if (this.visible) { 798 this.updateManagedBannerVisibility(); 799 } 800 }, 801 802 /** 803 * Updates managed banner visibility state. This function iterates over 804 * all input fields of a window and if any of these is marked as managed 805 * it triggers the managed banner to be visible. The banner can be enforced 806 * being on through the managed flag of this class but it can not be forced 807 * being off if managed items exist. 808 */ 809 updateManagedBannerVisibility: function() { 810 var bannerDiv = $('managed-prefs-banner'); 811 812 var hasManaged = this.managed; 813 if (!hasManaged) { 814 var inputElements = this.pageDiv.querySelectorAll('input'); 815 for (var i = 0, len = inputElements.length; i < len; i++) { 816 if (inputElements[i].managed) { 817 hasManaged = true; 818 break; 819 } 820 } 821 } 822 if (hasManaged) { 823 bannerDiv.hidden = false; 824 var height = window.getComputedStyle($('managed-prefs-banner')).height; 825 $('subpage-backdrop').style.top = height; 826 } else { 827 bannerDiv.hidden = true; 828 $('subpage-backdrop').style.top = '0'; 829 } 830 }, 831 832 /** 833 * Gets page visibility state. 834 */ 835 get visible() { 836 var page = $(this.pageDivName); 837 return page && page.ownerDocument.defaultView.getComputedStyle( 838 page).display == 'block'; 839 }, 840 841 /** 842 * Sets page visibility. 843 */ 844 set visible(visible) { 845 if ((this.visible && visible) || (!this.visible && !visible)) 846 return; 847 848 this.setContainerVisibility_(visible); 849 if (visible) { 850 this.pageDiv.classList.remove('hidden'); 851 852 if (this.tab) 853 this.tab.classList.add('navbar-item-selected'); 854 } else { 855 this.pageDiv.classList.add('hidden'); 856 857 if (this.tab) 858 this.tab.classList.remove('navbar-item-selected'); 859 } 860 861 OptionsPage.updatePageFreezeStates(); 862 863 // A subpage was shown or hidden. 864 if (!this.isOverlay && this.nestingLevel > 0) { 865 OptionsPage.updateSubpageBackdrop_(); 866 if (visible) { 867 // Scroll to the top of the newly-opened subpage. 868 window.scroll(document.body.scrollLeft, 0) 869 } 870 } 871 872 // The managed prefs banner is global, so after any visibility change 873 // update it based on the topmost page, not necessarily this page 874 // (e.g., if an ancestor is made visible after a child). 875 OptionsPage.updateManagedBannerVisibility(); 876 877 cr.dispatchPropertyChange(this, 'visible', visible, !visible); 878 }, 879 880 /** 881 * Shows or hides this page's container. 882 * @param {boolean} visible Whether the container should be visible or not. 883 * @private 884 */ 885 setContainerVisibility_: function(visible) { 886 var container = null; 887 if (this.isOverlay) { 888 container = $('overlay'); 889 } else { 890 var nestingLevel = this.nestingLevel; 891 if (nestingLevel > 0) 892 container = $('subpage-sheet-container-' + nestingLevel); 893 } 894 var isSubpage = !this.isOverlay; 895 896 if (!container || container.hidden != visible) 897 return; 898 899 if (visible) { 900 container.hidden = false; 901 if (isSubpage) { 902 var computedStyle = window.getComputedStyle(container); 903 container.style.WebkitPaddingStart = 904 parseInt(computedStyle.WebkitPaddingStart, 10) + 100 + 'px'; 905 } 906 // Separate animating changes from the removal of display:none. 907 window.setTimeout(function() { 908 container.classList.remove('transparent'); 909 if (isSubpage) 910 container.style.WebkitPaddingStart = ''; 911 }); 912 } else { 913 var self = this; 914 container.addEventListener('webkitTransitionEnd', function f(e) { 915 if (e.propertyName != 'opacity') 916 return; 917 container.removeEventListener('webkitTransitionEnd', f); 918 self.fadeCompleted_(container); 919 }); 920 container.classList.add('transparent'); 921 } 922 }, 923 924 /** 925 * Called when a container opacity transition finishes. 926 * @param {HTMLElement} container The container element. 927 * @private 928 */ 929 fadeCompleted_: function(container) { 930 if (container.classList.contains('transparent')) 931 container.hidden = true; 932 }, 933 934 /** 935 * Focuses the first control on the page. 936 */ 937 focusFirstElement: function() { 938 // Sets focus on the first interactive element in the page. 939 var focusElement = 940 this.pageDiv.querySelector('button, input, list, select'); 941 if (focusElement) 942 focusElement.focus(); 943 }, 944 945 /** 946 * The nesting level of this page. 947 * @type {number} The nesting level of this page (0 for top-level page) 948 */ 949 get nestingLevel() { 950 var level = 0; 951 var parent = this.parentPage; 952 while (parent) { 953 level++; 954 parent = parent.parentPage; 955 } 956 return level; 957 }, 958 959 /** 960 * Whether the page is considered 'sticky', such that it will 961 * remain a top-level page even if sub-pages change. 962 * @type {boolean} True if this page is sticky. 963 */ 964 get sticky() { 965 return false; 966 }, 967 968 /** 969 * Checks whether this page is an ancestor of the given page in terms of 970 * subpage nesting. 971 * @param {OptionsPage} page 972 * @return {boolean} True if this page is nested under |page| 973 */ 974 isAncestorOfPage: function(page) { 975 var parent = page.parentPage; 976 while (parent) { 977 if (parent == this) 978 return true; 979 parent = parent.parentPage; 980 } 981 return false; 982 }, 983 984 /** 985 * Whether it should be possible to show the page. 986 * @return {boolean} True if the page should be shown 987 */ 988 canShowPage: function() { 989 return true; 990 }, 991 992 /** 993 * Whether an overlay should be closed. Used by overlay implementation to 994 * handle special closing behaviors. 995 * @return {boolean} True if the overlay should be closed. 996 */ 997 shouldClose: function() { 998 return true; 999 }, 1000 }; 1001 1002 // Export 1003 return { 1004 OptionsPage: OptionsPage 1005 }; 1006 }); 1007