Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2014 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('cr.ui.pageManager', function() { | |
| 6 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager; | |
| 7 | |
| 8 /** | |
| 9 * PageManager contains a list of root Page and overlay Page objects and | |
| 10 * handles "navigation" by showing and hiding these pages and overlays. On | |
| 11 * initial load, PageManager can use the path to open the correct hierarchy | |
| 12 * of pages and overlay(s). Handlers for user events, like pressing buttons, | |
| 13 * can call into PageManager to open a particular overlay or cancel an | |
| 14 * existing overlay. | |
| 15 */ | |
| 16 var PageManager = { | |
| 17 /** | |
| 18 * True if page is served from a dialog. | |
| 19 * @type {boolean} | |
| 20 */ | |
| 21 isDialog: false, | |
| 22 | |
| 23 /** | |
| 24 * Offset of page container in pixels, to allow room for side menu. | |
| 25 * Simplified settings pages can override this if they don't use the menu. | |
| 26 * The default (155) comes from -webkit-margin-start in uber_shared.css | |
| 27 * TODO(michaelpg): Remove dependency on uber menu (crbug.com/313244). | |
| 28 * @type {number} | |
| 29 */ | |
| 30 horizontalOffset: 155, | |
| 31 | |
| 32 /** | |
| 33 * Root pages. Maps lower-case page names to the respective page object. | |
| 34 * @type {Array.Page} | |
|
Dan Beam
2014/07/30 01:00:02
!Object.<!Page> (you don't need to get crazy with
michaelpg
2014/07/30 21:42:20
or this?
!Object.<string, !Page>
Dan Beam
2014/07/31 02:21:32
there are no other keys than string in JS
michaelpg
2014/07/31 03:21:16
Still looks like the correct syntax for closure.
h
Dan Beam
2014/07/31 03:30:37
both work and both are ignored, annoyingly enough
| |
| 35 */ | |
| 36 registeredPages: {}, | |
| 37 | |
| 38 /** | |
| 39 * Pages which are meant to behave like modal dialogs. Maps lower-case | |
| 40 * overlay names to the respective overlay object. | |
| 41 * @type {Array.Page} | |
| 42 * @private | |
| 43 */ | |
| 44 registeredOverlayPages: {}, | |
| 45 | |
| 46 /** | |
| 47 * Initializes the complete page. | |
| 48 * @param {Page} defaultPage The page to be shown when no page is specified | |
|
Dan Beam
2014/07/30 01:00:02
cr.ui.page_manager.Page or Page? (be consistent ei
michaelpg
2014/07/30 21:42:20
Done. (fully qualified just to help show that Page
| |
| 49 * in the path. | |
| 50 */ | |
| 51 initialize: function(defaultPage) { | |
| 52 this.defaultPage_ = defaultPage; | |
| 53 | |
| 54 FocusOutlineManager.forDocument(document); | |
| 55 document.addEventListener('scroll', this.handleScroll_.bind(this)); | |
| 56 | |
| 57 // Trigger the scroll handler manually to set the initial state. | |
| 58 this.handleScroll_(); | |
| 59 | |
| 60 // Shake the dialog if the user clicks outside the dialog bounds. | |
| 61 var containers = [$('overlay-container-1'), $('overlay-container-2')]; | |
| 62 for (var i = 0; i < containers.length; i++) { | |
| 63 var overlay = containers[i]; | |
| 64 cr.ui.overlay.setupOverlay(overlay); | |
| 65 overlay.addEventListener('cancelOverlay', | |
| 66 this.cancelOverlay.bind(this)); | |
| 67 } | |
| 68 | |
| 69 cr.ui.overlay.globalInitialization(); | |
| 70 }, | |
| 71 | |
| 72 /** | |
| 73 * Registers new page. | |
| 74 * @param {cr.ui.page_manager.Page} page Page to register. | |
| 75 */ | |
| 76 register: function(page) { | |
| 77 this.registeredPages[page.name.toLowerCase()] = page; | |
| 78 page.initializePage(); | |
| 79 }, | |
| 80 | |
| 81 /** | |
| 82 * Registers a new Overlay page. | |
| 83 * @param {cr.ui.page_manager.Page} overlay Overlay to register. | |
| 84 * @param {cr.ui.page_manager.Page} parentPage Associated parent page for | |
| 85 * this overlay. | |
| 86 * @param {Array} associatedControls Array of control elements associated | |
| 87 * with this page. | |
| 88 */ | |
| 89 registerOverlay: function(overlay, | |
| 90 parentPage, | |
| 91 associatedControls) { | |
| 92 this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; | |
| 93 overlay.parentPage = parentPage; | |
| 94 if (associatedControls) { | |
| 95 overlay.associatedControls = associatedControls; | |
| 96 if (associatedControls.length) { | |
| 97 overlay.associatedSection = | |
| 98 this.findSectionForNode_(associatedControls[0]); | |
| 99 } | |
| 100 | |
| 101 // Sanity check. | |
| 102 for (var i = 0; i < associatedControls.length; ++i) { | |
| 103 assert(associatedControls[i], 'Invalid element passed.'); | |
| 104 } | |
| 105 } | |
| 106 | |
| 107 overlay.tab = undefined; | |
| 108 overlay.isOverlay = true; | |
| 109 | |
| 110 // Reverse the button strip for Windows and CrOS. See the documentation of | |
| 111 // Page.reverseButtonStripIfNecessary() for an explanation of why this is | |
| 112 // done. | |
| 113 if (cr.isWindows || cr.isChromeOS) | |
| 114 overlay.reverseButtonStripIfNecessary(); | |
| 115 | |
| 116 overlay.initializePage(); | |
| 117 }, | |
| 118 | |
| 119 /** | |
| 120 * Shows the default page. | |
| 121 * @param {boolean=} opt_updateHistory If we should update the history after | |
| 122 * showing the page (defaults to true). | |
| 123 */ | |
| 124 showDefaultPage: function(opt_updateHistory) { | |
| 125 assert(this.defaultPage_ instanceof cr.ui.pageManager.Page, | |
| 126 'PageManager must be initialized with a default page.'); | |
| 127 this.showPageByName(this.defaultPage_.name, opt_updateHistory); | |
| 128 }, | |
| 129 | |
| 130 /** | |
| 131 * Shows a registered page. This handles both root and overlay pages. | |
| 132 * @param {string} pageName Page name. | |
| 133 * @param {boolean=} opt_updateHistory If we should update the history after | |
| 134 * showing the page (defaults to true). | |
| 135 * @param {Object=} opt_propertyBag An optional bag of properties including | |
| 136 * replaceState (if history state should be replaced instead of pushed). | |
| 137 */ | |
| 138 showPageByName: function(pageName, | |
| 139 opt_updateHistory, | |
| 140 opt_propertyBag) { | |
| 141 opt_updateHistory = opt_updateHistory !== false; | |
|
Dan Beam
2014/07/30 01:00:02
it's weird to have an undefined argument end up be
michaelpg
2014/07/30 21:42:20
I think it's fair to have a "default" value of tru
| |
| 142 opt_propertyBag = opt_propertyBag || {}; | |
| 143 | |
| 144 // If a bubble is currently being shown, hide it. | |
| 145 this.hideBubble(); | |
| 146 | |
| 147 // Find the currently visible root-level page. | |
| 148 var rootPage = null; | |
| 149 for (var name in this.registeredPages) { | |
| 150 var page = this.registeredPages[name]; | |
| 151 if (page.visible && !page.parentPage) { | |
| 152 rootPage = page; | |
| 153 break; | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 // Find the target page. | |
| 158 var targetPage = this.registeredPages[pageName.toLowerCase()]; | |
| 159 if (!targetPage || !targetPage.canShowPage()) { | |
| 160 // If it's not a page, try it as an overlay. | |
| 161 if (!targetPage && this.showOverlay_(pageName, rootPage)) { | |
| 162 if (opt_updateHistory) | |
| 163 this.updateHistoryState_(!!opt_propertyBag.replaceState); | |
| 164 this.updateTitle_(); | |
| 165 return; | |
| 166 } | |
| 167 targetPage = this.defaultPage_; | |
| 168 } | |
| 169 | |
| 170 pageName = targetPage.name.toLowerCase(); | |
| 171 var targetPageWasVisible = targetPage.visible; | |
| 172 | |
| 173 // Determine if the root page is 'sticky', meaning that it | |
| 174 // shouldn't change when showing an overlay. This can happen for special | |
| 175 // pages like Search. | |
| 176 var isRootPageLocked = | |
| 177 rootPage && rootPage.sticky && targetPage.parentPage; | |
| 178 | |
| 179 var allPageNames = Array.prototype.concat.call( | |
| 180 Object.keys(this.registeredPages), | |
| 181 Object.keys(this.registeredOverlayPages)); | |
| 182 | |
| 183 // Notify pages if they will be hidden. | |
| 184 // TODO(michaelpg): Resolve code duplication. | |
|
Dan Beam
2014/07/30 01:00:02
seems unlikely that you'll be able to do this if s
michaelpg
2014/07/30 21:42:20
Yeah, but we can at least make a helper function t
| |
| 185 for (var i = 0; i < allPageNames.length; ++i) { | |
| 186 var name = allPageNames[i]; | |
| 187 var page = this.registeredPages[name] || | |
| 188 this.registeredOverlayPages[name]; | |
| 189 if (!page.parentPage && isRootPageLocked) | |
| 190 continue; | |
| 191 if (page.willHidePage && name != pageName && | |
| 192 !this.isAncestorOfPage(page, targetPage)) { | |
| 193 page.willHidePage(); | |
| 194 } | |
| 195 } | |
| 196 | |
| 197 // Update visibilities to show only the hierarchy of the target page. | |
| 198 for (var i = 0; i < allPageNames.length; ++i) { | |
| 199 var name = allPageNames[i]; | |
| 200 var page = this.registeredPages[name] || | |
| 201 this.registeredOverlayPages[name]; | |
| 202 if (!page.parentPage && isRootPageLocked) | |
| 203 continue; | |
| 204 page.visible = name == pageName || | |
| 205 this.isAncestorOfPage(page, targetPage); | |
| 206 } | |
| 207 | |
| 208 // Update the history and current location. | |
| 209 if (opt_updateHistory) | |
| 210 this.updateHistoryState_(!!opt_propertyBag.replaceState); | |
| 211 | |
| 212 // Update focus if any other control was focused on the previous page, | |
| 213 // or the previous page is not known. | |
| 214 if (document.activeElement != document.body && | |
| 215 (!rootPage || rootPage.pageDiv.contains(document.activeElement))) { | |
| 216 targetPage.focus(); | |
| 217 } | |
| 218 | |
| 219 // Notify pages if they were shown. | |
| 220 for (var i = 0; i < allPageNames.length; ++i) { | |
| 221 var name = allPageNames[i]; | |
| 222 var page = this.registeredPages[name] || | |
| 223 this.registeredOverlayPages[name]; | |
| 224 if (!page.parentPage && isRootPageLocked) | |
| 225 continue; | |
| 226 if (!targetPageWasVisible && page.didShowPage && | |
| 227 (name == pageName || this.isAncestorOfPage(page, targetPage))) { | |
| 228 page.didShowPage(); | |
| 229 } | |
| 230 } | |
| 231 | |
| 232 // Update the document title. Do this after didShowPage was called, in | |
| 233 // case a page decides to change its title. | |
| 234 this.updateTitle_(); | |
| 235 }, | |
| 236 | |
| 237 /** | |
| 238 * Returns the name of the page from the current path. | |
| 239 * @return {string} Name of the page specified by the current path. | |
| 240 */ | |
| 241 getPageNameFromPath: function() { | |
| 242 var path = location.pathname; | |
| 243 if (path.length <= 1) | |
| 244 return this.defaultPage_.name; | |
| 245 | |
| 246 // Skip starting slash and remove trailing slash (if any). | |
| 247 return path.slice(1).replace(/\/$/, ''); | |
| 248 }, | |
| 249 | |
| 250 /** | |
| 251 * Gets the level of the page. The root page (e.g., BrowserOptions) has | |
| 252 * level 0. | |
|
Dan Beam
2014/07/30 01:00:02
don't 4 \s indent doc continuations (but you /do/
michaelpg
2014/07/30 21:42:20
Done.
| |
| 253 * @return {number} How far down this page is from the root page. | |
| 254 */ | |
| 255 getNestingLevel: function(page) { | |
| 256 if (typeof page.nestingLevel != "undefined") | |
| 257 return page.nestingLevel; | |
| 258 | |
| 259 var level = 0; | |
| 260 var parent = page.parentPage; | |
| 261 while (parent) { | |
| 262 level++; | |
| 263 parent = parent.parentPage; | |
| 264 } | |
| 265 return level; | |
| 266 }, | |
| 267 | |
| 268 /** | |
| 269 * Checks whether one page is an ancestor of the other page in terms of | |
| 270 * subpage nesting. | |
| 271 * @param {Page} potential_ancestor The potential ancestor. | |
| 272 * @param {Page} potential_descendent The potential descendent. | |
| 273 * @return {boolean} True if |potential_descendent| is nested under | |
| 274 * |potential_descendent|. | |
| 275 */ | |
| 276 isAncestorOfPage: function(potential_ancestor, potential_descendent) { | |
|
Dan Beam
2014/07/30 01:00:02
nit: jsVarsLikeThis
michaelpg
2014/07/30 21:42:20
nitDone = true;
| |
| 277 var parent = potential_descendent.parentPage; | |
| 278 while (parent) { | |
| 279 if (parent == potential_ancestor) | |
| 280 return true; | |
| 281 parent = parent.parentPage; | |
| 282 } | |
| 283 return false; | |
| 284 }, | |
| 285 | |
| 286 /** | |
| 287 * Called when an page is shown or hidden to update the root page | |
| 288 * based on the page's new visibility. | |
| 289 * @param {Page} page The page being made visible or invisible. | |
| 290 */ | |
| 291 onPageVisibilityChanged: function(page) { | |
| 292 this.updateRootPageFreezeState(); | |
| 293 | |
| 294 if (page.isOverlay && !page.visible) | |
| 295 this.updateScrollPosition_(); | |
| 296 }, | |
| 297 | |
| 298 /** | |
| 299 * Returns the topmost visible page, or null if no page is visible. | |
| 300 * @return {Page} The topmost visible page. | |
| 301 */ | |
| 302 getTopmostVisiblePage: function() { | |
| 303 // Check overlays first since they're top-most if visible. | |
| 304 return this.getVisibleOverlay_() || | |
| 305 this.getTopmostVisibleNonOverlayPage_(); | |
| 306 }, | |
| 307 | |
| 308 /** | |
| 309 * Closes the visible overlay. Updates the history state after closing the | |
| 310 * overlay. | |
| 311 */ | |
| 312 closeOverlay: function() { | |
| 313 var overlay = this.getVisibleOverlay_(); | |
| 314 if (!overlay) | |
| 315 return; | |
| 316 | |
| 317 overlay.visible = false; | |
| 318 | |
| 319 if (overlay.didClosePage) | |
| 320 overlay.didClosePage(); | |
| 321 this.updateHistoryState_(false, {ignoreHash: true}); | |
| 322 this.updateTitle_(); | |
| 323 | |
| 324 this.restoreLastFocusedElement_(); | |
| 325 }, | |
| 326 | |
| 327 /** | |
| 328 * Closes all overlays and updates the history after each closed overlay. | |
| 329 */ | |
| 330 closeAllOverlays: function() { | |
| 331 while (this.isOverlayVisible_()) { | |
| 332 this.closeOverlay(); | |
| 333 } | |
| 334 }, | |
| 335 | |
| 336 /** | |
| 337 * Cancels (closes) the overlay, due to the user pressing <Esc>. | |
| 338 */ | |
| 339 cancelOverlay: function() { | |
| 340 // Blur the active element to ensure any changed pref value is saved. | |
| 341 document.activeElement.blur(); | |
| 342 var overlay = this.getVisibleOverlay_(); | |
| 343 if (!overlay) | |
| 344 return; | |
| 345 // Let the overlay handle the <Esc> if it wants to. | |
| 346 if (overlay.handleCancel) { | |
| 347 overlay.handleCancel(); | |
| 348 this.restoreLastFocusedElement_(); | |
| 349 } else { | |
| 350 this.closeOverlay(); | |
| 351 } | |
| 352 }, | |
| 353 | |
| 354 /** | |
| 355 * Shows an informational bubble displaying |content| and pointing at the | |
| 356 * |target| element. If |content| has focusable elements, they join the | |
| 357 * current page's tab order as siblings of |domSibling|. | |
| 358 * @param {HTMLDivElement} content The content of the bubble. | |
| 359 * @param {HTMLElement} target The element at which the bubble points. | |
| 360 * @param {HTMLElement} domSibling The element after which the bubble is | |
| 361 * added to the DOM. | |
| 362 * @param {cr.ui.ArrowLocation} location The arrow location. | |
| 363 */ | |
| 364 showBubble: function(content, target, domSibling, location) { | |
| 365 this.hideBubble(); | |
| 366 | |
| 367 var bubble = new cr.ui.AutoCloseBubble; | |
| 368 bubble.anchorNode = target; | |
| 369 bubble.domSibling = domSibling; | |
| 370 bubble.arrowLocation = location; | |
| 371 bubble.content = content; | |
| 372 bubble.show(); | |
| 373 this.bubble_ = bubble; | |
| 374 }, | |
| 375 | |
| 376 /** | |
| 377 * Hides the currently visible bubble, if any. | |
| 378 */ | |
| 379 hideBubble: function() { | |
| 380 if (this.bubble_) | |
| 381 this.bubble_.hide(); | |
| 382 }, | |
| 383 | |
| 384 /** | |
| 385 * Returns the currently visible bubble, or null if no bubble is visible. | |
| 386 * @return {AutoCloseBubble} The bubble currently being shown. | |
| 387 */ | |
| 388 getVisibleBubble: function() { | |
| 389 var bubble = this.bubble_; | |
| 390 return bubble && !bubble.hidden ? bubble : null; | |
| 391 }, | |
| 392 | |
| 393 /** | |
| 394 * Callback for window.onpopstate to handle back/forward navigations. | |
| 395 * @param {string} pageName The current page name. | |
| 396 * @param {Object} data State data pushed into history. | |
| 397 */ | |
| 398 setState: function(pageName, data) { | |
| 399 var currentOverlay = this.getVisibleOverlay_(); | |
| 400 var lowercaseName = pageName.toLowerCase(); | |
| 401 var newPage = this.registeredPages[lowercaseName] || | |
| 402 this.registeredOverlayPages[lowercaseName] || | |
| 403 this.defaultPage_; | |
| 404 if (currentOverlay && !this.isAncestorOfPage(currentOverlay, newPage)) { | |
| 405 currentOverlay.visible = false; | |
| 406 if (currentOverlay.didClosePage) currentOverlay.didClosePage(); | |
| 407 } | |
| 408 this.showPageByName(pageName, false); | |
| 409 }, | |
| 410 | |
| 411 | |
| 412 /** | |
| 413 * Whether the page is still loading (i.e. onload hasn't finished running). | |
| 414 * @return {boolean} Whether the page is still loading. | |
| 415 */ | |
| 416 isLoading: function() { | |
| 417 return document.documentElement.classList.contains('loading'); | |
| 418 }, | |
| 419 | |
| 420 /** | |
| 421 * Callback for window.onbeforeunload. Used to notify overlays that they | |
| 422 * will be closed. | |
| 423 */ | |
| 424 willClose: function() { | |
| 425 var overlay = this.getVisibleOverlay_(); | |
| 426 if (overlay && overlay.didClosePage) | |
| 427 overlay.didClosePage(); | |
| 428 }, | |
| 429 | |
| 430 /** | |
| 431 * Freezes/unfreezes the scroll position of the root page based on the | |
| 432 * current page stack. | |
| 433 */ | |
| 434 updateRootPageFreezeState: function() { | |
| 435 var topPage = this.getTopmostVisiblePage(); | |
| 436 if (topPage) | |
| 437 this.setRootPageFrozen_(topPage.isOverlay); | |
| 438 }, | |
| 439 | |
| 440 /** | |
| 441 * Change the horizontal offset used to reposition elements while showing an | |
| 442 * overlay from the default. | |
| 443 */ | |
| 444 setHorizontalOffset: function(value) { | |
| 445 this.horizontalOffset = value; | |
|
Dan Beam
2014/07/30 01:00:02
nit: i'd make a private horizontalOffset_ and pote
michaelpg
2014/07/30 21:42:20
I'll keep this as is for now: we'll probably want
| |
| 446 }, | |
| 447 | |
| 448 /** | |
| 449 * Shows a registered overlay page. Does not update history. | |
| 450 * @param {string} overlayName Page name. | |
| 451 * @param {Page} rootPage The currently visible root-level page. | |
| 452 * @return {boolean} Whether we showed an overlay. | |
| 453 * @private | |
| 454 */ | |
| 455 showOverlay_: function(overlayName, rootPage) { | |
| 456 var overlay = this.registeredOverlayPages[overlayName.toLowerCase()]; | |
| 457 if (!overlay || !overlay.canShowPage()) | |
| 458 return false; | |
| 459 | |
| 460 // Save the currently focused element in the page for restoration later. | |
| 461 var currentPage = this.getTopmostVisiblePage(); | |
| 462 if (currentPage) | |
| 463 currentPage.lastFocusedElement = document.activeElement; | |
| 464 | |
| 465 if ((!rootPage || !rootPage.sticky) && | |
| 466 overlay.parentPage && | |
| 467 !overlay.parentPage.visible) { | |
| 468 this.showPageByName(overlay.parentPage.name, false); | |
| 469 } | |
| 470 | |
| 471 if (!overlay.visible) { | |
| 472 overlay.visible = true; | |
| 473 if (overlay.didShowPage) | |
| 474 overlay.didShowPage(); | |
| 475 } | |
| 476 | |
| 477 // Change focus to the overlay if any other control was focused by | |
| 478 // keyboard before. Otherwise, no one should have focus. | |
| 479 if (document.activeElement != document.body) { | |
| 480 if (FocusOutlineManager.forDocument(document).visible) { | |
| 481 overlay.focus(); | |
| 482 } else if (!overlay.pageDiv.contains(document.activeElement)) { | |
| 483 document.activeElement.blur(); | |
| 484 } | |
| 485 } | |
| 486 | |
| 487 if ($('search-field') && $('search-field').value == '') { | |
| 488 var section = overlay.associatedSection; | |
| 489 if (section) | |
| 490 options.BrowserOptions.scrollToSection(section); | |
| 491 } | |
| 492 | |
| 493 return true; | |
| 494 }, | |
| 495 | |
| 496 /** | |
| 497 * Returns whether or not an overlay is visible. | |
| 498 * @return {boolean} True if an overlay is visible. | |
| 499 * @private | |
| 500 */ | |
| 501 isOverlayVisible_: function() { | |
| 502 return this.getVisibleOverlay_() != null; | |
| 503 }, | |
| 504 | |
| 505 /** | |
| 506 * Returns the currently visible overlay, or null if no page is visible. | |
| 507 * @return {Page} The visible overlay. | |
| 508 * @private | |
| 509 */ | |
| 510 getVisibleOverlay_: function() { | |
| 511 var topmostPage = null; | |
| 512 for (var name in this.registeredOverlayPages) { | |
| 513 var page = this.registeredOverlayPages[name]; | |
| 514 if (page.visible && | |
| 515 (!topmostPage || this.getNestingLevel(page) > | |
| 516 this.getNestingLevel(topmostPage))) { | |
| 517 topmostPage = page; | |
| 518 } | |
| 519 } | |
| 520 return topmostPage; | |
| 521 }, | |
| 522 | |
| 523 /** | |
| 524 * Hides the visible overlay. Does not affect the history state. | |
| 525 * @private | |
| 526 */ | |
| 527 hideOverlay_: function() { | |
| 528 var overlay = this.getVisibleOverlay_(); | |
| 529 if (overlay) | |
| 530 overlay.visible = false; | |
| 531 }, | |
| 532 | |
| 533 /** | |
| 534 * Returns the pages which are currently visible, ordered by nesting level | |
| 535 * (ascending). | |
| 536 * @return {Array.Page} The pages which are currently visible, ordered | |
| 537 * by nesting level (ascending). | |
| 538 * @private | |
| 539 */ | |
| 540 getVisiblePages_: function() { | |
| 541 var visiblePages = []; | |
| 542 for (var name in this.registeredPages) { | |
| 543 var page = this.registeredPages[name]; | |
| 544 if (page.visible) | |
| 545 visiblePages[this.getNestingLevel(page)] = page; | |
| 546 } | |
| 547 return visiblePages; | |
| 548 }, | |
| 549 | |
| 550 /** | |
| 551 * Returns the topmost visible page (overlays excluded). | |
| 552 * @return {Page} The topmost visible page aside from any overlays. | |
| 553 * @private | |
| 554 */ | |
| 555 getTopmostVisibleNonOverlayPage_: function() { | |
| 556 var topPage = null; | |
| 557 for (var name in this.registeredPages) { | |
| 558 var page = this.registeredPages[name]; | |
| 559 if (page.visible && | |
| 560 (!topPage || this.getNestingLevel(page) > | |
| 561 this.getNestingLevel(topPage))) { | |
| 562 topPage = page; | |
| 563 } | |
| 564 } | |
| 565 | |
| 566 return topPage; | |
| 567 }, | |
| 568 | |
| 569 /** | |
| 570 * Scrolls the page to the correct position (the top when opening an | |
| 571 * overlay, or the old scroll position a previously hidden overlay | |
| 572 * becomes visible). | |
| 573 * @private | |
| 574 */ | |
| 575 updateScrollPosition_: function() { | |
| 576 var container = $('page-container'); | |
| 577 var scrollTop = container.oldScrollTop || 0; | |
| 578 container.oldScrollTop = undefined; | |
| 579 window.scroll(scrollLeftForDocument(document), scrollTop); | |
| 580 }, | |
| 581 | |
| 582 /** | |
| 583 * Updates the title to title of the current page. | |
| 584 * @private | |
| 585 */ | |
| 586 updateTitle_: function() { | |
| 587 var page = this.getTopmostVisiblePage(); | |
| 588 // TODO(michaelpg): Remove dependency on uber (crbug.com/313244). | |
| 589 uber.setTitle(page.title); | |
| 590 }, | |
| 591 | |
| 592 /** | |
| 593 * Pushes the current page onto the history stack, replacing the current | |
| 594 * entry if appropriate. | |
| 595 * @param {boolean} replace If true, allow no history events to be created. | |
| 596 * @param {object=} opt_params A bag of optional params, including: | |
| 597 * {boolean} ignoreHash Whether to include the hash or not. | |
| 598 * @private | |
| 599 */ | |
| 600 updateHistoryState_: function(replace, opt_params) { | |
| 601 if (this.isDialog) | |
| 602 return; | |
| 603 | |
| 604 var page = this.getTopmostVisiblePage(); | |
| 605 var path = window.location.pathname + window.location.hash; | |
| 606 if (path) { | |
| 607 // Remove trailing slash. | |
| 608 path = path.slice(1).replace(/\/(?:#|$)/, ''); | |
| 609 } | |
| 610 | |
| 611 // If the page is already in history (the user may have clicked the same | |
| 612 // link twice, or this is the initial load), do nothing. | |
| 613 var hash = opt_params && opt_params.ignoreHash ? | |
| 614 '' : window.location.hash; | |
| 615 var newPath = (page == this.defaultPage_ ? '' : page.name) + hash; | |
| 616 if (path == newPath) | |
| 617 return; | |
| 618 | |
| 619 // TODO(michaelpg): Remove dependency on uber (crbug.com/313244). | |
| 620 var historyFunction = replace ? uber.replaceState : uber.pushState; | |
| 621 historyFunction.call(uber, {}, newPath); | |
| 622 }, | |
| 623 | |
| 624 /** | |
| 625 * Restores the last focused element on a given page. | |
| 626 * @private | |
| 627 */ | |
| 628 restoreLastFocusedElement_: function() { | |
| 629 var currentPage = this.getTopmostVisiblePage(); | |
| 630 if (currentPage.lastFocusedElement) | |
| 631 currentPage.lastFocusedElement.focus(); | |
| 632 }, | |
| 633 | |
| 634 /** | |
| 635 * Find an enclosing section for an element if it exists. | |
| 636 * @param {Element} element Element to search. | |
| 637 * @return {Element} The section element, or null. | |
| 638 * @private | |
| 639 */ | |
| 640 findSectionForNode_: function(node) { | |
| 641 while (node = node.parentNode) { | |
| 642 if (node.nodeName == 'SECTION') | |
| 643 return node; | |
| 644 } | |
| 645 return null; | |
| 646 }, | |
| 647 | |
| 648 /** | |
| 649 * Freezes/unfreezes the scroll position of the root page container. | |
| 650 * @param {boolean} freeze Whether the page should be frozen. | |
| 651 * @private | |
| 652 */ | |
| 653 setRootPageFrozen_: function(freeze) { | |
| 654 var container = $('page-container'); | |
| 655 if (container.classList.contains('frozen') == freeze) | |
| 656 return; | |
| 657 | |
| 658 if (freeze) { | |
| 659 // Lock the width, since auto width computation may change. | |
| 660 container.style.width = window.getComputedStyle(container).width; | |
| 661 container.oldScrollTop = scrollTopForDocument(document); | |
| 662 container.classList.add('frozen'); | |
| 663 var verticalPosition = | |
| 664 container.getBoundingClientRect().top - container.oldScrollTop; | |
| 665 container.style.top = verticalPosition + 'px'; | |
| 666 this.updateFrozenElementHorizontalPosition_(container); | |
| 667 } else { | |
| 668 container.classList.remove('frozen'); | |
| 669 container.style.top = ''; | |
| 670 container.style.left = ''; | |
| 671 container.style.right = ''; | |
| 672 container.style.width = ''; | |
| 673 } | |
| 674 }, | |
| 675 | |
| 676 /** | |
| 677 * Does a bounds check for the element on the given x, y client coordinates. | |
| 678 * @param {Element} e The DOM element. | |
| 679 * @param {number} x The client X to check. | |
| 680 * @param {number} y The client Y to check. | |
| 681 * @return {boolean} True if the point falls within the element's bounds. | |
| 682 * @private | |
| 683 */ | |
| 684 elementContainsPoint_: function(e, x, y) { | |
| 685 var clientRect = e.getBoundingClientRect(); | |
| 686 return x >= clientRect.left && x <= clientRect.right && | |
| 687 y >= clientRect.top && y <= clientRect.bottom; | |
| 688 }, | |
| 689 | |
| 690 /** | |
| 691 * Called when the page is scrolled; moves elements that are position:fixed | |
| 692 * but should only behave as if they are fixed for vertical scrolling. | |
| 693 * @private | |
| 694 */ | |
| 695 handleScroll_: function() { | |
| 696 this.updateAllFrozenElementPositions_(); | |
| 697 }, | |
| 698 | |
| 699 /** | |
| 700 * Updates all frozen pages to match the horizontal scroll position. | |
| 701 * @private | |
| 702 */ | |
| 703 updateAllFrozenElementPositions_: function() { | |
| 704 var frozenElements = document.querySelectorAll('.frozen'); | |
| 705 for (var i = 0; i < frozenElements.length; i++) | |
| 706 this.updateFrozenElementHorizontalPosition_(frozenElements[i]); | |
| 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 updateFrozenElementHorizontalPosition_: function(e) { | |
| 715 if (isRTL()) { | |
| 716 e.style.right = this.horizontalOffset + 'px'; | |
| 717 } else { | |
| 718 var scrollLeft = scrollLeftForDocument(document); | |
| 719 e.style.left = this.horizontalOffset - scrollLeft + 'px'; | |
| 720 } | |
| 721 }, | |
| 722 }; | |
| 723 | |
| 724 // Export | |
| 725 return { | |
| 726 PageManager: PageManager | |
| 727 }; | |
| 728 }); | |
| OLD | NEW |