1// Copyright (c) 2012 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 5cr.define('options', function() { 6 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager; 7 8 ///////////////////////////////////////////////////////////////////////////// 9 // OptionsPage class: 10 11 /** 12 * Base class for options page. 13 * @constructor 14 * @param {string} name Options page name. 15 * @param {string} title Options page title, used for history. 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 // |pageDiv.page| is set to the page object (this) when the page is visible 24 // to track which page is being shown when multiple pages can share the same 25 // underlying div. 26 this.pageDiv.page = null; 27 this.tab = null; 28 this.lastFocusedElement = null; 29 } 30 31 /** 32 * This is the absolute difference maintained between standard and 33 * fixed-width font sizes. Refer http://crbug.com/91922. 34 * @const 35 */ 36 OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3; 37 38 /** 39 * Offset of page container in pixels, to allow room for side menu. 40 * Simplified settings pages can override this if they don't use the menu. 41 * The default (155) comes from -webkit-margin-start in uber_shared.css 42 * @private 43 */ 44 OptionsPage.horizontalOffset = 155; 45 46 /** 47 * Main level option pages. Maps lower-case page names to the respective page 48 * object. 49 * @protected 50 */ 51 OptionsPage.registeredPages = {}; 52 53 /** 54 * Pages which are meant to behave like modal dialogs. Maps lower-case overlay 55 * names to the respective overlay object. 56 * @protected 57 */ 58 OptionsPage.registeredOverlayPages = {}; 59 60 /** 61 * Gets the default page (to be shown on initial load). 62 */ 63 OptionsPage.getDefaultPage = function() { 64 return BrowserOptions.getInstance(); 65 }; 66 67 /** 68 * Shows the default page. 69 */ 70 OptionsPage.showDefaultPage = function() { 71 this.navigateToPage(this.getDefaultPage().name); 72 }; 73 74 /** 75 * "Navigates" to a page, meaning that the page will be shown and the 76 * appropriate entry is placed in the history. 77 * @param {string} pageName Page name. 78 */ 79 OptionsPage.navigateToPage = function(pageName) { 80 this.showPageByName(pageName, true); 81 }; 82 83 /** 84 * Shows a registered page. This handles both top-level and overlay pages. 85 * @param {string} pageName Page name. 86 * @param {boolean} updateHistory True if we should update the history after 87 * showing the page. 88 * @param {Object=} opt_propertyBag An optional bag of properties including 89 * replaceState (if history state should be replaced instead of pushed). 90 * @private 91 */ 92 OptionsPage.showPageByName = function(pageName, 93 updateHistory, 94 opt_propertyBag) { 95 // If |opt_propertyBag| is non-truthy, homogenize to object. 96 opt_propertyBag = opt_propertyBag || {}; 97 98 // If a bubble is currently being shown, hide it. 99 this.hideBubble(); 100 101 // Find the currently visible root-level page. 102 var rootPage = null; 103 for (var name in this.registeredPages) { 104 var page = this.registeredPages[name]; 105 if (page.visible && !page.parentPage) { 106 rootPage = page; 107 break; 108 } 109 } 110 111 // Find the target page. 112 var targetPage = this.registeredPages[pageName.toLowerCase()]; 113 if (!targetPage || !targetPage.canShowPage()) { 114 // If it's not a page, try it as an overlay. 115 if (!targetPage && this.showOverlay_(pageName, rootPage)) { 116 if (updateHistory) 117 this.updateHistoryState_(!!opt_propertyBag.replaceState); 118 return; 119 } else { 120 targetPage = this.getDefaultPage(); 121 } 122 } 123 124 pageName = targetPage.name.toLowerCase(); 125 var targetPageWasVisible = targetPage.visible; 126 127 // Determine if the root page is 'sticky', meaning that it 128 // shouldn't change when showing an overlay. This can happen for special 129 // pages like Search. 130 var isRootPageLocked = 131 rootPage && rootPage.sticky && targetPage.parentPage; 132 133 var allPageNames = Array.prototype.concat.call( 134 Object.keys(this.registeredPages), 135 Object.keys(this.registeredOverlayPages)); 136 137 // Notify pages if they will be hidden. 138 for (var i = 0; i < allPageNames.length; ++i) { 139 var name = allPageNames[i]; 140 var page = this.registeredPages[name] || 141 this.registeredOverlayPages[name]; 142 if (!page.parentPage && isRootPageLocked) 143 continue; 144 if (page.willHidePage && name != pageName && 145 !page.isAncestorOfPage(targetPage)) { 146 page.willHidePage(); 147 } 148 } 149 150 // Update visibilities to show only the hierarchy of the target page. 151 for (var i = 0; i < allPageNames.length; ++i) { 152 var name = allPageNames[i]; 153 var page = this.registeredPages[name] || 154 this.registeredOverlayPages[name]; 155 if (!page.parentPage && isRootPageLocked) 156 continue; 157 page.visible = name == pageName || page.isAncestorOfPage(targetPage); 158 } 159 160 // Update the history and current location. 161 if (updateHistory) 162 this.updateHistoryState_(!!opt_propertyBag.replaceState); 163 164 // Update tab title. 165 this.setTitle_(targetPage.title); 166 167 // Update focus if any other control was focused on the previous page, 168 // or the previous page is not known. 169 if (document.activeElement != document.body && 170 (!rootPage || rootPage.pageDiv.contains(document.activeElement))) { 171 targetPage.focus(); 172 } 173 174 // Notify pages if they were shown. 175 for (var i = 0; i < allPageNames.length; ++i) { 176 var name = allPageNames[i]; 177 var page = this.registeredPages[name] || 178 this.registeredOverlayPages[name]; 179 if (!page.parentPage && isRootPageLocked) 180 continue; 181 if (!targetPageWasVisible && page.didShowPage && 182 (name == pageName || page.isAncestorOfPage(targetPage))) { 183 page.didShowPage(); 184 } 185 } 186 }; 187 188 /** 189 * Sets the title of the page. This is accomplished by calling into the 190 * parent page API. 191 * @param {string} title The title string. 192 * @private 193 */ 194 OptionsPage.setTitle_ = function(title) { 195 uber.invokeMethodOnParent('setTitle', {title: title}); 196 }; 197 198 /** 199 * Scrolls the page to the correct position (the top when opening an overlay, 200 * or the old scroll position a previously hidden overlay becomes visible). 201 * @private 202 */ 203 OptionsPage.updateScrollPosition_ = function() { 204 var container = $('page-container'); 205 var scrollTop = container.oldScrollTop || 0; 206 container.oldScrollTop = undefined; 207 window.scroll(scrollLeftForDocument(document), scrollTop); 208 }; 209 210 /** 211 * Pushes the current page onto the history stack, overriding the last page 212 * if it is the generic chrome://settings/. 213 * @param {boolean} replace If true, allow no history events to be created. 214 * @param {object=} opt_params A bag of optional params, including: 215 * {boolean} ignoreHash Whether to include the hash or not. 216 * @private 217 */ 218 OptionsPage.updateHistoryState_ = function(replace, opt_params) { 219 var page = this.getTopmostVisiblePage(); 220 var path = window.location.pathname + window.location.hash; 221 if (path) 222 path = path.slice(1).replace(/\/(?:#|$)/, ''); // Remove trailing slash. 223 224 // Update tab title. 225 this.setTitle_(page.title); 226 227 // The page is already in history (the user may have clicked the same link 228 // twice). Do nothing. 229 if (path == page.name && !OptionsPage.isLoading()) 230 return; 231 232 var hash = opt_params && opt_params.ignoreHash ? '' : window.location.hash; 233 234 // If settings are embedded, tell the outer page to set its "path" to the 235 // inner frame's path. 236 var outerPath = (page == this.getDefaultPage() ? '' : page.name) + hash; 237 uber.invokeMethodOnParent('setPath', {path: outerPath}); 238 239 // If there is no path, the current location is chrome://settings/. 240 // Override this with the new page. 241 var historyFunction = path && !replace ? window.history.pushState : 242 window.history.replaceState; 243 historyFunction.call(window.history, 244 {pageName: page.name}, 245 page.title, 246 '/' + page.name + hash); 247 }; 248 249 /** 250 * Shows a registered Overlay page. Does not update history. 251 * @param {string} overlayName Page name. 252 * @param {OptionPage} rootPage The currently visible root-level page. 253 * @return {boolean} whether we showed an overlay. 254 */ 255 OptionsPage.showOverlay_ = function(overlayName, rootPage) { 256 var overlay = this.registeredOverlayPages[overlayName.toLowerCase()]; 257 if (!overlay || !overlay.canShowPage()) 258 return false; 259 260 // Save the currently focused element in the page for restoration later. 261 var currentPage = this.getTopmostVisiblePage(); 262 if (currentPage) 263 currentPage.lastFocusedElement = document.activeElement; 264 265 if ((!rootPage || !rootPage.sticky) && 266 overlay.parentPage && 267 !overlay.parentPage.visible) { 268 this.showPageByName(overlay.parentPage.name, false); 269 } 270 271 if (!overlay.visible) { 272 overlay.visible = true; 273 if (overlay.didShowPage) overlay.didShowPage(); 274 } 275 276 // Update tab title. 277 this.setTitle_(overlay.title); 278 279 // Change focus to the overlay if any other control was focused by keyboard 280 // before. Otherwise, no one should have focus. 281 if (document.activeElement != document.body) { 282 if (FocusOutlineManager.forDocument(document).visible) { 283 overlay.focus(); 284 } else if (!overlay.pageDiv.contains(document.activeElement)) { 285 document.activeElement.blur(); 286 } 287 } 288 289 if ($('search-field').value == '') { 290 var section = overlay.associatedSection; 291 if (section) 292 options.BrowserOptions.scrollToSection(section); 293 } 294 295 return true; 296 }; 297 298 /** 299 * Returns whether or not an overlay is visible. 300 * @return {boolean} True if an overlay is visible. 301 * @private 302 */ 303 OptionsPage.isOverlayVisible_ = function() { 304 return this.getVisibleOverlay_() != null; 305 }; 306 307 /** 308 * Returns the currently visible overlay, or null if no page is visible. 309 * @return {OptionPage} The visible overlay. 310 */ 311 OptionsPage.getVisibleOverlay_ = function() { 312 var topmostPage = null; 313 for (var name in this.registeredOverlayPages) { 314 var page = this.registeredOverlayPages[name]; 315 if (page.visible && 316 (!topmostPage || page.nestingLevel > topmostPage.nestingLevel)) { 317 topmostPage = page; 318 } 319 } 320 return topmostPage; 321 }; 322 323 /** 324 * Restores the last focused element on a given page. 325 */ 326 OptionsPage.restoreLastFocusedElement_ = function() { 327 var currentPage = this.getTopmostVisiblePage(); 328 if (currentPage.lastFocusedElement) 329 currentPage.lastFocusedElement.focus(); 330 }; 331 332 /** 333 * Closes the visible overlay. Updates the history state after closing the 334 * overlay. 335 */ 336 OptionsPage.closeOverlay = function() { 337 var overlay = this.getVisibleOverlay_(); 338 if (!overlay) 339 return; 340 341 overlay.visible = false; 342 343 if (overlay.didClosePage) overlay.didClosePage(); 344 this.updateHistoryState_(false, {ignoreHash: true}); 345 346 this.restoreLastFocusedElement_(); 347 }; 348 349 /** 350 * Cancels (closes) the overlay, due to the user pressing <Esc>. 351 */ 352 OptionsPage.cancelOverlay = function() { 353 // Blur the active element to ensure any changed pref value is saved. 354 document.activeElement.blur(); 355 var overlay = this.getVisibleOverlay_(); 356 // Let the overlay handle the <Esc> if it wants to. 357 if (overlay.handleCancel) { 358 overlay.handleCancel(); 359 this.restoreLastFocusedElement_(); 360 } else { 361 this.closeOverlay(); 362 } 363 }; 364 365 /** 366 * Hides the visible overlay. Does not affect the history state. 367 * @private 368 */ 369 OptionsPage.hideOverlay_ = function() { 370 var overlay = this.getVisibleOverlay_(); 371 if (overlay) 372 overlay.visible = false; 373 }; 374 375 /** 376 * Returns the pages which are currently visible, ordered by nesting level 377 * (ascending). 378 * @return {Array.OptionPage} The pages which are currently visible, ordered 379 * by nesting level (ascending). 380 */ 381 OptionsPage.getVisiblePages_ = function() { 382 var visiblePages = []; 383 for (var name in this.registeredPages) { 384 var page = this.registeredPages[name]; 385 if (page.visible) 386 visiblePages[page.nestingLevel] = page; 387 } 388 return visiblePages; 389 }; 390 391 /** 392 * Returns the topmost visible page (overlays excluded). 393 * @return {OptionPage} The topmost visible page aside any overlay. 394 * @private 395 */ 396 OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { 397 var topPage = null; 398 for (var name in this.registeredPages) { 399 var page = this.registeredPages[name]; 400 if (page.visible && 401 (!topPage || page.nestingLevel > topPage.nestingLevel)) 402 topPage = page; 403 } 404 405 return topPage; 406 }; 407 408 /** 409 * Returns the topmost visible page, or null if no page is visible. 410 * @return {OptionPage} The topmost visible page. 411 */ 412 OptionsPage.getTopmostVisiblePage = function() { 413 // Check overlays first since they're top-most if visible. 414 return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); 415 }; 416 417 /** 418 * Returns the currently visible bubble, or null if no bubble is visible. 419 * @return {AutoCloseBubble} The bubble currently being shown. 420 */ 421 OptionsPage.getVisibleBubble = function() { 422 var bubble = OptionsPage.bubble_; 423 return bubble && !bubble.hidden ? bubble : null; 424 }; 425 426 /** 427 * Shows an informational bubble displaying |content| and pointing at the 428 * |target| element. If |content| has focusable elements, they join the 429 * current page's tab order as siblings of |domSibling|. 430 * @param {HTMLDivElement} content The content of the bubble. 431 * @param {HTMLElement} target The element at which the bubble points. 432 * @param {HTMLElement} domSibling The element after which the bubble is added 433 * to the DOM. 434 * @param {cr.ui.ArrowLocation} location The arrow location. 435 */ 436 OptionsPage.showBubble = function(content, target, domSibling, location) { 437 OptionsPage.hideBubble(); 438 439 var bubble = new cr.ui.AutoCloseBubble; 440 bubble.anchorNode = target; 441 bubble.domSibling = domSibling; 442 bubble.arrowLocation = location; 443 bubble.content = content; 444 bubble.show(); 445 OptionsPage.bubble_ = bubble; 446 }; 447 448 /** 449 * Hides the currently visible bubble, if any. 450 */ 451 OptionsPage.hideBubble = function() { 452 if (OptionsPage.bubble_) 453 OptionsPage.bubble_.hide(); 454 }; 455 456 /** 457 * Shows the tab contents for the given navigation tab. 458 * @param {!Element} tab The tab that the user clicked. 459 */ 460 OptionsPage.showTab = function(tab) { 461 // Search parents until we find a tab, or the nav bar itself. This allows 462 // tabs to have child nodes, e.g. labels in separately-styled spans. 463 while (tab && !tab.classList.contains('subpages-nav-tabs') && 464 !tab.classList.contains('tab')) { 465 tab = tab.parentNode; 466 } 467 if (!tab || !tab.classList.contains('tab')) 468 return; 469 470 // Find tab bar of the tab. 471 var tabBar = tab; 472 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { 473 tabBar = tabBar.parentNode; 474 } 475 if (!tabBar) 476 return; 477 478 if (tabBar.activeNavTab != null) { 479 tabBar.activeNavTab.classList.remove('active-tab'); 480 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. 481 remove('active-tab-contents'); 482 } 483 484 tab.classList.add('active-tab'); 485 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); 486 tabBar.activeNavTab = tab; 487 }; 488 489 /** 490 * Registers new options page. 491 * @param {OptionsPage} page Page to register. 492 */ 493 OptionsPage.register = function(page) { 494 this.registeredPages[page.name.toLowerCase()] = page; 495 page.initializePage(); 496 }; 497 498 /** 499 * Find an enclosing section for an element if it exists. 500 * @param {Element} element Element to search. 501 * @return {OptionPage} The section element, or null. 502 * @private 503 */ 504 OptionsPage.findSectionForNode_ = function(node) { 505 while (node = node.parentNode) { 506 if (node.nodeName == 'SECTION') 507 return node; 508 } 509 return null; 510 }; 511 512 /** 513 * Registers a new Overlay page. 514 * @param {OptionsPage} overlay Overlay to register. 515 * @param {OptionsPage} parentPage Associated parent page for this overlay. 516 * @param {Array} associatedControls Array of control elements associated with 517 * this page. 518 */ 519 OptionsPage.registerOverlay = function(overlay, 520 parentPage, 521 associatedControls) { 522 this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; 523 overlay.parentPage = parentPage; 524 if (associatedControls) { 525 overlay.associatedControls = associatedControls; 526 if (associatedControls.length) { 527 overlay.associatedSection = 528 this.findSectionForNode_(associatedControls[0]); 529 } 530 531 // Sanity check. 532 for (var i = 0; i < associatedControls.length; ++i) { 533 assert(associatedControls[i], 'Invalid element passed.'); 534 } 535 } 536 537 // Reverse the button strip for views. See the documentation of 538 // reverseButtonStripIfNecessary_() for an explanation of why this is done. 539 if (cr.isViews) 540 this.reverseButtonStripIfNecessary_(overlay); 541 542 overlay.tab = undefined; 543 overlay.isOverlay = true; 544 overlay.initializePage(); 545 }; 546 547 /** 548 * Reverses the child elements of a button strip if it hasn't already been 549 * reversed. This is necessary because WebKit does not alter the tab order for 550 * elements that are visually reversed using -webkit-box-direction: reverse, 551 * and the button order is reversed for views. See http://webk.it/62664 for 552 * more information. 553 * @param {Object} overlay The overlay containing the button strip to reverse. 554 * @private 555 */ 556 OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) { 557 var buttonStrips = 558 overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])'); 559 560 // Reverse all button-strips in the overlay. 561 for (var j = 0; j < buttonStrips.length; j++) { 562 var buttonStrip = buttonStrips[j]; 563 564 var childNodes = buttonStrip.childNodes; 565 for (var i = childNodes.length - 1; i >= 0; i--) 566 buttonStrip.appendChild(childNodes[i]); 567 568 buttonStrip.setAttribute('reversed', ''); 569 } 570 }; 571 572 /** 573 * Callback for window.onpopstate to handle back/forward navigations. 574 * @param {Object} data State data pushed into history. 575 */ 576 OptionsPage.setState = function(data) { 577 if (data && data.pageName) { 578 var currentOverlay = this.getVisibleOverlay_(); 579 var lowercaseName = data.pageName.toLowerCase(); 580 var newPage = this.registeredPages[lowercaseName] || 581 this.registeredOverlayPages[lowercaseName] || 582 this.getDefaultPage(); 583 if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) { 584 currentOverlay.visible = false; 585 if (currentOverlay.didClosePage) currentOverlay.didClosePage(); 586 } 587 this.showPageByName(data.pageName, false); 588 } 589 }; 590 591 /** 592 * Callback for window.onbeforeunload. Used to notify overlays that they will 593 * be closed. 594 */ 595 OptionsPage.willClose = function() { 596 var overlay = this.getVisibleOverlay_(); 597 if (overlay && overlay.didClosePage) 598 overlay.didClosePage(); 599 }; 600 601 /** 602 * Freezes/unfreezes the scroll position of the root page container. 603 * @param {boolean} freeze Whether the page should be frozen. 604 * @private 605 */ 606 OptionsPage.setRootPageFrozen_ = function(freeze) { 607 var container = $('page-container'); 608 if (container.classList.contains('frozen') == freeze) 609 return; 610 611 if (freeze) { 612 // Lock the width, since auto width computation may change. 613 container.style.width = window.getComputedStyle(container).width; 614 container.oldScrollTop = scrollTopForDocument(document); 615 container.classList.add('frozen'); 616 var verticalPosition = 617 container.getBoundingClientRect().top - container.oldScrollTop; 618 container.style.top = verticalPosition + 'px'; 619 this.updateFrozenElementHorizontalPosition_(container); 620 } else { 621 container.classList.remove('frozen'); 622 container.style.top = ''; 623 container.style.left = ''; 624 container.style.right = ''; 625 container.style.width = ''; 626 } 627 }; 628 629 /** 630 * Freezes/unfreezes the scroll position of the root page based on the current 631 * page stack. 632 */ 633 OptionsPage.updateRootPageFreezeState = function() { 634 var topPage = OptionsPage.getTopmostVisiblePage(); 635 if (topPage) 636 this.setRootPageFrozen_(topPage.isOverlay); 637 }; 638 639 /** 640 * Initializes the complete options page. This will cause all C++ handlers to 641 * be invoked to do final setup. 642 */ 643 OptionsPage.initialize = function() { 644 chrome.send('coreOptionsInitialize'); 645 uber.onContentFrameLoaded(); 646 FocusOutlineManager.forDocument(document); 647 document.addEventListener('scroll', this.handleScroll_.bind(this)); 648 649 // Trigger the scroll handler manually to set the initial state. 650 this.handleScroll_(); 651 652 // Shake the dialog if the user clicks outside the dialog bounds. 653 var containers = [$('overlay-container-1'), $('overlay-container-2')]; 654 for (var i = 0; i < containers.length; i++) { 655 var overlay = containers[i]; 656 cr.ui.overlay.setupOverlay(overlay); 657 overlay.addEventListener('cancelOverlay', 658 OptionsPage.cancelOverlay.bind(OptionsPage)); 659 } 660 661 cr.ui.overlay.globalInitialization(); 662 }; 663 664 /** 665 * Does a bounds check for the element on the given x, y client coordinates. 666 * @param {Element} e The DOM element. 667 * @param {number} x The client X to check. 668 * @param {number} y The client Y to check. 669 * @return {boolean} True if the point falls within the element's bounds. 670 * @private 671 */ 672 OptionsPage.elementContainsPoint_ = function(e, x, y) { 673 var clientRect = e.getBoundingClientRect(); 674 return x >= clientRect.left && x <= clientRect.right && 675 y >= clientRect.top && y <= clientRect.bottom; 676 }; 677 678 /** 679 * Called when the page is scrolled; moves elements that are position:fixed 680 * but should only behave as if they are fixed for vertical scrolling. 681 * @private 682 */ 683 OptionsPage.handleScroll_ = function() { 684 this.updateAllFrozenElementPositions_(); 685 }; 686 687 /** 688 * Updates all frozen pages to match the horizontal scroll position. 689 * @private 690 */ 691 OptionsPage.updateAllFrozenElementPositions_ = function() { 692 var frozenElements = document.querySelectorAll('.frozen'); 693 for (var i = 0; i < frozenElements.length; i++) 694 this.updateFrozenElementHorizontalPosition_(frozenElements[i]); 695 }; 696 697 /** 698 * Updates the given frozen element to match the horizontal scroll position. 699 * @param {HTMLElement} e The frozen element to update. 700 * @private 701 */ 702 OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { 703 if (isRTL()) { 704 e.style.right = OptionsPage.horizontalOffset + 'px'; 705 } else { 706 var scrollLeft = scrollLeftForDocument(document); 707 e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px'; 708 } 709 }; 710 711 /** 712 * Change the horizontal offset used to reposition elements while showing an 713 * overlay from the default. 714 */ 715 OptionsPage.setHorizontalOffset = function(value) { 716 OptionsPage.horizontalOffset = value; 717 }; 718 719 OptionsPage.setClearPluginLSODataEnabled = function(enabled) { 720 if (enabled) { 721 document.documentElement.setAttribute( 722 'flashPluginSupportsClearSiteData', ''); 723 } else { 724 document.documentElement.removeAttribute( 725 'flashPluginSupportsClearSiteData'); 726 } 727 if (navigator.plugins['Shockwave Flash']) 728 document.documentElement.setAttribute('hasFlashPlugin', ''); 729 }; 730 731 OptionsPage.setPepperFlashSettingsEnabled = function(enabled) { 732 if (enabled) { 733 document.documentElement.setAttribute( 734 'enablePepperFlashSettings', ''); 735 } else { 736 document.documentElement.removeAttribute( 737 'enablePepperFlashSettings'); 738 } 739 }; 740 741 OptionsPage.setIsSettingsApp = function() { 742 document.documentElement.classList.add('settings-app'); 743 }; 744 745 OptionsPage.isSettingsApp = function() { 746 return document.documentElement.classList.contains('settings-app'); 747 }; 748 749 /** 750 * Whether the page is still loading (i.e. onload hasn't finished running). 751 * @return {boolean} Whether the page is still loading. 752 */ 753 OptionsPage.isLoading = function() { 754 return document.documentElement.classList.contains('loading'); 755 }; 756 757 OptionsPage.prototype = { 758 __proto__: cr.EventTarget.prototype, 759 760 /** 761 * The parent page of this option page, or null for top-level pages. 762 * @type {OptionsPage} 763 */ 764 parentPage: null, 765 766 /** 767 * The section on the parent page that is associated with this page. 768 * Can be null. 769 * @type {Element} 770 */ 771 associatedSection: null, 772 773 /** 774 * An array of controls that are associated with this page. The first 775 * control should be located on a top-level page. 776 * @type {OptionsPage} 777 */ 778 associatedControls: null, 779 780 /** 781 * Initializes page content. 782 */ 783 initializePage: function() {}, 784 785 /** 786 * Sets focus on the first focusable element. Override for a custom focus 787 * strategy. 788 */ 789 focus: function() { 790 // Do not change focus if any control on this page is already focused. 791 if (this.pageDiv.contains(document.activeElement)) 792 return; 793 794 var elements = this.pageDiv.querySelectorAll( 795 'input, list, select, textarea, button'); 796 for (var i = 0; i < elements.length; i++) { 797 var element = elements[i]; 798 // Try to focus. If fails, then continue. 799 element.focus(); 800 if (document.activeElement == element) 801 return; 802 } 803 }, 804 805 /** 806 * Gets the container div for this page if it is an overlay. 807 * @type {HTMLElement} 808 */ 809 get container() { 810 assert(this.isOverlay); 811 return this.pageDiv.parentNode; 812 }, 813 814 /** 815 * Gets page visibility state. 816 * @type {boolean} 817 */ 818 get visible() { 819 // If this is an overlay dialog it is no longer considered visible while 820 // the overlay is fading out. See http://crbug.com/118629. 821 if (this.isOverlay && 822 this.container.classList.contains('transparent')) { 823 return false; 824 } 825 if (this.pageDiv.hidden) 826 return false; 827 return this.pageDiv.page == this; 828 }, 829 830 /** 831 * Sets page visibility. 832 * @type {boolean} 833 */ 834 set visible(visible) { 835 if ((this.visible && visible) || (!this.visible && !visible)) 836 return; 837 838 // If using an overlay, the visibility of the dialog is toggled at the 839 // same time as the overlay to show the dialog's out transition. This 840 // is handled in setOverlayVisible. 841 if (this.isOverlay) { 842 this.setOverlayVisible_(visible); 843 } else { 844 this.pageDiv.page = this; 845 this.pageDiv.hidden = !visible; 846 this.onVisibilityChanged_(); 847 } 848 849 cr.dispatchPropertyChange(this, 'visible', visible, !visible); 850 }, 851 852 /** 853 * Shows or hides an overlay (including any visible dialog). 854 * @param {boolean} visible Whether the overlay should be visible or not. 855 * @private 856 */ 857 setOverlayVisible_: function(visible) { 858 assert(this.isOverlay); 859 var pageDiv = this.pageDiv; 860 var container = this.container; 861 862 if (visible) 863 uber.invokeMethodOnParent('beginInterceptingEvents'); 864 865 if (container.hidden != visible) { 866 if (visible) { 867 // If the container is set hidden and then immediately set visible 868 // again, the fadeCompleted_ callback would cause it to be erroneously 869 // hidden again. Removing the transparent tag avoids that. 870 container.classList.remove('transparent'); 871 872 // Hide all dialogs in this container since a different one may have 873 // been previously visible before fading out. 874 var pages = container.querySelectorAll('.page'); 875 for (var i = 0; i < pages.length; i++) 876 pages[i].hidden = true; 877 // Show the new dialog. 878 pageDiv.hidden = false; 879 pageDiv.page = this; 880 } 881 return; 882 } 883 884 var self = this; 885 var loading = OptionsPage.isLoading(); 886 if (!loading) { 887 // TODO(flackr): Use an event delegate to avoid having to subscribe and 888 // unsubscribe for webkitTransitionEnd events. 889 container.addEventListener('webkitTransitionEnd', function f(e) { 890 if (e.target != e.currentTarget || e.propertyName != 'opacity') 891 return; 892 container.removeEventListener('webkitTransitionEnd', f); 893 self.fadeCompleted_(); 894 }); 895 } 896 897 if (visible) { 898 container.hidden = false; 899 pageDiv.hidden = false; 900 pageDiv.page = this; 901 // NOTE: This is a hacky way to force the container to layout which 902 // will allow us to trigger the webkit transition. 903 container.scrollTop; 904 905 this.pageDiv.removeAttribute('aria-hidden'); 906 if (this.parentPage) { 907 this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden', 908 true); 909 } 910 container.classList.remove('transparent'); 911 this.onVisibilityChanged_(); 912 } else { 913 // Kick change events for text fields. 914 if (pageDiv.contains(document.activeElement)) 915 document.activeElement.blur(); 916 container.classList.add('transparent'); 917 } 918 919 if (loading) 920 this.fadeCompleted_(); 921 }, 922 923 /** 924 * Called when a container opacity transition finishes. 925 * @private 926 */ 927 fadeCompleted_: function() { 928 if (this.container.classList.contains('transparent')) { 929 this.pageDiv.hidden = true; 930 this.container.hidden = true; 931 932 if (this.parentPage) 933 this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden'); 934 935 if (this.nestingLevel == 1) 936 uber.invokeMethodOnParent('stopInterceptingEvents'); 937 938 this.onVisibilityChanged_(); 939 } 940 }, 941 942 /** 943 * Called when a page is shown or hidden to update the root options page 944 * based on this page's visibility. 945 * @private 946 */ 947 onVisibilityChanged_: function() { 948 OptionsPage.updateRootPageFreezeState(); 949 950 if (this.isOverlay && !this.visible) 951 OptionsPage.updateScrollPosition_(); 952 }, 953 954 /** 955 * The nesting level of this page. 956 * @type {number} The nesting level of this page (0 for top-level page) 957 */ 958 get nestingLevel() { 959 var level = 0; 960 var parent = this.parentPage; 961 while (parent) { 962 level++; 963 parent = parent.parentPage; 964 } 965 return level; 966 }, 967 968 /** 969 * Whether the page is considered 'sticky', such that it will 970 * remain a top-level page even if sub-pages change. 971 * @type {boolean} True if this page is sticky. 972 */ 973 get sticky() { 974 return false; 975 }, 976 977 /** 978 * Checks whether this page is an ancestor of the given page in terms of 979 * subpage nesting. 980 * @param {OptionsPage} page The potential descendent of this page. 981 * @return {boolean} True if |page| is nested under this page. 982 */ 983 isAncestorOfPage: function(page) { 984 var parent = page.parentPage; 985 while (parent) { 986 if (parent == this) 987 return true; 988 parent = parent.parentPage; 989 } 990 return false; 991 }, 992 993 /** 994 * Whether it should be possible to show the page. 995 * @return {boolean} True if the page should be shown. 996 */ 997 canShowPage: function() { 998 return true; 999 }, 1000 }; 1001 1002 // Export 1003 return { 1004 OptionsPage: OptionsPage 1005 }; 1006}); 1007