Index: chrome/browser/resources/settings/settings_page/main_page_behavior.js |
diff --git a/chrome/browser/resources/settings/settings_page/main_page_behavior.js b/chrome/browser/resources/settings/settings_page/main_page_behavior.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..99dbc67c6181c689a6c3d63067f450ed323534c5 |
--- /dev/null |
+++ b/chrome/browser/resources/settings/settings_page/main_page_behavior.js |
@@ -0,0 +1,379 @@ |
+// 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. |
+ |
+// Fast out, slow in. |
+var EASING_FUNCTION = 'cubic-bezier(0.4, 0, 0.2, 1)'; |
+var EXPAND_DURATION = 350; |
+ |
+/** |
+ * Provides animations to expand and collapse individual sections in a page. |
+ * Expanded sections take up the full height of the container. At most one |
+ * section should be expanded at any given time. |
+ * @polymerBehavior Polymer.MainPageBehavior |
+ */ |
+var MainPageBehaviorImpl = { |
+ /** |
+ * @type {string} Selector to get the sections. Derived elements |
+ * must override. |
+ */ |
+ sectionSelector: '', |
+ |
+ /** @type {?Element} The scrolling container. Elements must set this. */ |
+ scroller: null, |
+ |
+ /** |
+ * 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) { |
+ var sections = Polymer.dom(this.root).querySelectorAll( |
+ this.sectionSelector + ':not([section=' + sectionName + '])'); |
+ for (var section of sections) |
+ section.hidden = hidden; |
+ }, |
+ |
+ /** |
+ * Animates the card in |section|, expanding it to fill the page. |
+ * @param {!SettingsSectionElement} section |
+ */ |
+ expandSection: function(section) { |
+ // If another section's card is expanding, cancel that animation first. |
+ var expanding = this.$$('.expanding'); |
+ if (expanding) { |
+ if (expanding == section) |
+ return; |
+ |
+ if (this.animations['section']) { |
+ // Cancel the animation, then call startExpandSection_. |
+ this.cancelAnimation('section', function() { |
+ this.startExpandSection_(section); |
+ }.bind(this)); |
+ } else { |
+ // The animation must have finished but its promise hasn't resolved yet. |
+ // When it resolves, collapse that section's card before expanding |
+ // this one. |
+ setTimeout(function() { |
+ this.collapseSection( |
+ /** @type {!SettingsSectionElement} */(expanding)); |
+ this.finishAnimation('section', function() { |
+ this.startExpandSection_(section); |
+ }.bind(this)); |
+ }.bind(this)); |
+ } |
+ |
+ return; |
+ } |
+ |
+ if (this.$$('.collapsing') && this.animations['section']) { |
+ // Finish the collapse animation before expanding. |
+ this.finishAnimation('section', function() { |
+ this.startExpandSection_(section); |
+ }.bind(this)); |
+ return; |
+ } |
+ |
+ this.startExpandSection_(section); |
+ }, |
+ |
+ /** |
+ * Helper function to set up and start the expand animation. |
+ * @param {!SettingsSectionElement} section |
+ */ |
+ startExpandSection_: function(section) { |
+ if (section.classList.contains('expanded')) |
+ return; |
+ |
+ // Freeze the scroller and save its position. |
+ this.listScrollTop_ = this.scroller.scrollTop; |
+ |
+ var scrollerWidth = this.scroller.clientWidth; |
+ this.scroller.style.overflow = 'hidden'; |
+ // Adjust width to compensate for scroller. |
+ var scrollbarWidth = this.scroller.clientWidth - scrollerWidth; |
+ this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)'; |
+ |
+ // Freezes the section's height so its card can be removed from the flow. |
+ this.freezeSection_(section); |
+ |
+ // Expand the section's card to fill the parent. |
+ var animationPromise = this.playExpandSection_(section); |
+ |
+ animationPromise.then(function() { |
+ this.scroller.scrollTop = 0; |
+ this.toggleOtherSectionsHidden_(section.section, true); |
+ }.bind(this), function() { |
+ // Animation was canceled; restore the section. |
+ this.unfreezeSection_(section); |
+ }.bind(this)).then(function() { |
+ this.scroller.style.overflow = ''; |
+ this.scroller.style.width = ''; |
+ }.bind(this)); |
+ }, |
+ |
+ /** |
+ * Animates the card in |section|, collapsing it back into its section. |
+ * @param {!SettingsSectionElement} section |
+ */ |
+ collapseSection: function(section) { |
+ // If the section's card is still expanding, cancel the expand animation. |
+ if (section.classList.contains('expanding')) { |
+ if (this.animations['section']) { |
+ this.cancelAnimation('section'); |
+ } else { |
+ // The animation must have finished but its promise hasn't finished |
+ // resolving; try again asynchronously. |
+ this.async(function() { |
+ this.collapseSection(section); |
+ }); |
+ } |
+ return; |
+ } |
+ |
+ if (!section.classList.contains('expanded')) |
+ return; |
+ |
+ this.toggleOtherSectionsHidden_(section.section, false); |
+ |
+ var scrollerWidth = this.scroller.clientWidth; |
+ this.scroller.style.overflow = 'hidden'; |
+ // Adjust width to compensate for scroller. |
+ var scrollbarWidth = this.scroller.clientWidth - scrollerWidth; |
+ this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)'; |
+ |
+ this.playCollapseSection_(section).then(function() { |
+ this.unfreezeSection_(section); |
+ this.scroller.style.overflow = ''; |
+ this.scroller.style.width = ''; |
+ section.classList.remove('collapsing'); |
+ }.bind(this)); |
+ }, |
+ |
+ /** |
+ * Freezes a section's height so its card can be removed from the flow without |
+ * affecting the layout of the surrounding sections. |
+ * @param {!SettingsSectionElement} section |
+ * @private |
+ */ |
+ freezeSection_: function(section) { |
+ var card = section.$.card; |
+ section.style.height = section.clientHeight + 'px'; |
+ |
+ var cardHeight = card.offsetHeight; |
+ var cardWidth = card.offsetWidth; |
+ // If the section is not displayed yet (e.g., navigated directly to a |
+ // sub-page), cardHeight and cardWidth are 0, so do not set the height or |
+ // width explicitly. |
+ // TODO(michaelpg): Improve this logic when refactoring |
+ // settings-animated-pages. |
+ if (cardHeight && cardWidth) { |
+ // TODO(michaelpg): Temporary hack to store the height the section should |
+ // collapse to when it closes. |
+ card.origHeight_ = cardHeight; |
+ |
+ card.style.height = cardHeight + 'px'; |
+ card.style.width = cardWidth + 'px'; |
+ } else { |
+ // Set an invalid value so we don't try to use it later. |
+ card.origHeight_ = NaN; |
+ } |
+ |
+ // Place the section's card at its current position but removed from the |
+ // flow. |
+ card.style.top = card.getBoundingClientRect().top + 'px'; |
+ section.classList.add('frozen'); |
+ }, |
+ |
+ /** |
+ * After freezeSection_, restores the section to its normal height. |
+ * @param {!SettingsSectionElement} section |
+ * @private |
+ */ |
+ unfreezeSection_: function(section) { |
+ if (!section.classList.contains('frozen')) |
+ return; |
+ var card = section.$.card; |
+ section.classList.remove('frozen'); |
+ card.style.top = ''; |
+ card.style.height = ''; |
+ card.style.width = ''; |
+ section.style.height = ''; |
+ }, |
+ |
+ /** |
+ * Expands the card in |section| to fill the page. |
+ * @param {!SettingsSectionElement} section |
+ * @return {!Promise} |
+ * @private |
+ */ |
+ playExpandSection_: function(section) { |
+ var card = section.$.card; |
+ |
+ // The card should start at the top of the page. |
+ var targetTop = this.parentElement.getBoundingClientRect().top; |
+ |
+ section.classList.add('expanding'); |
+ |
+ // Expand the card, using minHeight. (The card must span the container's |
+ // client height, so it must be at least 100% in case the card is too short. |
+ // If the card is already taller than the container's client height, we |
+ // don't want to shrink the card to 100% or the content will overflow, so |
+ // we can't use height, and animating height wouldn't look right anyway.) |
+ var keyframes = [{ |
+ top: card.style.top, |
+ minHeight: card.style.height, |
+ easing: EASING_FUNCTION, |
+ }, { |
+ top: targetTop + 'px', |
+ minHeight: 'calc(100% - ' + targetTop + 'px)', |
+ }]; |
+ var options = /** @type {!KeyframeEffectOptions} */({ |
+ duration: EXPAND_DURATION |
+ }); |
+ // TODO(michaelpg): Change elevation of sections. |
+ var promise; |
+ if (keyframes[0].top && keyframes[0].minHeight) |
+ promise = this.animateElement('section', card, keyframes, options); |
+ else |
+ promise = Promise.resolve(); |
+ |
+ promise.then(function() { |
+ section.classList.add('expanded'); |
+ card.style.top = ''; |
+ this.style.margin = 'auto'; |
+ section.$.header.hidden = true; |
+ section.style.height = ''; |
+ }.bind(this), function() { |
+ // The animation was canceled; catch the error and continue. |
+ }).then(function() { |
+ // Whether finished or canceled, clean up the animation. |
+ section.classList.remove('expanding'); |
+ card.style.height = ''; |
+ }); |
+ |
+ return promise; |
+ }, |
+ |
+ /** |
+ * Collapses the card in |section| back to its normal position. |
+ * @param {!SettingsSectionElement} section |
+ * @return {!Promise} |
+ * @private |
+ */ |
+ playCollapseSection_: function(section) { |
+ var card = section.$.card; |
+ var cardStyle = getComputedStyle(card); |
+ |
+ this.style.margin = ''; |
+ section.$.header.hidden = false; |
+ |
+ var startingTop = this.parentElement.getBoundingClientRect().top; |
+ |
+ var cardHeightStart = card.clientHeight; |
+ |
+ section.classList.add('collapsing'); |
+ section.classList.remove('expanding', 'expanded'); |
+ |
+ // If we navigated here directly, we don't know the original height of the |
+ // section, so we skip the animation. |
+ // TODO(michaelpg): remove this condition once sliding is implemented. |
+ if (isNaN(card.origHeight_)) |
+ return Promise.resolve(); |
+ |
+ // Restore the section to its proper height to make room for the card. |
+ section.style.height = section.clientHeight + card.origHeight_ + 'px'; |
+ |
+ // TODO(michaelpg): this should be in collapseSection(), but we need to wait |
+ // until the full page height is available (setting the section height). |
+ this.scroller.scrollTop = this.listScrollTop_; |
+ |
+ // The card is unpositioned, so use its position as the ending state, |
+ // but account for scroll. |
+ var targetTop = card.getBoundingClientRect().top - this.scroller.scrollTop; |
+ |
+ var keyframes = [{ |
+ top: startingTop + 'px', |
+ minHeight: cardHeightStart + 'px', |
+ easing: EASING_FUNCTION, |
+ }, { |
+ top: targetTop + 'px', |
+ minHeight: card.origHeight_ + 'px', |
+ }]; |
+ var options = /** @type {!KeyframeEffectOptions} */({ |
+ duration: EXPAND_DURATION |
+ }); |
+ var promise = this.animateElement('section', card, keyframes, options); |
+ return promise; |
+ }, |
+}; |
+ |
+/** @polymerBehavior */ |
+var MainPageBehavior = [ |
+ TransitionBehavior, |
+ MainPageBehaviorImpl |
+]; |
+ |
+/** |
+ * TODO(michaelpg): integrate slide animations. |
+ * @polymerBehavior RoutableBehavior |
+ */ |
+var RoutableBehaviorImpl = { |
+ properties: { |
+ /** Contains the current route. */ |
+ currentRoute: { |
+ type: Object, |
+ notify: true, |
+ observer: 'currentRouteChanged_', |
+ }, |
+ }, |
+ |
+ /** @private */ |
+ currentRouteChanged_: function(newRoute, oldRoute) { |
+ // route.section is only non-empty when the user is within a subpage. |
+ // When the user is not in a subpage, but on the Basic page, route.section |
+ // is an empty string. |
+ var newRouteIsSubpage = newRoute && newRoute.section; |
+ var oldRouteIsSubpage = oldRoute && oldRoute.section; |
+ |
+ if (!oldRoute && newRouteIsSubpage) { |
+ // Allow the page to load before expanding the section. TODO(michaelpg): |
+ // Time this better when refactoring settings-animated-pages. |
+ setTimeout(function() { |
+ var section = this.getSection_(newRoute.section); |
+ if (section) |
+ this.expandSection(section); |
+ }.bind(this)); |
+ return; |
+ } |
+ |
+ if (!newRouteIsSubpage && oldRouteIsSubpage) { |
+ var section = this.getSection_(oldRoute.section); |
+ if (section) |
+ this.collapseSection(section); |
+ } else if (newRouteIsSubpage && |
+ (!oldRouteIsSubpage || newRoute.section != oldRoute.section)) { |
+ var section = this.getSection_(newRoute.section); |
+ if (section) |
+ this.expandSection(section); |
+ } |
+ }, |
+ |
+ /** |
+ * Helper function to get a section from the local DOM. |
+ * @param {string} section Section name of the element to get. |
+ * @return {?SettingsSectionElement} |
+ * @private |
+ */ |
+ getSection_: function(section) { |
+ return /** @type {?SettingsSectionElement} */( |
+ this.$$('[section=' + section + ']')); |
+ }, |
+}; |
+ |
+/** @polymerBehavior */ |
+var RoutableBehavior = [ |
+ MainPageBehavior, |
+ RoutableBehaviorImpl |
+]; |