| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview | 6 * @fileoverview |
| 7 * 'settings-section' shows a paper material themed section with a header | 7 * 'settings-section' shows a paper material themed section with a header |
| 8 * which shows its page title. | 8 * which shows its page title. |
| 9 * | 9 * |
| 10 * The section can expand vertically to fill its container's padding edge. | 10 * The section can expand vertically to fill its container's padding edge. |
| 11 * | 11 * |
| 12 * Example: | 12 * Example: |
| 13 * | 13 * |
| 14 * <settings-section page-title="[[pageTitle]]" section="privacy"> | 14 * <settings-section page-title="[[pageTitle]]" section="privacy"> |
| 15 * <!-- Insert your section controls here --> | 15 * <!-- Insert your section controls here --> |
| 16 * </settings-section> | 16 * </settings-section> |
| 17 */ | 17 */ |
| 18 | 18 |
| 19 // Fast out, slow in. | |
| 20 var EASING_FUNCTION = 'cubic-bezier(0.4, 0, 0.2, 1)'; | |
| 21 var EXPAND_DURATION = 350; | |
| 22 | |
| 23 var SettingsSectionElement = Polymer({ | 19 var SettingsSectionElement = Polymer({ |
| 24 is: 'settings-section', | 20 is: 'settings-section', |
| 25 | 21 |
| 26 properties: { | 22 properties: { |
| 27 /** | 23 /** |
| 28 * The section name should match a name specified in route.js. The | 24 * The section name should match a name specified in route.js. The |
| 29 * MainPageBehavior will expand this section if this section name matches | 25 * MainPageBehavior will expand this section if this section name matches |
| 30 * currentRoute.section. | 26 * currentRoute.section. |
| 31 */ | 27 */ |
| 32 section: String, | 28 section: String, |
| 33 | 29 |
| 34 /** Title for the section header. */ | 30 /** Title for the section header. */ |
| 35 pageTitle: String, | 31 pageTitle: String, |
| 36 | 32 |
| 37 /** | 33 /** |
| 38 * A CSS attribute used for temporarily hiding a SETTINGS-SECTION for the | 34 * A CSS attribute used for temporarily hiding a SETTINGS-SECTION for the |
| 39 * purposes of searching. | 35 * purposes of searching. |
| 40 */ | 36 */ |
| 41 hiddenBySearch: { | 37 hiddenBySearch: { |
| 42 type: Boolean, | 38 type: Boolean, |
| 43 value: false, | 39 value: false, |
| 44 reflectToAttribute: true, | 40 reflectToAttribute: true, |
| 45 }, | 41 }, |
| 42 |
| 43 /** |
| 44 * Original height of the collapsed section, used as the target height when |
| 45 * collapsing after being expanded. |
| 46 * TODO(michaelpg): Get the height dynamically when collapsing using the |
| 47 * card's main page. |
| 48 * @private |
| 49 */ |
| 50 collapsedHeight_: { |
| 51 type: Number, |
| 52 value: NaN, |
| 53 }, |
| 46 }, | 54 }, |
| 47 | 55 |
| 48 /** | 56 /** |
| 49 * Freezes the section's height so its card can be removed from the flow | 57 * Freezes the section's height so its card can be removed from the flow |
| 50 * without affecting the layout of the surrounding sections. | 58 * without affecting the layout of the surrounding sections. |
| 51 * @param {boolean} frozen True to freeze, false to unfreeze. | 59 * @param {boolean} frozen True to freeze, false to unfreeze. |
| 52 * @private | 60 * @private |
| 53 */ | 61 */ |
| 54 setFrozen: function(frozen) { | 62 setFrozen: function(frozen) { |
| 55 var card = this.$.card; | 63 var card = this.$.card; |
| 56 if (frozen) { | 64 if (frozen) { |
| 57 this.style.height = this.clientHeight + 'px'; | 65 this.style.height = this.clientHeight + 'px'; |
| 58 | 66 |
| 59 var cardHeight = card.offsetHeight; | 67 var cardHeight = card.offsetHeight; |
| 60 var cardWidth = card.offsetWidth; | 68 var cardWidth = card.offsetWidth; |
| 61 // If the section is not displayed yet (e.g., navigated directly to a | 69 // If the section is not displayed yet (e.g., navigated directly to a |
| 62 // sub-page), cardHeight and cardWidth are 0, so do not set the height or | 70 // sub-page), cardHeight and cardWidth are 0, so do not set the height or |
| 63 // width explicitly. | 71 // width explicitly. |
| 64 // TODO(michaelpg): Improve this logic when refactoring | |
| 65 // settings-animated-pages. | |
| 66 if (cardHeight && cardWidth) { | 72 if (cardHeight && cardWidth) { |
| 67 // TODO(michaelpg): Temporary hack to store the height the section | |
| 68 // should collapse to when it closes. | |
| 69 card.origHeight_ = cardHeight; | |
| 70 | |
| 71 card.style.height = cardHeight + 'px'; | 73 card.style.height = cardHeight + 'px'; |
| 72 card.style.width = cardWidth + 'px'; | 74 card.style.width = cardWidth + 'px'; |
| 73 } else { | |
| 74 // Set an invalid value so we don't try to use it later. | |
| 75 card.origHeight_ = NaN; | |
| 76 } | 75 } |
| 77 | 76 |
| 78 // Place the section's card at its current position but removed from the | 77 // Place the section's card at its current position but removed from the |
| 79 // flow. | 78 // flow. |
| 80 card.style.top = card.getBoundingClientRect().top + 'px'; | 79 card.style.top = card.getBoundingClientRect().top + 'px'; |
| 81 this.classList.add('frozen'); | 80 this.classList.add('frozen'); |
| 82 } else { | 81 } else { |
| 83 // Restore the section to its normal height. | 82 // Restore the section to its normal height. |
| 84 if (!this.classList.contains('frozen')) | 83 if (!this.classList.contains('frozen')) |
| 85 return; | 84 return; |
| 86 this.classList.remove('frozen'); | 85 this.classList.remove('frozen'); |
| 87 this.$.card.style.top = ''; | 86 this.$.card.style.top = ''; |
| 88 this.$.card.style.height = ''; | 87 this.$.card.style.height = ''; |
| 89 this.$.card.style.width = ''; | 88 this.$.card.style.width = ''; |
| 90 this.style.height = ''; | 89 this.style.height = ''; |
| 91 } | 90 } |
| 92 }, | 91 }, |
| 93 | 92 |
| 94 /** | 93 /** |
| 95 * @return {boolean} True if the section is currently rendered and not | 94 * @return {boolean} True if the section is currently rendered and not |
| 96 * already expanded or transitioning. | 95 * already expanded or transitioning. |
| 97 */ | 96 */ |
| 98 canAnimateExpand: function() { | 97 canAnimateExpand: function() { |
| 99 return !this.classList.contains('expanded'); | 98 return !this.classList.contains('expanding') && |
| 99 !this.classList.contains('expanded') && this.$.card.clientHeight > 0; |
| 100 }, | 100 }, |
| 101 | 101 |
| 102 /** | 102 /** |
| 103 * Animates the section expanding to fill the container. The section is fixed | 103 * Animates the section expanding to fill the container. The section is fixed |
| 104 * in the viewport during the animation. The section adds the "expanding" | 104 * in the viewport during the animation, making it safe to adjust the rest of |
| 105 * class while the animation plays. | 105 * the DOM after calling this. The section adds the "expanding" class while |
| 106 * the animation plays and "expanded" after it finishes. |
| 106 * | 107 * |
| 107 * @param {!HTMLElement} container The scrolling container to fill. | 108 * @param {!HTMLElement} container The scrolling container to fill. |
| 108 * @return {?settings.animation.Animation} Animation played, if any. | 109 * @return {!settings.animation.Animation} |
| 109 */ | 110 */ |
| 110 animateExpand: function(container) { | 111 animateExpand: function(container) { |
| 111 var card = this.$.card; | 112 // Set the section's height so its card can be removed from the flow |
| 112 | 113 // without affecting the surrounding sections during the animation. |
| 113 // The card should start at the top of the page. | 114 this.collapsedHeight_ = this.clientHeight; |
| 114 var targetTop = container.getBoundingClientRect().top; | 115 this.style.height = this.collapsedHeight_ + 'px'; |
| 115 | 116 |
| 116 this.classList.add('expanding'); | 117 this.classList.add('expanding'); |
| 117 | 118 |
| 118 // Nothing to animate. | 119 // Start the card in place, at its distance from the container's padding. |
| 119 if (!card.style.top || !card.style.height) | 120 var startTop = this.$.card.getBoundingClientRect().top + 'px'; |
| 120 return null; | 121 var startHeight = this.$.card.clientHeight + 'px'; |
| 121 | 122 |
| 122 // Expand the card, using minHeight. (The card must span the container's | 123 // Target position is the container's top edge in the viewport. |
| 123 // client height, so it must be at least 100% in case the card is too short. | 124 var containerTop = container.getBoundingClientRect().top; |
| 124 // If the card is already taller than the container's client height, we | 125 var endTop = containerTop + 'px'; |
| 125 // don't want to shrink the card to 100% or the content will overflow, so | 126 // The card should stretch from the bottom of the toolbar to the bottom of |
| 126 // we can't use height, and animating height wouldn't look right anyway.) | 127 // the page. calc(100% - top) lets the card resize if the window resizes. |
| 127 var keyframes = [{ | 128 var endHeight = 'calc(100% - ' + containerTop + 'px)'; |
| 128 top: card.style.top, | |
| 129 minHeight: card.style.height, | |
| 130 easing: EASING_FUNCTION, | |
| 131 }, { | |
| 132 top: targetTop + 'px', | |
| 133 minHeight: 'calc(100% - ' + targetTop + 'px)', | |
| 134 }]; | |
| 135 var options = /** @type {!KeyframeEffectOptions} */({ | |
| 136 duration: EXPAND_DURATION | |
| 137 }); | |
| 138 // TODO(michaelpg): Change elevation of sections. | |
| 139 return new settings.animation.Animation(card, keyframes, options); | |
| 140 }, | |
| 141 | 129 |
| 142 /** | 130 var animation = |
| 143 * Cleans up after animateExpand(). | 131 this.animateCard_('fixed', startTop, endTop, startHeight, endHeight); |
| 144 * @param {boolean} finished Whether the animation finished successfully. | 132 animation.finished.then(function() { |
| 145 */ | |
| 146 cleanUpAnimateExpand: function(finished) { | |
| 147 if (finished) { | |
| 148 this.classList.add('expanded'); | 133 this.classList.add('expanded'); |
| 149 this.$.card.style.top = ''; | 134 }.bind(this), function() {}).then(function() { |
| 150 this.$.header.hidden = true; | 135 // Unset these changes whether the animation finished or canceled. |
| 136 this.classList.remove('expanding'); |
| 151 this.style.height = ''; | 137 this.style.height = ''; |
| 152 } | 138 }.bind(this)); |
| 153 | 139 return animation; |
| 154 this.classList.remove('expanding'); | |
| 155 this.$.card.style.height = ''; | |
| 156 this.$.card.style.width = ''; | |
| 157 }, | 140 }, |
| 158 | 141 |
| 159 /** @return {boolean} True if the section is currently expanded. */ | 142 /** @return {boolean} True if the section is currently expanded. */ |
| 160 canAnimateCollapse: function() { | 143 canAnimateCollapse: function() { |
| 161 return this.classList.contains('expanded'); | 144 return this.classList.contains('expanded') && this.clientHeight > 0 && |
| 145 !Number.isNaN(this.collapsedHeight_); |
| 146 }, |
| 147 |
| 148 /** |
| 149 * Prepares for the animation before the other sections become visible. |
| 150 * Call before animateCollapse(). |
| 151 * @param {!HTMLElement} container |
| 152 */ |
| 153 setUpAnimateCollapse: function(container) { |
| 154 // Prepare the dimensions and set position: fixed. |
| 155 this.$.card.style.width = this.$.card.clientWidth + 'px'; |
| 156 this.$.card.style.height = this.$.card.clientHeight + 'px'; |
| 157 this.$.card.style.top = container.getBoundingClientRect().top + 'px'; |
| 158 this.$.card.style.position = 'fixed'; |
| 159 |
| 160 // The section can now collapse back into its original height the page so |
| 161 // the other sections appear in the right places. |
| 162 this.classList.remove('expanded'); |
| 163 this.classList.add('collapsing'); |
| 164 this.style.height = this.collapsedHeight_ + 'px'; |
| 162 }, | 165 }, |
| 163 | 166 |
| 164 /** | 167 /** |
| 165 * Collapses an expanded section's card back into position in the main page. | 168 * Collapses an expanded section's card back into position in the main page. |
| 166 * Adds the "expanding" class during the animation. | 169 * Call after calling animateCollapse(), unhiding other content and scrolling. |
| 167 * @param {!HTMLElement} container The scrolling container the card fills. | 170 * @param {!HTMLElement} container The scrolling container the card fills. |
| 168 * @param {number} prevScrollTop scrollTop of the container before this | 171 * @return {!settings.animation.Animation} |
| 169 * section expanded. | |
| 170 * @return {?settings.animation.Animation} Animation played, if any. | |
| 171 */ | 172 */ |
| 172 animateCollapse: function(container, prevScrollTop) { | 173 animateCollapse: function(container) { |
| 173 this.$.header.hidden = false; | 174 // Make the card position: absolute, so scrolling is less of a crapshoot. |
| 175 // First find the current distance between this section and the card using |
| 176 // fixed coordinates; the absolute distance will be the same. |
| 177 var fixedCardTop = this.$.card.getBoundingClientRect().top; |
| 178 var fixedSectionTop = this.getBoundingClientRect().top; |
| 179 var distance = fixedCardTop - fixedSectionTop; |
| 174 | 180 |
| 175 var startingTop = container.getBoundingClientRect().top; | 181 // The target position is right below our header. |
| 182 var headerStyle = getComputedStyle(this.$.header); |
| 183 var cardTargetTop = this.$.header.offsetHeight + |
| 184 parseFloat(headerStyle.marginBottom) + |
| 185 parseFloat(headerStyle.marginTop); |
| 176 | 186 |
| 177 var card = this.$.card; | 187 // Start the card at its current height and distance from our top. |
| 178 var cardHeightStart = card.clientHeight; | 188 var startTop = distance + 'px'; |
| 179 var cardWidthStart = card.clientWidth; | 189 var startHeight = this.$.card.style.height; |
| 180 | 190 |
| 181 this.classList.add('collapsing'); | 191 // End at the bottom of our header. |
| 182 this.classList.remove('expanding', 'expanded'); | 192 var endTop = cardTargetTop + 'px'; |
| 193 var endHeight = (this.collapsedHeight_ - cardTargetTop) + 'px'; |
| 183 | 194 |
| 184 // If we navigated here directly, we don't know the original height of the | 195 // The card no longer needs position: fixed. |
| 185 // section, so we skip the animation. | 196 this.$.card.style.position = ''; |
| 186 // TODO(michaelpg): remove this condition once sliding is implemented. | |
| 187 if (Number.isNaN(card.origHeight_)) | |
| 188 return null; | |
| 189 | 197 |
| 190 // Restore the section to its proper height to make room for the card. | 198 // Collapse this section, animate the card into place, and remove its |
| 191 this.style.height = (this.clientHeight + card.origHeight_) + 'px'; | 199 // other properties. |
| 200 var animation = |
| 201 this.animateCard_('absolute', startTop, endTop, startHeight, endHeight); |
| 202 this.$.card.style.width = ''; |
| 203 this.$.card.style.height = ''; |
| 204 this.$.card.style.top = ''; |
| 192 | 205 |
| 193 // TODO(michaelpg): this should be in MainPageBehavior(), but we need to | 206 animation.finished.then(function() { |
| 194 // wait until the full page height is available (setting the section | 207 this.classList.remove('expanded'); |
| 195 // height). | 208 }.bind(this), function() {}).then(function() { |
| 196 container.scrollTop = prevScrollTop; | 209 // The card now determines the section's height automatically. |
| 197 | 210 this.style.height = ''; |
| 198 // The card is unpositioned, so use its position as the ending state, | 211 this.classList.remove('collapsing'); |
| 199 // but account for scroll. | 212 }.bind(this)); |
| 200 var targetTop = card.getBoundingClientRect().top - container.scrollTop; | 213 return animation; |
| 201 | |
| 202 // Account for the section header. | |
| 203 var headerStyle = getComputedStyle(this.$.header); | |
| 204 targetTop += this.$.header.offsetHeight + | |
| 205 parseInt(headerStyle.marginBottom, 10) + | |
| 206 parseInt(headerStyle.marginTop, 10); | |
| 207 | |
| 208 var keyframes = [{ | |
| 209 top: startingTop + 'px', | |
| 210 minHeight: cardHeightStart + 'px', | |
| 211 easing: EASING_FUNCTION, | |
| 212 }, { | |
| 213 top: targetTop + 'px', | |
| 214 minHeight: card.origHeight_ + 'px', | |
| 215 }]; | |
| 216 var options = /** @type {!KeyframeEffectOptions} */({ | |
| 217 duration: EXPAND_DURATION | |
| 218 }); | |
| 219 | |
| 220 card.style.width = cardWidthStart + 'px'; | |
| 221 | |
| 222 return new settings.animation.Animation(card, keyframes, options); | |
| 223 }, | 214 }, |
| 224 | 215 |
| 225 /** | 216 /** |
| 226 * Cleans up after animateCollapse(). | 217 * Helper function to animate the card's position and height. |
| 227 * @param {boolean} finished Whether the animation finished successfully. | 218 * @param {string} position CSS position property. |
| 219 * @param {string} startTop Initial top value. |
| 220 * @param {string} endTop Target top value. |
| 221 * @param {string} startHeight Initial height value. |
| 222 * @param {string} endHeight Target height value. |
| 223 * @return {!settings.animation.Animation} |
| 224 * @private |
| 228 */ | 225 */ |
| 229 cleanUpAnimateCollapse: function(finished) { | 226 animateCard_: function(position, startTop, endTop, startHeight, endHeight) { |
| 230 if (finished) | 227 // Width does not change. |
| 231 this.$.card.style.width = ''; | 228 var width = this.$.card.clientWidth + 'px'; |
| 229 |
| 230 var startFrame = { |
| 231 position: position, |
| 232 width: width, |
| 233 top: startTop, |
| 234 height: startHeight, |
| 235 }; |
| 236 |
| 237 var endFrame = { |
| 238 position: position, |
| 239 width: width, |
| 240 top: endTop, |
| 241 height: endHeight, |
| 242 }; |
| 243 |
| 244 var options = /** @type {!KeyframeEffectOptions} */({ |
| 245 duration: settings.animation.Timing.DURATION, |
| 246 easing: settings.animation.Timing.EASING, |
| 247 }); |
| 248 |
| 249 return new settings.animation.Animation( |
| 250 this.$.card, [startFrame, endFrame], options); |
| 232 }, | 251 }, |
| 233 }); | 252 }); |
| OLD | NEW |