Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 cr.define('options', function() { | 5 cr.define('options', function() { |
| 6 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager; | 6 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager; |
| 7 /** @const */ var PageManager = cr.ui.pageManager.PageManager; | |
| 7 | 8 |
| 8 ///////////////////////////////////////////////////////////////////////////// | 9 var OptionsPage = { |
| 9 // OptionsPage class: | 10 /** |
| 11 * This is the absolute difference maintained between standard and | |
| 12 * fixed-width font sizes. Refer http://crbug.com/91922. | |
| 13 * @const | |
| 14 */ | |
| 15 SIZE_DIFFERENCE_FIXED_STANDARD: 3, | |
| 10 | 16 |
| 11 /** | 17 /** |
| 12 * Base class for options page. | 18 * Initializes the complete options page. This will cause all C++ handlers |
| 13 * @constructor | 19 * to be invoked to do final setup. |
| 14 * @param {string} name Options page name. | 20 */ |
| 15 * @param {string} title Options page title, used for history. | 21 initialize: function() { |
| 16 * @extends {EventTarget} | 22 chrome.send('coreOptionsInitialize'); |
| 17 */ | 23 uber.onContentFrameLoaded(); |
| 18 function OptionsPage(name, title, pageDivName) { | 24 PageManager.initialize(BrowserOptions.getInstance()); |
|
Dan Beam
2014/07/31 02:21:32
eventually:
PageManager.addObserver(this);
and
michaelpg
2014/07/31 03:21:16
Yup, that's the goal!
Dan Beam
2014/07/31 03:30:37
w00t
| |
| 19 this.name = name; | 25 }, |
| 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 | 26 |
| 31 /** | 27 /** |
| 32 * This is the absolute difference maintained between standard and | 28 * Shows the tab contents for the given navigation tab. |
| 33 * fixed-width font sizes. Refer http://crbug.com/91922. | 29 * @param {!Element} tab The tab that the user clicked. |
| 34 * @const | 30 */ |
| 35 */ | 31 showTab: function(tab) { |
| 36 OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3; | 32 // Search parents until we find a tab, or the nav bar itself. This allows |
| 33 // tabs to have child nodes, e.g. labels in separately-styled spans. | |
| 34 while (tab && !tab.classList.contains('subpages-nav-tabs') && | |
| 35 !tab.classList.contains('tab')) { | |
| 36 tab = tab.parentNode; | |
| 37 } | |
| 38 if (!tab || !tab.classList.contains('tab')) | |
| 39 return; | |
| 37 | 40 |
| 38 /** | 41 // Find tab bar of the tab. |
| 39 * Offset of page container in pixels, to allow room for side menu. | 42 var tabBar = tab; |
| 40 * Simplified settings pages can override this if they don't use the menu. | 43 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { |
| 41 * The default (155) comes from -webkit-margin-start in uber_shared.css | 44 tabBar = tabBar.parentNode; |
| 42 * @private | 45 } |
| 43 */ | 46 if (!tabBar) |
| 44 OptionsPage.horizontalOffset = 155; | 47 return; |
| 45 | 48 |
| 46 /** | 49 if (tabBar.activeNavTab != null) { |
| 47 * Main level option pages. Maps lower-case page names to the respective page | 50 tabBar.activeNavTab.classList.remove('active-tab'); |
| 48 * object. | 51 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. |
| 49 * @protected | 52 remove('active-tab-contents'); |
| 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 if (!overlay) | |
| 359 return; | |
| 360 // Let the overlay handle the <Esc> if it wants to. | |
| 361 if (overlay.handleCancel) { | |
| 362 overlay.handleCancel(); | |
| 363 this.restoreLastFocusedElement_(); | |
| 364 } else { | |
| 365 this.closeOverlay(); | |
| 366 } | |
| 367 }; | |
| 368 | |
| 369 /** | |
| 370 * Hides the visible overlay. Does not affect the history state. | |
| 371 * @private | |
| 372 */ | |
| 373 OptionsPage.hideOverlay_ = function() { | |
| 374 var overlay = this.getVisibleOverlay_(); | |
| 375 if (overlay) | |
| 376 overlay.visible = false; | |
| 377 }; | |
| 378 | |
| 379 /** | |
| 380 * Returns the pages which are currently visible, ordered by nesting level | |
| 381 * (ascending). | |
| 382 * @return {Array.OptionPage} The pages which are currently visible, ordered | |
| 383 * by nesting level (ascending). | |
| 384 */ | |
| 385 OptionsPage.getVisiblePages_ = function() { | |
| 386 var visiblePages = []; | |
| 387 for (var name in this.registeredPages) { | |
| 388 var page = this.registeredPages[name]; | |
| 389 if (page.visible) | |
| 390 visiblePages[page.nestingLevel] = page; | |
| 391 } | |
| 392 return visiblePages; | |
| 393 }; | |
| 394 | |
| 395 /** | |
| 396 * Returns the topmost visible page (overlays excluded). | |
| 397 * @return {OptionPage} The topmost visible page aside any overlay. | |
| 398 * @private | |
| 399 */ | |
| 400 OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { | |
| 401 var topPage = null; | |
| 402 for (var name in this.registeredPages) { | |
| 403 var page = this.registeredPages[name]; | |
| 404 if (page.visible && | |
| 405 (!topPage || page.nestingLevel > topPage.nestingLevel)) | |
| 406 topPage = page; | |
| 407 } | |
| 408 | |
| 409 return topPage; | |
| 410 }; | |
| 411 | |
| 412 /** | |
| 413 * Returns the topmost visible page, or null if no page is visible. | |
| 414 * @return {OptionPage} The topmost visible page. | |
| 415 */ | |
| 416 OptionsPage.getTopmostVisiblePage = function() { | |
| 417 // Check overlays first since they're top-most if visible. | |
| 418 return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); | |
| 419 }; | |
| 420 | |
| 421 /** | |
| 422 * Returns the currently visible bubble, or null if no bubble is visible. | |
| 423 * @return {AutoCloseBubble} The bubble currently being shown. | |
| 424 */ | |
| 425 OptionsPage.getVisibleBubble = function() { | |
| 426 var bubble = OptionsPage.bubble_; | |
| 427 return bubble && !bubble.hidden ? bubble : null; | |
| 428 }; | |
| 429 | |
| 430 /** | |
| 431 * Shows an informational bubble displaying |content| and pointing at the | |
| 432 * |target| element. If |content| has focusable elements, they join the | |
| 433 * current page's tab order as siblings of |domSibling|. | |
| 434 * @param {HTMLDivElement} content The content of the bubble. | |
| 435 * @param {HTMLElement} target The element at which the bubble points. | |
| 436 * @param {HTMLElement} domSibling The element after which the bubble is added | |
| 437 * to the DOM. | |
| 438 * @param {cr.ui.ArrowLocation} location The arrow location. | |
| 439 */ | |
| 440 OptionsPage.showBubble = function(content, target, domSibling, location) { | |
| 441 OptionsPage.hideBubble(); | |
| 442 | |
| 443 var bubble = new cr.ui.AutoCloseBubble; | |
| 444 bubble.anchorNode = target; | |
| 445 bubble.domSibling = domSibling; | |
| 446 bubble.arrowLocation = location; | |
| 447 bubble.content = content; | |
| 448 bubble.show(); | |
| 449 OptionsPage.bubble_ = bubble; | |
| 450 }; | |
| 451 | |
| 452 /** | |
| 453 * Hides the currently visible bubble, if any. | |
| 454 */ | |
| 455 OptionsPage.hideBubble = function() { | |
| 456 if (OptionsPage.bubble_) | |
| 457 OptionsPage.bubble_.hide(); | |
| 458 }; | |
| 459 | |
| 460 /** | |
| 461 * Shows the tab contents for the given navigation tab. | |
| 462 * @param {!Element} tab The tab that the user clicked. | |
| 463 */ | |
| 464 OptionsPage.showTab = function(tab) { | |
| 465 // Search parents until we find a tab, or the nav bar itself. This allows | |
| 466 // tabs to have child nodes, e.g. labels in separately-styled spans. | |
| 467 while (tab && !tab.classList.contains('subpages-nav-tabs') && | |
| 468 !tab.classList.contains('tab')) { | |
| 469 tab = tab.parentNode; | |
| 470 } | |
| 471 if (!tab || !tab.classList.contains('tab')) | |
| 472 return; | |
| 473 | |
| 474 // Find tab bar of the tab. | |
| 475 var tabBar = tab; | |
| 476 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { | |
| 477 tabBar = tabBar.parentNode; | |
| 478 } | |
| 479 if (!tabBar) | |
| 480 return; | |
| 481 | |
| 482 if (tabBar.activeNavTab != null) { | |
| 483 tabBar.activeNavTab.classList.remove('active-tab'); | |
| 484 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. | |
| 485 remove('active-tab-contents'); | |
| 486 } | |
| 487 | |
| 488 tab.classList.add('active-tab'); | |
| 489 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); | |
| 490 tabBar.activeNavTab = tab; | |
| 491 }; | |
| 492 | |
| 493 /** | |
| 494 * Registers new options page. | |
| 495 * @param {OptionsPage} page Page to register. | |
| 496 */ | |
| 497 OptionsPage.register = function(page) { | |
| 498 this.registeredPages[page.name.toLowerCase()] = page; | |
| 499 page.initializePage(); | |
| 500 }; | |
| 501 | |
| 502 /** | |
| 503 * Find an enclosing section for an element if it exists. | |
| 504 * @param {Element} element Element to search. | |
| 505 * @return {OptionPage} The section element, or null. | |
| 506 * @private | |
| 507 */ | |
| 508 OptionsPage.findSectionForNode_ = function(node) { | |
| 509 while (node = node.parentNode) { | |
| 510 if (node.nodeName == 'SECTION') | |
| 511 return node; | |
| 512 } | |
| 513 return null; | |
| 514 }; | |
| 515 | |
| 516 /** | |
| 517 * Registers a new Overlay page. | |
| 518 * @param {OptionsPage} overlay Overlay to register. | |
| 519 * @param {OptionsPage} parentPage Associated parent page for this overlay. | |
| 520 * @param {Array} associatedControls Array of control elements associated with | |
| 521 * this page. | |
| 522 */ | |
| 523 OptionsPage.registerOverlay = function(overlay, | |
| 524 parentPage, | |
| 525 associatedControls) { | |
| 526 this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; | |
| 527 overlay.parentPage = parentPage; | |
| 528 if (associatedControls) { | |
| 529 overlay.associatedControls = associatedControls; | |
| 530 if (associatedControls.length) { | |
| 531 overlay.associatedSection = | |
| 532 this.findSectionForNode_(associatedControls[0]); | |
| 533 } | 53 } |
| 534 | 54 |
| 535 // Sanity check. | 55 tab.classList.add('active-tab'); |
| 536 for (var i = 0; i < associatedControls.length; ++i) { | 56 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); |
| 537 assert(associatedControls[i], 'Invalid element passed.'); | 57 tabBar.activeNavTab = tab; |
| 538 } | 58 }, |
| 539 } | |
| 540 | |
| 541 // Reverse the button strip for Windows and CrOS. See the documentation of | |
| 542 // reverseButtonStripIfNecessary_() for an explanation of why this is done. | |
| 543 if (cr.isWindows || cr.isChromeOS) | |
| 544 this.reverseButtonStripIfNecessary_(overlay); | |
| 545 | |
| 546 overlay.tab = undefined; | |
| 547 overlay.isOverlay = true; | |
| 548 overlay.initializePage(); | |
| 549 }; | |
| 550 | |
| 551 /** | |
| 552 * Reverses the child elements of a button strip if it hasn't already been | |
| 553 * reversed. This is necessary because WebKit does not alter the tab order for | |
| 554 * elements that are visually reversed using -webkit-box-direction: reverse, | |
| 555 * and the button order is reversed for views. See http://webk.it/62664 for | |
| 556 * more information. | |
| 557 * @param {Object} overlay The overlay containing the button strip to reverse. | |
| 558 * @private | |
| 559 */ | |
| 560 OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) { | |
| 561 var buttonStrips = | |
| 562 overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])'); | |
| 563 | |
| 564 // Reverse all button-strips in the overlay. | |
| 565 for (var j = 0; j < buttonStrips.length; j++) { | |
| 566 var buttonStrip = buttonStrips[j]; | |
| 567 | |
| 568 var childNodes = buttonStrip.childNodes; | |
| 569 for (var i = childNodes.length - 1; i >= 0; i--) | |
| 570 buttonStrip.appendChild(childNodes[i]); | |
| 571 | |
| 572 buttonStrip.setAttribute('reversed', ''); | |
| 573 } | |
| 574 }; | |
| 575 | |
| 576 /** | |
| 577 * Returns the name of the page from the current path. | |
| 578 */ | |
| 579 OptionsPage.getPageNameFromPath = function() { | |
| 580 var path = location.pathname; | |
| 581 if (path.length <= 1) | |
| 582 return this.getDefaultPage().name; | |
| 583 | |
| 584 // Skip starting slash and remove trailing slash (if any). | |
| 585 return path.slice(1).replace(/\/$/, ''); | |
| 586 }; | |
| 587 | |
| 588 /** | |
| 589 * Callback for window.onpopstate to handle back/forward navigations. | |
| 590 * @param {string} pageName The current page name. | |
| 591 * @param {Object} data State data pushed into history. | |
| 592 */ | |
| 593 OptionsPage.setState = function(pageName, data) { | |
| 594 var currentOverlay = this.getVisibleOverlay_(); | |
| 595 var lowercaseName = pageName.toLowerCase(); | |
| 596 var newPage = this.registeredPages[lowercaseName] || | |
| 597 this.registeredOverlayPages[lowercaseName] || | |
| 598 this.getDefaultPage(); | |
| 599 if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) { | |
| 600 currentOverlay.visible = false; | |
| 601 if (currentOverlay.didClosePage) currentOverlay.didClosePage(); | |
| 602 } | |
| 603 this.showPageByName(pageName, false); | |
| 604 }; | |
| 605 | |
| 606 /** | |
| 607 * Callback for window.onbeforeunload. Used to notify overlays that they will | |
| 608 * be closed. | |
| 609 */ | |
| 610 OptionsPage.willClose = function() { | |
| 611 var overlay = this.getVisibleOverlay_(); | |
| 612 if (overlay && overlay.didClosePage) | |
| 613 overlay.didClosePage(); | |
| 614 }; | |
| 615 | |
| 616 /** | |
| 617 * Freezes/unfreezes the scroll position of the root page container. | |
| 618 * @param {boolean} freeze Whether the page should be frozen. | |
| 619 * @private | |
| 620 */ | |
| 621 OptionsPage.setRootPageFrozen_ = function(freeze) { | |
| 622 var container = $('page-container'); | |
| 623 if (container.classList.contains('frozen') == freeze) | |
| 624 return; | |
| 625 | |
| 626 if (freeze) { | |
| 627 // Lock the width, since auto width computation may change. | |
| 628 container.style.width = window.getComputedStyle(container).width; | |
| 629 container.oldScrollTop = scrollTopForDocument(document); | |
| 630 container.classList.add('frozen'); | |
| 631 var verticalPosition = | |
| 632 container.getBoundingClientRect().top - container.oldScrollTop; | |
| 633 container.style.top = verticalPosition + 'px'; | |
| 634 this.updateFrozenElementHorizontalPosition_(container); | |
| 635 } else { | |
| 636 container.classList.remove('frozen'); | |
| 637 container.style.top = ''; | |
| 638 container.style.left = ''; | |
| 639 container.style.right = ''; | |
| 640 container.style.width = ''; | |
| 641 } | |
| 642 }; | |
| 643 | |
| 644 /** | |
| 645 * Freezes/unfreezes the scroll position of the root page based on the current | |
| 646 * page stack. | |
| 647 */ | |
| 648 OptionsPage.updateRootPageFreezeState = function() { | |
| 649 var topPage = OptionsPage.getTopmostVisiblePage(); | |
| 650 if (topPage) | |
| 651 this.setRootPageFrozen_(topPage.isOverlay); | |
| 652 }; | |
| 653 | |
| 654 /** | |
| 655 * Initializes the complete options page. This will cause all C++ handlers to | |
| 656 * be invoked to do final setup. | |
| 657 */ | |
| 658 OptionsPage.initialize = function() { | |
| 659 chrome.send('coreOptionsInitialize'); | |
| 660 uber.onContentFrameLoaded(); | |
| 661 FocusOutlineManager.forDocument(document); | |
| 662 document.addEventListener('scroll', this.handleScroll_.bind(this)); | |
| 663 | |
| 664 // Trigger the scroll handler manually to set the initial state. | |
| 665 this.handleScroll_(); | |
| 666 | |
| 667 // Shake the dialog if the user clicks outside the dialog bounds. | |
| 668 var containers = [$('overlay-container-1'), $('overlay-container-2')]; | |
| 669 for (var i = 0; i < containers.length; i++) { | |
| 670 var overlay = containers[i]; | |
| 671 cr.ui.overlay.setupOverlay(overlay); | |
| 672 overlay.addEventListener('cancelOverlay', | |
| 673 OptionsPage.cancelOverlay.bind(OptionsPage)); | |
| 674 } | |
| 675 | |
| 676 cr.ui.overlay.globalInitialization(); | |
| 677 }; | |
| 678 | |
| 679 /** | |
| 680 * Does a bounds check for the element on the given x, y client coordinates. | |
| 681 * @param {Element} e The DOM element. | |
| 682 * @param {number} x The client X to check. | |
| 683 * @param {number} y The client Y to check. | |
| 684 * @return {boolean} True if the point falls within the element's bounds. | |
| 685 * @private | |
| 686 */ | |
| 687 OptionsPage.elementContainsPoint_ = function(e, x, y) { | |
| 688 var clientRect = e.getBoundingClientRect(); | |
| 689 return x >= clientRect.left && x <= clientRect.right && | |
| 690 y >= clientRect.top && y <= clientRect.bottom; | |
| 691 }; | |
| 692 | |
| 693 /** | |
| 694 * Called when the page is scrolled; moves elements that are position:fixed | |
| 695 * but should only behave as if they are fixed for vertical scrolling. | |
| 696 * @private | |
| 697 */ | |
| 698 OptionsPage.handleScroll_ = function() { | |
| 699 this.updateAllFrozenElementPositions_(); | |
| 700 }; | |
| 701 | |
| 702 /** | |
| 703 * Updates all frozen pages to match the horizontal scroll position. | |
| 704 * @private | |
| 705 */ | |
| 706 OptionsPage.updateAllFrozenElementPositions_ = function() { | |
| 707 var frozenElements = document.querySelectorAll('.frozen'); | |
| 708 for (var i = 0; i < frozenElements.length; i++) | |
| 709 this.updateFrozenElementHorizontalPosition_(frozenElements[i]); | |
| 710 }; | |
| 711 | |
| 712 /** | |
| 713 * Updates the given frozen element to match the horizontal scroll position. | |
| 714 * @param {HTMLElement} e The frozen element to update. | |
| 715 * @private | |
| 716 */ | |
| 717 OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { | |
| 718 if (isRTL()) { | |
| 719 e.style.right = OptionsPage.horizontalOffset + 'px'; | |
| 720 } else { | |
| 721 var scrollLeft = scrollLeftForDocument(document); | |
| 722 e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px'; | |
| 723 } | |
| 724 }; | |
| 725 | |
| 726 /** | |
| 727 * Change the horizontal offset used to reposition elements while showing an | |
| 728 * overlay from the default. | |
| 729 */ | |
| 730 OptionsPage.setHorizontalOffset = function(value) { | |
| 731 OptionsPage.horizontalOffset = value; | |
| 732 }; | |
| 733 | |
| 734 OptionsPage.setClearPluginLSODataEnabled = function(enabled) { | |
| 735 if (enabled) { | |
| 736 document.documentElement.setAttribute( | |
| 737 'flashPluginSupportsClearSiteData', ''); | |
| 738 } else { | |
| 739 document.documentElement.removeAttribute( | |
| 740 'flashPluginSupportsClearSiteData'); | |
| 741 } | |
| 742 if (navigator.plugins['Shockwave Flash']) | |
| 743 document.documentElement.setAttribute('hasFlashPlugin', ''); | |
| 744 }; | |
| 745 | |
| 746 OptionsPage.setPepperFlashSettingsEnabled = function(enabled) { | |
| 747 if (enabled) { | |
| 748 document.documentElement.setAttribute( | |
| 749 'enablePepperFlashSettings', ''); | |
| 750 } else { | |
| 751 document.documentElement.removeAttribute( | |
| 752 'enablePepperFlashSettings'); | |
| 753 } | |
| 754 }; | |
| 755 | |
| 756 OptionsPage.setIsSettingsApp = function() { | |
| 757 document.documentElement.classList.add('settings-app'); | |
| 758 }; | |
| 759 | |
| 760 OptionsPage.isSettingsApp = function() { | |
| 761 return document.documentElement.classList.contains('settings-app'); | |
| 762 }; | |
| 763 | |
| 764 /** | |
| 765 * Whether the page is still loading (i.e. onload hasn't finished running). | |
| 766 * @return {boolean} Whether the page is still loading. | |
| 767 */ | |
| 768 OptionsPage.isLoading = function() { | |
| 769 return document.documentElement.classList.contains('loading'); | |
| 770 }; | |
| 771 | |
| 772 OptionsPage.prototype = { | |
| 773 __proto__: cr.EventTarget.prototype, | |
| 774 | 59 |
| 775 /** | 60 /** |
| 776 * The parent page of this option page, or null for top-level pages. | 61 * Shows or hides options for clearing Flash LSOs. |
| 777 * @type {OptionsPage} | 62 * @param {boolean} enabled Whether plugin data can be cleared. |
| 778 */ | 63 */ |
| 779 parentPage: null, | 64 setClearPluginLSODataEnabled: function(enabled) { |
| 65 if (enabled) { | |
| 66 document.documentElement.setAttribute( | |
| 67 'flashPluginSupportsClearSiteData', ''); | |
| 68 } else { | |
| 69 document.documentElement.removeAttribute( | |
| 70 'flashPluginSupportsClearSiteData'); | |
| 71 } | |
| 72 if (navigator.plugins['Shockwave Flash']) | |
| 73 document.documentElement.setAttribute('hasFlashPlugin', ''); | |
| 74 }, | |
| 780 | 75 |
| 781 /** | 76 /** |
| 782 * The section on the parent page that is associated with this page. | 77 * Shows or hides Pepper Flash settings. |
| 783 * Can be null. | 78 * @param {boolean} enabled Whether Pepper Flash settings should be enabled. |
| 784 * @type {Element} | |
| 785 */ | 79 */ |
| 786 associatedSection: null, | 80 setPepperFlashSettingsEnabled: function(enabled) { |
| 787 | 81 if (enabled) { |
| 788 /** | 82 document.documentElement.setAttribute( |
| 789 * An array of controls that are associated with this page. The first | 83 'enablePepperFlashSettings', ''); |
| 790 * control should be located on a top-level page. | 84 } else { |
| 791 * @type {OptionsPage} | 85 document.documentElement.removeAttribute( |
| 792 */ | 86 'enablePepperFlashSettings'); |
| 793 associatedControls: null, | |
| 794 | |
| 795 /** | |
| 796 * Initializes page content. | |
| 797 */ | |
| 798 initializePage: function() {}, | |
| 799 | |
| 800 /** | |
| 801 * Sets focus on the first focusable element. Override for a custom focus | |
| 802 * strategy. | |
| 803 */ | |
| 804 focus: function() { | |
| 805 // Do not change focus if any control on this page is already focused. | |
| 806 if (this.pageDiv.contains(document.activeElement)) | |
| 807 return; | |
| 808 | |
| 809 var elements = this.pageDiv.querySelectorAll( | |
| 810 'input, list, select, textarea, button'); | |
| 811 for (var i = 0; i < elements.length; i++) { | |
| 812 var element = elements[i]; | |
| 813 // Try to focus. If fails, then continue. | |
| 814 element.focus(); | |
| 815 if (document.activeElement == element) | |
| 816 return; | |
| 817 } | 87 } |
| 818 }, | 88 }, |
| 819 | 89 |
| 820 /** | 90 /** |
| 821 * Gets the container div for this page if it is an overlay. | 91 * Sets whether Settings is shown as a standalone page in a window for the |
| 822 * @type {HTMLElement} | 92 * app launcher settings "app". |
| 93 * @param {boolean} isSettingsApp Whether this page is shown standalone. | |
| 823 */ | 94 */ |
| 824 get container() { | 95 setIsSettingsApp: function(isSettingsApp) { |
| 825 assert(this.isOverlay); | 96 document.documentElement.classList.toggle('settings-app', isSettingsApp); |
| 826 return this.pageDiv.parentNode; | |
| 827 }, | 97 }, |
| 828 | 98 |
| 829 /** | 99 /** |
| 830 * Gets page visibility state. | 100 * Returns true if Settings is shown as an "app" (in a window by itself) |
| 831 * @type {boolean} | 101 * for the app launcher settings "app". |
| 102 * @return {boolean} Whether this page is shown standalone. | |
| 832 */ | 103 */ |
| 833 get visible() { | 104 isSettingsApp: function() { |
| 834 // If this is an overlay dialog it is no longer considered visible while | 105 return document.documentElement.classList.contains('settings-app'); |
| 835 // the overlay is fading out. See http://crbug.com/118629. | |
| 836 if (this.isOverlay && | |
| 837 this.container.classList.contains('transparent')) { | |
| 838 return false; | |
| 839 } | |
| 840 if (this.pageDiv.hidden) | |
| 841 return false; | |
| 842 return this.pageDiv.page == this; | |
| 843 }, | |
| 844 | |
| 845 /** | |
| 846 * Sets page visibility. | |
| 847 * @type {boolean} | |
| 848 */ | |
| 849 set visible(visible) { | |
| 850 if ((this.visible && visible) || (!this.visible && !visible)) | |
| 851 return; | |
| 852 | |
| 853 // If using an overlay, the visibility of the dialog is toggled at the | |
| 854 // same time as the overlay to show the dialog's out transition. This | |
| 855 // is handled in setOverlayVisible. | |
| 856 if (this.isOverlay) { | |
| 857 this.setOverlayVisible_(visible); | |
| 858 } else { | |
| 859 this.pageDiv.page = this; | |
| 860 this.pageDiv.hidden = !visible; | |
| 861 this.onVisibilityChanged_(); | |
| 862 } | |
| 863 | |
| 864 cr.dispatchPropertyChange(this, 'visible', visible, !visible); | |
| 865 }, | |
| 866 | |
| 867 /** | |
| 868 * Shows or hides an overlay (including any visible dialog). | |
| 869 * @param {boolean} visible Whether the overlay should be visible or not. | |
| 870 * @private | |
| 871 */ | |
| 872 setOverlayVisible_: function(visible) { | |
| 873 assert(this.isOverlay); | |
| 874 var pageDiv = this.pageDiv; | |
| 875 var container = this.container; | |
| 876 | |
| 877 if (visible) | |
| 878 uber.invokeMethodOnParent('beginInterceptingEvents'); | |
| 879 | |
| 880 if (container.hidden != visible) { | |
| 881 if (visible) { | |
| 882 // If the container is set hidden and then immediately set visible | |
| 883 // again, the fadeCompleted_ callback would cause it to be erroneously | |
| 884 // hidden again. Removing the transparent tag avoids that. | |
| 885 container.classList.remove('transparent'); | |
| 886 | |
| 887 // Hide all dialogs in this container since a different one may have | |
| 888 // been previously visible before fading out. | |
| 889 var pages = container.querySelectorAll('.page'); | |
| 890 for (var i = 0; i < pages.length; i++) | |
| 891 pages[i].hidden = true; | |
| 892 // Show the new dialog. | |
| 893 pageDiv.hidden = false; | |
| 894 pageDiv.page = this; | |
| 895 } | |
| 896 return; | |
| 897 } | |
| 898 | |
| 899 var self = this; | |
| 900 var loading = OptionsPage.isLoading(); | |
| 901 if (!loading) { | |
| 902 // TODO(flackr): Use an event delegate to avoid having to subscribe and | |
| 903 // unsubscribe for webkitTransitionEnd events. | |
| 904 container.addEventListener('webkitTransitionEnd', function f(e) { | |
| 905 var propName = e.propertyName; | |
| 906 if (e.target != e.currentTarget || | |
| 907 (propName && propName != 'opacity')) { | |
| 908 return; | |
| 909 } | |
| 910 container.removeEventListener('webkitTransitionEnd', f); | |
| 911 self.fadeCompleted_(); | |
| 912 }); | |
| 913 // -webkit-transition is 200ms. Let's wait for 400ms. | |
| 914 ensureTransitionEndEvent(container, 400); | |
| 915 } | |
| 916 | |
| 917 if (visible) { | |
| 918 container.hidden = false; | |
| 919 pageDiv.hidden = false; | |
| 920 pageDiv.page = this; | |
| 921 // NOTE: This is a hacky way to force the container to layout which | |
| 922 // will allow us to trigger the webkit transition. | |
| 923 container.scrollTop; | |
| 924 | |
| 925 this.pageDiv.removeAttribute('aria-hidden'); | |
| 926 if (this.parentPage) { | |
| 927 this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden', | |
| 928 true); | |
| 929 } | |
| 930 container.classList.remove('transparent'); | |
| 931 this.onVisibilityChanged_(); | |
| 932 } else { | |
| 933 // Kick change events for text fields. | |
| 934 if (pageDiv.contains(document.activeElement)) | |
| 935 document.activeElement.blur(); | |
| 936 container.classList.add('transparent'); | |
| 937 } | |
| 938 | |
| 939 if (loading) | |
| 940 this.fadeCompleted_(); | |
| 941 }, | |
| 942 | |
| 943 /** | |
| 944 * Called when a container opacity transition finishes. | |
| 945 * @private | |
| 946 */ | |
| 947 fadeCompleted_: function() { | |
| 948 if (this.container.classList.contains('transparent')) { | |
| 949 this.pageDiv.hidden = true; | |
| 950 this.container.hidden = true; | |
| 951 | |
| 952 if (this.parentPage) | |
| 953 this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden'); | |
| 954 | |
| 955 if (this.nestingLevel == 1) | |
| 956 uber.invokeMethodOnParent('stopInterceptingEvents'); | |
| 957 | |
| 958 this.onVisibilityChanged_(); | |
| 959 } | |
| 960 }, | |
| 961 | |
| 962 /** | |
| 963 * Called when a page is shown or hidden to update the root options page | |
| 964 * based on this page's visibility. | |
| 965 * @private | |
| 966 */ | |
| 967 onVisibilityChanged_: function() { | |
| 968 OptionsPage.updateRootPageFreezeState(); | |
| 969 | |
| 970 if (this.isOverlay && !this.visible) | |
| 971 OptionsPage.updateScrollPosition_(); | |
| 972 }, | |
| 973 | |
| 974 /** | |
| 975 * The nesting level of this page. | |
| 976 * @type {number} The nesting level of this page (0 for top-level page) | |
| 977 */ | |
| 978 get nestingLevel() { | |
| 979 var level = 0; | |
| 980 var parent = this.parentPage; | |
| 981 while (parent) { | |
| 982 level++; | |
| 983 parent = parent.parentPage; | |
| 984 } | |
| 985 return level; | |
| 986 }, | |
| 987 | |
| 988 /** | |
| 989 * Whether the page is considered 'sticky', such that it will | |
| 990 * remain a top-level page even if sub-pages change. | |
| 991 * @type {boolean} True if this page is sticky. | |
| 992 */ | |
| 993 get sticky() { | |
| 994 return false; | |
| 995 }, | |
| 996 | |
| 997 /** | |
| 998 * Checks whether this page is an ancestor of the given page in terms of | |
| 999 * subpage nesting. | |
| 1000 * @param {OptionsPage} page The potential descendent of this page. | |
| 1001 * @return {boolean} True if |page| is nested under this page. | |
| 1002 */ | |
| 1003 isAncestorOfPage: function(page) { | |
| 1004 var parent = page.parentPage; | |
| 1005 while (parent) { | |
| 1006 if (parent == this) | |
| 1007 return true; | |
| 1008 parent = parent.parentPage; | |
| 1009 } | |
| 1010 return false; | |
| 1011 }, | |
| 1012 | |
| 1013 /** | |
| 1014 * Whether it should be possible to show the page. | |
| 1015 * @return {boolean} True if the page should be shown. | |
| 1016 */ | |
| 1017 canShowPage: function() { | |
| 1018 return true; | |
| 1019 }, | 106 }, |
| 1020 }; | 107 }; |
| 1021 | 108 |
| 1022 // Export | 109 // Export |
| 1023 return { | 110 return { |
| 1024 OptionsPage: OptionsPage | 111 OptionsPage: OptionsPage |
| 1025 }; | 112 }; |
| 1026 }); | 113 }); |
| OLD | NEW |