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