OLD | NEW |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 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 // Fast out, slow in. | |
6 var EASING_FUNCTION = 'cubic-bezier(0.4, 0, 0.2, 1)'; | |
7 var EXPAND_DURATION = 350; | |
8 | |
9 /** | 5 /** |
10 * Calls |readyTest| repeatedly until it returns true, then calls | 6 * Calls |readyTest| repeatedly until it returns true, then calls |
11 * |readyCallback|. | 7 * |readyCallback|. |
12 * @param {function():boolean} readyTest | 8 * @param {function():boolean} readyTest |
13 * @param {!Function} readyCallback | 9 * @param {!Function} readyCallback |
14 */ | 10 */ |
15 function doWhenReady(readyTest, readyCallback) { | 11 function doWhenReady(readyTest, readyCallback) { |
16 // TODO(dschuyler): Determine whether this hack can be removed. | 12 // TODO(dschuyler): Determine whether this hack can be removed. |
17 // See also: https://github.com/Polymer/polymer/issues/3629 | 13 // See also: https://github.com/Polymer/polymer/issues/3629 |
18 var intervalId = setInterval(function() { | 14 var intervalId = setInterval(function() { |
19 if (readyTest()) { | 15 if (readyTest()) { |
20 clearInterval(intervalId); | 16 clearInterval(intervalId); |
21 readyCallback(); | 17 readyCallback(); |
22 } | 18 } |
23 }, 10); | 19 }, 10); |
24 } | 20 } |
25 | 21 |
26 /** | 22 /** |
27 * Provides animations to expand and collapse individual sections in a page. | 23 * Provides animations to expand and collapse individual sections in a page. |
28 * Expanded sections take up the full height of the container. At most one | 24 * Expanded sections take up the full height of the container. At most one |
29 * section should be expanded at any given time. | 25 * section should be expanded at any given time. |
30 * @polymerBehavior Polymer.MainPageBehavior | 26 * @polymerBehavior Polymer.MainPageBehavior |
31 */ | 27 */ |
32 var MainPageBehaviorImpl = { | 28 var MainPageBehaviorImpl = { |
33 /** @type {?Element} The scrolling container. */ | 29 /** @type {?HTMLElement} The scrolling container. */ |
34 scroller: null, | 30 scroller: null, |
35 | 31 |
36 /** @override */ | 32 /** @override */ |
37 attached: function() { | 33 attached: function() { |
38 if (this.domHost && this.domHost.parentNode.tagName == 'PAPER-HEADER-PANEL') | 34 if (this.domHost && this.domHost.parentNode.tagName == 'PAPER-HEADER-PANEL') |
39 this.scroller = this.domHost.parentNode.scroller; | 35 this.scroller = this.domHost.parentNode.scroller; |
40 else | 36 else |
41 this.scroller = document.body; // Used in unit tests. | 37 this.scroller = document.body; // Used in unit tests. |
42 }, | 38 }, |
43 | 39 |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
95 } | 91 } |
96 | 92 |
97 this.startExpandSection_(section); | 93 this.startExpandSection_(section); |
98 }, | 94 }, |
99 | 95 |
100 /** | 96 /** |
101 * Helper function to set up and start the expand animation. | 97 * Helper function to set up and start the expand animation. |
102 * @param {!SettingsSectionElement} section | 98 * @param {!SettingsSectionElement} section |
103 */ | 99 */ |
104 startExpandSection_: function(section) { | 100 startExpandSection_: function(section) { |
105 if (section.classList.contains('expanded')) | 101 if (!section.canAnimateExpand()) |
106 return; | 102 return; |
107 | 103 |
108 // Freeze the scroller and save its position. | 104 // Freeze the scroller and save its position. |
109 this.listScrollTop_ = this.scroller.scrollTop; | 105 this.listScrollTop_ = this.scroller.scrollTop; |
110 | 106 |
111 var scrollerWidth = this.scroller.clientWidth; | 107 var scrollerWidth = this.scroller.clientWidth; |
112 this.scroller.style.overflow = 'hidden'; | 108 this.scroller.style.overflow = 'hidden'; |
113 // Adjust width to compensate for scroller. | 109 // Adjust width to compensate for scroller. |
114 var scrollbarWidth = this.scroller.clientWidth - scrollerWidth; | 110 var scrollbarWidth = this.scroller.clientWidth - scrollerWidth; |
115 this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)'; | 111 this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)'; |
116 | 112 |
117 // Freezes the section's height so its card can be removed from the flow. | 113 // Freezes the section's height so its card can be removed from the flow. |
118 this.freezeSection_(section); | 114 section.setFrozen(true); |
119 | 115 |
120 // Expand the section's card to fill the parent. | 116 // Expand the section's card to fill the parent. |
121 var animationPromise = this.playExpandSection_(section); | 117 var animationPromise = this.playExpandSection_(section); |
122 | 118 |
123 animationPromise.then(function() { | 119 animationPromise.then(function() { |
124 this.scroller.scrollTop = 0; | 120 this.scroller.scrollTop = 0; |
125 this.toggleOtherSectionsHidden_(section.section, true); | 121 this.toggleOtherSectionsHidden_(section.section, true); |
126 }.bind(this), function() { | 122 }.bind(this), function() { |
127 // Animation was canceled; restore the section. | 123 // Animation was canceled; restore the section. |
128 this.unfreezeSection_(section); | 124 section.setFrozen(false); |
129 }.bind(this)).then(function() { | 125 }.bind(this)).then(function() { |
130 this.scroller.style.overflow = ''; | 126 this.scroller.style.overflow = ''; |
131 this.scroller.style.width = ''; | 127 this.scroller.style.width = ''; |
132 }.bind(this)); | 128 }.bind(this)); |
133 }, | 129 }, |
134 | 130 |
135 /** | 131 /** |
136 * Animates the card in |section|, collapsing it back into its section. | 132 * Animates the card in |section|, collapsing it back into its section. |
137 * @param {!SettingsSectionElement} section | 133 * @param {!SettingsSectionElement} section |
138 */ | 134 */ |
139 collapseSection: function(section) { | 135 collapseSection: function(section) { |
140 // If the section's card is still expanding, cancel the expand animation. | 136 // If the section's card is still expanding, cancel the expand animation. |
141 if (section.classList.contains('expanding')) { | 137 if (section.classList.contains('expanding')) { |
142 if (this.animations['section']) { | 138 if (this.animations['section']) { |
143 this.cancelAnimation('section'); | 139 this.cancelAnimation('section'); |
144 } else { | 140 } else { |
145 // The animation must have finished but its promise hasn't finished | 141 // The animation must have finished but its promise hasn't finished |
146 // resolving; try again asynchronously. | 142 // resolving; try again asynchronously. |
147 this.async(function() { | 143 this.async(function() { |
148 this.collapseSection(section); | 144 this.collapseSection(section); |
149 }); | 145 }); |
150 } | 146 } |
151 return; | 147 return; |
152 } | 148 } |
153 | 149 |
154 if (!section.classList.contains('expanded')) | 150 if (!section.canAnimateCollapse()) |
155 return; | 151 return; |
156 | 152 |
157 this.toggleOtherSectionsHidden_(section.section, false); | 153 this.toggleOtherSectionsHidden_(section.section, false); |
158 | 154 |
159 var scrollerWidth = this.scroller.clientWidth; | 155 var scrollerWidth = this.scroller.clientWidth; |
160 this.scroller.style.overflow = 'hidden'; | 156 this.scroller.style.overflow = 'hidden'; |
161 // Adjust width to compensate for scroller. | 157 // Adjust width to compensate for scroller. |
162 var scrollbarWidth = this.scroller.clientWidth - scrollerWidth; | 158 var scrollbarWidth = this.scroller.clientWidth - scrollerWidth; |
163 this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)'; | 159 this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)'; |
164 | 160 |
165 this.playCollapseSection_(section).then(function() { | 161 this.playCollapseSection_(section).then(function() { |
166 this.unfreezeSection_(section); | 162 section.setFrozen(false); |
167 this.scroller.style.overflow = ''; | 163 this.scroller.style.overflow = ''; |
168 this.scroller.style.width = ''; | 164 this.scroller.style.width = ''; |
169 section.classList.remove('collapsing'); | 165 section.classList.remove('collapsing'); |
170 }.bind(this)); | 166 }.bind(this)); |
171 }, | 167 }, |
172 | 168 |
173 /** | 169 /** |
174 * Freezes a section's height so its card can be removed from the flow without | |
175 * affecting the layout of the surrounding sections. | |
176 * @param {!SettingsSectionElement} section | |
177 * @private | |
178 */ | |
179 freezeSection_: function(section) { | |
180 var card = section.$.card; | |
181 section.style.height = section.clientHeight + 'px'; | |
182 | |
183 var cardHeight = card.offsetHeight; | |
184 var cardWidth = card.offsetWidth; | |
185 // If the section is not displayed yet (e.g., navigated directly to a | |
186 // sub-page), cardHeight and cardWidth are 0, so do not set the height or | |
187 // width explicitly. | |
188 // TODO(michaelpg): Improve this logic when refactoring | |
189 // settings-animated-pages. | |
190 if (cardHeight && cardWidth) { | |
191 // TODO(michaelpg): Temporary hack to store the height the section should | |
192 // collapse to when it closes. | |
193 card.origHeight_ = cardHeight; | |
194 | |
195 card.style.height = cardHeight + 'px'; | |
196 card.style.width = cardWidth + 'px'; | |
197 } else { | |
198 // Set an invalid value so we don't try to use it later. | |
199 card.origHeight_ = NaN; | |
200 } | |
201 | |
202 // Place the section's card at its current position but removed from the | |
203 // flow. | |
204 card.style.top = card.getBoundingClientRect().top + 'px'; | |
205 section.classList.add('frozen'); | |
206 }, | |
207 | |
208 /** | |
209 * After freezeSection_, restores the section to its normal height. | |
210 * @param {!SettingsSectionElement} section | |
211 * @private | |
212 */ | |
213 unfreezeSection_: function(section) { | |
214 if (!section.classList.contains('frozen')) | |
215 return; | |
216 var card = section.$.card; | |
217 section.classList.remove('frozen'); | |
218 card.style.top = ''; | |
219 card.style.height = ''; | |
220 card.style.width = ''; | |
221 section.style.height = ''; | |
222 }, | |
223 | |
224 /** | |
225 * Expands the card in |section| to fill the page. | 170 * Expands the card in |section| to fill the page. |
226 * @param {!SettingsSectionElement} section | 171 * @param {!SettingsSectionElement} section |
227 * @return {!Promise} | 172 * @return {!Promise} |
228 * @private | 173 * @private |
229 */ | 174 */ |
230 playExpandSection_: function(section) { | 175 playExpandSection_: function(section) { |
231 var card = section.$.card; | 176 // We must be attached. |
| 177 assert(this.scroller); |
232 | 178 |
233 // The card should start at the top of the page. | 179 var promise; |
234 var targetTop = this.scroller.getBoundingClientRect().top; | 180 var animationConfig = section.animateExpand(this.scroller); |
| 181 if (animationConfig) { |
| 182 promise = this.animateElement('section', animationConfig.card, |
| 183 animationConfig.keyframes, animationConfig.options); |
| 184 } else { |
| 185 promise = Promise.resolve(); |
| 186 } |
235 | 187 |
236 section.classList.add('expanding'); | 188 var finished; |
237 | |
238 // Expand the card, using minHeight. (The card must span the container's | |
239 // client height, so it must be at least 100% in case the card is too short. | |
240 // If the card is already taller than the container's client height, we | |
241 // don't want to shrink the card to 100% or the content will overflow, so | |
242 // we can't use height, and animating height wouldn't look right anyway.) | |
243 var keyframes = [{ | |
244 top: card.style.top, | |
245 minHeight: card.style.height, | |
246 easing: EASING_FUNCTION, | |
247 }, { | |
248 top: targetTop + 'px', | |
249 minHeight: 'calc(100% - ' + targetTop + 'px)', | |
250 }]; | |
251 var options = /** @type {!KeyframeEffectOptions} */({ | |
252 duration: EXPAND_DURATION | |
253 }); | |
254 // TODO(michaelpg): Change elevation of sections. | |
255 var promise; | |
256 if (keyframes[0].top && keyframes[0].minHeight) | |
257 promise = this.animateElement('section', card, keyframes, options); | |
258 else | |
259 promise = Promise.resolve(); | |
260 | |
261 promise.then(function() { | 189 promise.then(function() { |
262 section.classList.add('expanded'); | 190 finished = true; |
263 card.style.top = ''; | |
264 this.style.margin = 'auto'; | 191 this.style.margin = 'auto'; |
265 section.$.header.hidden = true; | |
266 section.style.height = ''; | |
267 }.bind(this), function() { | 192 }.bind(this), function() { |
268 // The animation was canceled; catch the error and continue. | 193 // The animation was canceled; catch the error and continue. |
| 194 finished = false; |
269 }).then(function() { | 195 }).then(function() { |
270 // Whether finished or canceled, clean up the animation. | 196 section.cleanUpAnimateExpand(finished); |
271 section.classList.remove('expanding'); | |
272 card.style.height = ''; | |
273 card.style.width = ''; | |
274 }); | 197 }); |
275 | 198 |
276 return promise; | 199 return promise; |
277 }, | 200 }, |
278 | 201 |
279 /** | 202 /** |
280 * Collapses the card in |section| back to its normal position. | 203 * Collapses the card in |section| back to its normal position. |
281 * @param {!SettingsSectionElement} section | 204 * @param {!SettingsSectionElement} section |
282 * @return {!Promise} | 205 * @return {!Promise} |
283 * @private | 206 * @private |
284 */ | 207 */ |
285 playCollapseSection_: function(section) { | 208 playCollapseSection_: function(section) { |
286 var card = section.$.card; | 209 // We must be attached. |
| 210 assert(this.scroller); |
287 | 211 |
288 this.style.margin = ''; | 212 this.style.margin = ''; |
289 section.$.header.hidden = false; | |
290 | 213 |
291 var startingTop = this.scroller.getBoundingClientRect().top; | 214 var promise; |
| 215 var animationConfig = |
| 216 section.animateCollapse(this.scroller, this.listScrollTop_); |
| 217 if (animationConfig) { |
| 218 promise = this.animateElement('section', animationConfig.card, |
| 219 animationConfig.keyframes, animationConfig.options); |
| 220 } else { |
| 221 promise = Promise.resolve(); |
| 222 } |
292 | 223 |
293 var cardHeightStart = card.clientHeight; | |
294 var cardWidthStart = card.clientWidth; | |
295 | |
296 section.classList.add('collapsing'); | |
297 section.classList.remove('expanding', 'expanded'); | |
298 | |
299 // If we navigated here directly, we don't know the original height of the | |
300 // section, so we skip the animation. | |
301 // TODO(michaelpg): remove this condition once sliding is implemented. | |
302 if (isNaN(card.origHeight_)) | |
303 return Promise.resolve(); | |
304 | |
305 // Restore the section to its proper height to make room for the card. | |
306 section.style.height = section.clientHeight + card.origHeight_ + 'px'; | |
307 | |
308 // TODO(michaelpg): this should be in collapseSection(), but we need to wait | |
309 // until the full page height is available (setting the section height). | |
310 this.scroller.scrollTop = this.listScrollTop_; | |
311 | |
312 // The card is unpositioned, so use its position as the ending state, | |
313 // but account for scroll. | |
314 var targetTop = card.getBoundingClientRect().top - this.scroller.scrollTop; | |
315 | |
316 // Account for the section header. | |
317 var headerStyle = getComputedStyle(section.$.header); | |
318 targetTop += section.$.header.offsetHeight + | |
319 parseInt(headerStyle.marginBottom, 10) + | |
320 parseInt(headerStyle.marginTop, 10); | |
321 | |
322 var keyframes = [{ | |
323 top: startingTop + 'px', | |
324 minHeight: cardHeightStart + 'px', | |
325 easing: EASING_FUNCTION, | |
326 }, { | |
327 top: targetTop + 'px', | |
328 minHeight: card.origHeight_ + 'px', | |
329 }]; | |
330 var options = /** @type {!KeyframeEffectOptions} */({ | |
331 duration: EXPAND_DURATION | |
332 }); | |
333 | |
334 card.style.width = cardWidthStart + 'px'; | |
335 var promise = this.animateElement('section', card, keyframes, options); | |
336 promise.then(function() { | 224 promise.then(function() { |
337 card.style.width = ''; | 225 section.cleanUpAnimateCollapse(true); |
| 226 }, function() { |
| 227 section.cleanUpAnimateCollapse(false); |
338 }); | 228 }); |
339 return promise; | 229 return promise; |
340 }, | 230 }, |
341 }; | 231 }; |
342 | 232 |
343 | 233 |
344 /** @polymerBehavior */ | 234 /** @polymerBehavior */ |
345 var MainPageBehavior = [ | 235 var MainPageBehavior = [ |
346 TransitionBehavior, | 236 TransitionBehavior, |
347 MainPageBehaviorImpl | 237 MainPageBehaviorImpl |
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
421 this.$$('[section=' + section + ']')); | 311 this.$$('[section=' + section + ']')); |
422 }, | 312 }, |
423 }; | 313 }; |
424 | 314 |
425 | 315 |
426 /** @polymerBehavior */ | 316 /** @polymerBehavior */ |
427 var RoutableBehavior = [ | 317 var RoutableBehavior = [ |
428 MainPageBehavior, | 318 MainPageBehavior, |
429 RoutableBehaviorImpl | 319 RoutableBehaviorImpl |
430 ]; | 320 ]; |
OLD | NEW |