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 |