| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * Responds to route changes by expanding, collapsing, or scrolling to sections |
| * on the page. Expanded sections take up the full height of the container. At |
| * most one section should be expanded at any given time. |
| * @polymerBehavior MainPageBehavior |
| */ |
| const MainPageBehaviorImpl = { |
| properties: { |
| /** |
| * Help CSS to alter style during the horizontal swipe animation. |
| * Note that this is unrelated to the |currentAnimation_| (which refers to |
| * the vertical expand animation). |
| */ |
| isSubpageAnimating: { |
| reflectToAttribute: true, |
| type: Boolean, |
| }, |
| |
| /** |
| * Whether a search operation is in progress or previous search results are |
| * being displayed. |
| * @private {boolean} |
| */ |
| inSearchMode: { |
| type: Boolean, |
| value: false, |
| observer: 'inSearchModeChanged_', |
| }, |
| }, |
| |
| /** @type {?HTMLElement} The scrolling container. */ |
| scroller: null, |
| |
| listeners: {'neon-animation-finish': 'onNeonAnimationFinish_'}, |
| |
| /** @override */ |
| attached: function() { |
| this.scroller = this.domHost ? this.domHost.parentNode : document.body; |
| }, |
| |
| /** |
| * Remove the is-animating attribute once the animation is complete. |
| * This may catch animations finishing more often than needed, which is not |
| * known to cause any issues (e.g. when animating from a shallower page to a |
| * deeper page; or when transitioning to the main page). |
| * @private |
| */ |
| onNeonAnimationFinish_: function() { |
| this.isSubpageAnimating = false; |
| }, |
| |
| /** |
| * @param {!settings.Route} newRoute |
| * @param {settings.Route} oldRoute |
| */ |
| currentRouteChanged: function(newRoute, oldRoute) { |
| const oldRouteWasSection = !!oldRoute && !!oldRoute.parent && |
| !!oldRoute.section && oldRoute.parent.section != oldRoute.section; |
| |
| if (this.scroller) { |
| // When navigating from a section to the root route, we just need to |
| // scroll to the top, and can early exit afterwards. |
| if (oldRouteWasSection && newRoute == settings.routes.BASIC) { |
| this.scroller.scrollTop = 0; |
| return; |
| } |
| |
| // When navigating to the About page, we need to scroll to the top, and |
| // still do the rest of section management. |
| if (newRoute == settings.routes.ABOUT) |
| this.scroller.scrollTop = 0; |
| } |
| |
| // Scroll to the section except for back/forward. Also scroll for any |
| // in-page back/forward navigations (from a section or the root page). |
| // Also always scroll when coming from either the About or root page. |
| const scrollToSection = !settings.lastRouteChangeWasPopstate() || |
| oldRouteWasSection || oldRoute == settings.routes.BASIC || |
| oldRoute == settings.routes.ABOUT; |
| |
| // TODO(dschuyler): This doesn't set the flag in the case of going to or |
| // from the main page. It seems sensible to set the flag in those cases, |
| // unfortunately bug 708465 happens. Figure out why that is and then set |
| // this flag more broadly. |
| if (oldRoute && oldRoute.isSubpage() && newRoute.isSubpage()) |
| this.isSubpageAnimating = true; |
| |
| // For previously uncreated pages (including on first load), allow the page |
| // to render before scrolling to or expanding the section. |
| if (!oldRoute) { |
| this.fire('hide-container'); |
| setTimeout(() => { |
| this.fire('show-container'); |
| this.tryTransitionToSection_(scrollToSection, true); |
| }); |
| } else if (this.scrollHeight == 0) { |
| setTimeout(this.tryTransitionToSection_.bind(this, scrollToSection)); |
| } else { |
| this.tryTransitionToSection_(scrollToSection); |
| } |
| }, |
| |
| /** |
| * When exiting search mode, we need to make another attempt to scroll to |
| * the correct section, since it has just been re-rendered. |
| * @private |
| */ |
| inSearchModeChanged_: function(inSearchMode) { |
| if (!this.isAttached) |
| return; |
| |
| if (!inSearchMode) |
| this.tryTransitionToSection_(!settings.lastRouteChangeWasPopstate()); |
| }, |
| |
| /** |
| * If possible, transitions to the current route's section (by expanding or |
| * scrolling to it). If another transition is running, finishes or cancels |
| * that one, then schedules this function again. This ensures the current |
| * section is quickly shown, without getting the page into a broken state -- |
| * if currentRoute changes in between calls, just transition to the new route. |
| * @param {boolean} scrollToSection |
| * @param {boolean=} immediate Whether to instantly expand instead of animate. |
| * @private |
| */ |
| tryTransitionToSection_: function(scrollToSection, immediate) { |
| const currentRoute = settings.getCurrentRoute(); |
| const currentSection = this.getSection(currentRoute.section); |
| |
| // If an animation is already playing, try finishing or canceling it. |
| if (this.currentAnimation_) { |
| this.maybeStopCurrentAnimation_(); |
| // Either way, this function will be called again once the current |
| // animation ends. |
| return; |
| } |
| |
| let promise; |
| const expandedSection = /** @type {?SettingsSectionElement} */ ( |
| this.$$('settings-section.expanded')); |
| if (expandedSection) { |
| // If the section shouldn't be expanded, collapse it. |
| if (!currentRoute.isSubpage() || expandedSection != currentSection) { |
| promise = this.collapseSection_(expandedSection); |
| } else { |
| // Scroll to top while sliding to another subpage. |
| this.scroller.scrollTop = 0; |
| } |
| } else if (currentSection) { |
| // Expand the section into a subpage or scroll to it on the main page. |
| if (currentRoute.isSubpage()) { |
| if (immediate) |
| this.expandSectionImmediate_(currentSection); |
| else |
| promise = this.expandSection_(currentSection); |
| } else if (scrollToSection) { |
| currentSection.show(); |
| } |
| } else if ( |
| this.tagName == 'SETTINGS-BASIC-PAGE' && settings.routes.ADVANCED && |
| settings.routes.ADVANCED.contains(currentRoute) && |
| // Need to exclude routes that correspond to 'non-sectioned' children of |
| // ADVANCED, otherwise tryTransitionToSection_ will recurse endlessly. |
| !currentRoute.isNavigableDialog) { |
| assert(currentRoute.section); |
| // Hide the container again while Advanced Page template is being loaded. |
| this.fire('hide-container'); |
| promise = this.$$('#advancedPageTemplate').get(); |
| } |
| |
| // When this animation ends, another may be necessary. Call this function |
| // again after the promise resolves. |
| if (promise) { |
| promise.then(this.tryTransitionToSection_.bind(this, scrollToSection)) |
| .then(() => { |
| this.fire('show-container'); |
| }); |
| } |
| }, |
| |
| /** |
| * If the current animation is inconsistent with the current route, stops the |
| * animation by finishing or canceling it so the new route can be animated to. |
| * @private |
| */ |
| maybeStopCurrentAnimation_: function() { |
| const currentRoute = settings.getCurrentRoute(); |
| const animatingSection = /** @type {?SettingsSectionElement} */ ( |
| this.$$('settings-section.expanding, settings-section.collapsing')); |
| assert(animatingSection); |
| |
| if (animatingSection.classList.contains('expanding')) { |
| // Cancel the animation to go back to the main page if the animating |
| // section shouldn't be expanded. |
| if (animatingSection.section != currentRoute.section || |
| !currentRoute.isSubpage()) { |
| this.currentAnimation_.cancel(); |
| } |
| // Otherwise, let the expand animation continue. |
| return; |
| } |
| |
| assert(animatingSection.classList.contains('collapsing')); |
| if (!currentRoute.isSubpage()) |
| return; |
| |
| // If the collapsing section actually matches the current route's section, |
| // we can just cancel the animation to re-expand the section. |
| if (animatingSection.section == currentRoute.section) { |
| this.currentAnimation_.cancel(); |
| return; |
| } |
| |
| // The current route is a subpage, so that section needs to expand. |
| // Immediately finish the current collapse animation so that can happen. |
| this.currentAnimation_.finish(); |
| }, |
| |
| /** |
| * Immediately expand the card in |section| to fill the page. |
| * @param {!SettingsSectionElement} section |
| * @private |
| */ |
| expandSectionImmediate_: function(section) { |
| assert(this.scroller); |
| section.immediateExpand(this.scroller); |
| this.finishedExpanding_(section); |
| // TODO(scottchen): iron-list inside subpages need this to render correctly. |
| this.fire('resize'); |
| }, |
| |
| /** |
| * Animates the card in |section|, expanding it to fill the page. |
| * @param {!SettingsSectionElement} section |
| * @return {!Promise} Resolved when the transition is finished or canceled. |
| * @private |
| */ |
| expandSection_: function(section) { |
| assert(this.scroller); |
| if (!section.canAnimateExpand()) { |
| // Try to wait for the section to be created. |
| return new Promise(function(resolve, reject) { |
| setTimeout(resolve); |
| }); |
| } |
| |
| // Save the scroller position before freezing it. |
| this.origScrollTop_ = this.scroller.scrollTop; |
| this.fire('freeze-scroll', true); |
| |
| // Freeze the section's height so its card can be removed from the flow. |
| section.setFrozen(true); |
| |
| this.currentAnimation_ = section.animateExpand(this.scroller); |
| |
| return this.currentAnimation_.finished |
| .then( |
| () => { |
| this.finishedExpanding_(section); |
| }, |
| () => { |
| // The animation was canceled; restore the section and scroll |
| // position. |
| section.setFrozen(false); |
| this.scroller.scrollTop = this.origScrollTop_; |
| }) |
| .then(() => { |
| this.fire('freeze-scroll', false); |
| this.currentAnimation_ = null; |
| }); |
| }, |
| |
| /** @private */ |
| finishedExpanding_: function(section) { |
| // Hide other sections and scroll to the top of the subpage. |
| this.classList.add('showing-subpage'); |
| this.toggleOtherSectionsHidden_(section.section, true); |
| this.scroller.scrollTop = 0; |
| section.setFrozen(false); |
| |
| // Notify that the page is fully expanded. |
| this.fire('subpage-expand'); |
| }, |
| |
| /** |
| * Animates the card in |section|, collapsing it back into its section. |
| * @param {!SettingsSectionElement} section |
| * @return {!Promise} Resolved when the transition is finished or canceled. |
| */ |
| collapseSection_: function(section) { |
| assert(this.scroller); |
| assert(section.classList.contains('expanded')); |
| |
| // Don't animate the collapse if we are transitioning between Basic/Advanced |
| // and About, since the section won't be visible. |
| const needAnimate = |
| settings.routes.ABOUT.contains(settings.getCurrentRoute()) == |
| (section.domHost.tagName == 'SETTINGS-ABOUT-PAGE'); |
| |
| // Animate the collapse if the section knows the original height, except |
| // when switching between Basic/Advanced and About. |
| const shouldAnimateCollapse = needAnimate && section.canAnimateCollapse(); |
| if (shouldAnimateCollapse) { |
| this.fire('freeze-scroll', true); |
| // Do the initial collapse setup, which takes the section out of the flow, |
| // before showing everything. |
| section.setUpAnimateCollapse(this.scroller); |
| } else { |
| section.classList.remove('expanded'); |
| } |
| |
| // Show everything. |
| this.toggleOtherSectionsHidden_(section.section, false); |
| this.classList.remove('showing-subpage'); |
| |
| if (!shouldAnimateCollapse) { |
| // Finish by restoring the section into the page. |
| section.setFrozen(false); |
| return Promise.resolve(); |
| } |
| |
| // Play the actual collapse animation. |
| return new Promise((resolve, reject) => { |
| // Wait for the other sections to show up so we can scroll properly. |
| setTimeout(() => { |
| const newSection = settings.getCurrentRoute().section && |
| this.getSection(settings.getCurrentRoute().section); |
| |
| // Scroll to the new section or the original position. |
| if (newSection && !settings.lastRouteChangeWasPopstate() && |
| !settings.getCurrentRoute().isSubpage()) { |
| newSection.scrollIntoView(); |
| } else { |
| this.scroller.scrollTop = this.origScrollTop_; |
| } |
| |
| this.currentAnimation_ = section.animateCollapse( |
| /** @type {!HTMLElement} */ (this.scroller)); |
| |
| this.currentAnimation_.finished |
| .catch(() => { |
| // The collapse was canceled, so the page is showing a subpage |
| // still. |
| this.fire('subpage-expand'); |
| }) |
| .then(() => { |
| // Clean up after the animation succeeds or cancels. |
| section.setFrozen(false); |
| section.classList.remove('collapsing'); |
| this.fire('freeze-scroll', false); |
| this.currentAnimation_ = null; |
| resolve(); |
| }); |
| }); |
| }); |
| }, |
| |
| /** |
| * Hides or unhides the sections not being expanded. |
| * @param {string} sectionName The section to keep visible. |
| * @param {boolean} hidden Whether the sections should be hidden. |
| * @private |
| */ |
| toggleOtherSectionsHidden_: function(sectionName, hidden) { |
| const sections = |
| Polymer.dom(this.root).querySelectorAll('settings-section'); |
| for (let i = 0; i < sections.length; i++) |
| sections[i].hidden = hidden && (sections[i].section != sectionName); |
| }, |
| |
| /** |
| * Helper function to get a section from the local DOM. |
| * @param {string} section Section name of the element to get. |
| * @return {?SettingsSectionElement} |
| */ |
| getSection: function(section) { |
| if (!section) |
| return null; |
| return /** @type {?SettingsSectionElement} */ ( |
| this.$$('settings-section[section="' + section + '"]')); |
| }, |
| }; |
| |
| /** @polymerBehavior */ |
| const MainPageBehavior = [ |
| settings.RouteObserverBehavior, |
| MainPageBehaviorImpl, |
| ]; |