| 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 | 
| +]; | 
|  |