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 |