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