OLD | NEW |
---|---|
(Empty) | |
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 | |
3 // found in the LICENSE file. | |
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 /** | |
10 * Provides animations to expand and collapse individual sections in a page. | |
11 * Expanded sections take up the full height of the container. At most one | |
12 * section should be expanded at any given time. | |
13 * @polymerBehavior Polymer.MainPageBehavior | |
14 */ | |
15 var MainPageBehaviorImpl = { | |
16 /** | |
17 * @type {string} Selector to get the sections. Derived elements | |
18 * must override. | |
19 */ | |
20 sectionSelector: '', | |
21 | |
22 /** @type {?Element} The scrolling container. Elements must set this. */ | |
23 scroller: null, | |
24 | |
25 /** | |
26 * Hides or unhides the sections not being expanded. | |
27 * @param {string} section The section to keep visible. | |
28 * @param {boolean} hidden Whether the sections should be hidden. | |
29 * @private | |
30 */ | |
31 toggleOtherSectionsHidden_: function(section, hidden) { | |
32 var sections = Polymer.dom(this.root).querySelectorAll( | |
33 this.sectionSelector + ':not([section=' + section + '])'); | |
34 for (var section of sections) | |
35 section.hidden = hidden; | |
36 }, | |
37 | |
38 /** | |
39 * Animates the card in |section|, expanding it to fill the page. | |
40 * @param {!SettingsSectionElement} section | |
41 */ | |
42 expandSection: function(section) { | |
43 // If another section's card is expanding, cancel that animation first. | |
44 var expanding = this.$$('.expanding'); | |
45 if (expanding) { | |
46 if (expanding == section) | |
47 return; | |
48 | |
49 if (this.animations['section']) { | |
50 // Cancel the animation, then call startExpandSection_. | |
51 this.cancelAnimation('section', function() { | |
52 this.startExpandSection_(section); | |
53 }.bind(this)); | |
54 } else { | |
55 // The animation must have finished but its promise hasn't resolved yet. | |
56 // When it resolves, collapse that section's card before expanding | |
57 // this one. | |
58 setTimeout(function() { | |
59 this.collapseSection( | |
60 /** @type {!SettingsSectionElement} */(expanding)); | |
61 this.finishAnimation('section', function() { | |
62 this.startExpandSection_(section); | |
63 }.bind(this)); | |
64 }.bind(this)); | |
65 } | |
66 | |
67 return; | |
68 } | |
69 | |
70 if (this.$$('.collapsing') && this.animations['section']) { | |
71 // Finish the collapse animation before expanding. | |
72 this.finishAnimation('section', function() { | |
73 this.startExpandSection_(section); | |
74 }.bind(this)); | |
75 return; | |
76 } | |
77 | |
78 this.startExpandSection_(section); | |
79 }, | |
80 | |
81 /** | |
82 * Helper function to set up and start the expand animation. | |
83 * @param {!SettingsSectionElement} section | |
84 */ | |
85 startExpandSection_: function(section) { | |
86 if (section.classList.contains('expanded')) | |
87 return; | |
88 | |
89 // Freeze the scroller and save its position. | |
90 this.listScrollTop_ = this.scroller.scrollTop; | |
91 | |
92 var scrollerWidth = this.scroller.clientWidth; | |
93 this.scroller.style.overflow = 'hidden'; | |
94 // Adjust width to compensate for scroller. | |
95 var scrollbarWidth = this.scroller.clientWidth - scrollerWidth; | |
96 this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)'; | |
97 | |
98 // Freezes the section's height so its card can be removed from the flow. | |
99 this.freezeSection_(section); | |
100 | |
101 // Expand the section's card to fill the parent. | |
102 var animationPromise = this.playExpandSection_(section); | |
103 | |
104 animationPromise.then(function() { | |
105 this.scroller.scrollTop = 0; | |
106 this.toggleOtherSectionsHidden_(section.section, true); | |
107 }.bind(this), function() { | |
108 // Animation was canceled; restore the section. | |
109 this.unfreezeSection_(section); | |
110 }.bind(this)).then(function() { | |
111 this.scroller.style.overflow = ''; | |
112 this.scroller.style.width = ''; | |
113 }.bind(this)); | |
114 }, | |
115 | |
116 /** | |
117 * Animates the card in |section|, collapsing it back into its section. | |
118 * @param {!SettingsSectionElement} section | |
119 */ | |
120 collapseSection: function(section) { | |
121 // If the section's card is still expanding, cancel the expand animation. | |
122 if (section.classList.contains('expanding')) { | |
123 if (this.animations['section']) { | |
124 this.cancelAnimation('section'); | |
125 } else { | |
126 // The animation must have finished but its promise hasn't finished | |
127 // resolving; try again asynchronously. | |
128 this.async(function() { | |
129 this.collapseSection(section); | |
130 }); | |
131 } | |
132 return; | |
133 } | |
134 | |
135 if (!section.classList.contains('expanded')) | |
136 return; | |
137 | |
138 this.toggleOtherSectionsHidden_(section.section, false); | |
139 | |
140 var scrollerWidth = this.scroller.clientWidth; | |
141 this.scroller.style.overflow = 'hidden'; | |
142 // Adjust width to compensate for scroller. | |
143 var scrollbarWidth = this.scroller.clientWidth - scrollerWidth; | |
144 this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)'; | |
145 | |
146 this.playCollapseSection_(section).then(function() { | |
147 this.unfreezeSection_(section); | |
148 this.scroller.style.overflow = ''; | |
149 this.scroller.style.width = ''; | |
150 section.classList.remove('collapsing'); | |
151 }.bind(this)); | |
152 }, | |
153 | |
154 /** | |
155 * Freezes a section's height so its card can be removed from the flow without | |
156 * affecting the layout of the surrounding sections. | |
157 * @param {!SettingsSectionElement} section | |
158 * @private | |
159 */ | |
160 freezeSection_: function(section) { | |
161 var card = section.$.card; | |
162 section.style.height = section.clientHeight + 'px'; | |
163 | |
164 var cardHeight = card.offsetHeight; | |
165 var cardWidth = card.offsetWidth; | |
166 // If the section is not displayed yet (e.g., navigated directly to a | |
167 // sub-page), cardHeight and cardWidth are 0, so do not set the height or | |
168 // width explicitly. | |
169 // TODO(michaelpg): Improve this logic when refactoring | |
170 // settings-animated-pages. | |
171 if (cardHeight && cardWidth) { | |
172 // TODO(michaelpg): Temporary hack to store the height the section should | |
173 // collapse to when it closes. | |
174 card.origHeight_ = cardHeight; | |
175 | |
176 card.style.height = cardHeight + 'px'; | |
177 card.style.width = cardWidth + 'px'; | |
178 } else { | |
179 // Set an invalid value so we don't try to use it later. | |
180 card.origHeight_ = NaN; | |
181 } | |
182 | |
183 // Place the section's card at its current position but removed from the | |
184 // flow. | |
185 card.style.top = card.getBoundingClientRect().top + 'px'; | |
186 section.classList.add('frozen'); | |
187 }, | |
188 | |
189 /** | |
190 * After freezeSection_, restores the section to its normal height. | |
191 * @param {!SettingsSectionElement} section | |
192 * @private | |
193 */ | |
194 unfreezeSection_: function(section) { | |
195 if (!section.classList.contains('frozen')) | |
196 return; | |
197 var card = section.$.card; | |
198 section.classList.remove('frozen'); | |
199 card.style.top = ''; | |
200 card.style.height = ''; | |
201 card.style.width = ''; | |
202 section.style.height = ''; | |
203 }, | |
204 | |
205 /** | |
206 * Expands the card in |section| to fill the page. | |
207 * @param {!SettingsSectionElement} section | |
208 * @return {!Promise} | |
209 * @private | |
210 */ | |
211 playExpandSection_: function(section) { | |
212 var card = section.$.card; | |
213 | |
214 // The card should start at the top of the page. | |
215 var targetTop = this.parentElement.getBoundingClientRect().top; | |
216 | |
217 section.classList.add('expanding'); | |
218 | |
219 // Expand the card, using minHeight. (The card must span the container's | |
220 // client height, so it must be at least 100% in case the card is too short. | |
221 // If the card is already taller than the container's client height, we | |
222 // don't want to shrink the card to 100% or the content will overflow, so | |
223 // we can't use height, and animating height wouldn't look right anyway.) | |
224 var keyframes = [{ | |
225 top: card.style.top, | |
226 minHeight: card.style.height, | |
227 easing: EASING_FUNCTION, | |
228 }, { | |
229 top: targetTop + 'px', | |
230 minHeight: 'calc(100% - ' + targetTop + 'px)', | |
231 }]; | |
232 var options = {duration: EXPAND_DURATION}; | |
233 // TODO(michaelpg): Change elevation of sections. | |
234 var promise; | |
235 if (keyframes[0].top && keyframes[0].minHeight) | |
236 promise = this.animateElement('section', card, keyframes, options); | |
237 else | |
238 promise = Promise.resolve(); | |
239 | |
240 promise.then(function() { | |
241 section.classList.add('expanded'); | |
242 card.style.top = ''; | |
243 this.style.margin = 'auto'; | |
244 section.$.header.hidden = true; | |
245 section.style.height = ''; | |
246 }.bind(this), function() { | |
247 // The animation was canceled; catch the error and continue. | |
248 }).then(function() { | |
249 // Whether finished or canceled, clean up the animation. | |
250 section.classList.remove('expanding'); | |
251 card.style.height = ''; | |
252 }); | |
253 | |
254 return promise; | |
255 }, | |
256 | |
257 /** | |
258 * Collapses the card in |section| back to its normal position. | |
259 * @param {!SettingsSectionElement} section | |
260 * @return {!Promise} | |
261 * @private | |
262 */ | |
263 playCollapseSection_: function(section) { | |
264 var card = section.$.card; | |
265 var cardStyle = getComputedStyle(card); | |
266 | |
267 this.style.margin = ''; | |
268 section.$.header.hidden = false; | |
269 | |
270 var startingTop = this.parentElement.getBoundingClientRect().top; | |
271 | |
272 var cardHeightStart = card.clientHeight; | |
273 | |
274 section.classList.add('collapsing'); | |
275 section.classList.remove('expanding', 'expanded'); | |
276 | |
277 // If we navigated here directly, we don't know the original height of the | |
278 // section, so we skip the animation. | |
279 // TODO(michaelpg): remove this condition once sliding is implemented. | |
280 if (Number.isNaN(card.origHeight_)) | |
Dan Beam
2016/03/04 19:24:24
fwiw: isNaN is also on window, so this could just
michaelpg
2016/03/14 18:57:44
Done.
| |
281 return Promise.resolve(); | |
282 | |
283 // Restore the section to its proper height to make room for the card. | |
284 section.style.height = section.clientHeight + card.origHeight_ + 'px'; | |
285 | |
286 // TODO(michaelpg): this should be in collapseSection(), but we need to wait | |
287 // until the full page height is available (setting the section height). | |
288 this.scroller.scrollTop = this.listScrollTop_; | |
289 | |
290 // The card is unpositioned, so use its position as the ending state, | |
291 // but account for scroll. | |
292 var targetTop = card.getBoundingClientRect().top - this.scroller.scrollTop; | |
293 | |
294 var keyframes = [{ | |
295 top: startingTop + 'px', | |
296 minHeight: cardHeightStart + 'px', | |
297 easing: EASING_FUNCTION, | |
298 }, { | |
299 top: targetTop + 'px', | |
300 minHeight: card.origHeight_ + 'px', | |
301 }]; | |
302 var options = {duration: EXPAND_DURATION}; | |
303 var promise = this.animateElement('section', card, keyframes, options); | |
304 return promise; | |
305 }, | |
306 }; | |
307 | |
308 /** @polymerBehavior */ | |
309 var MainPageBehavior = [ | |
310 TransitionBehavior, | |
311 MainPageBehaviorImpl | |
312 ]; | |
313 | |
314 /** | |
315 * TODO(michaelpg): integrate slide animations. | |
316 * @polymerBehavior RoutableBehavior | |
317 */ | |
318 var RoutableBehaviorImpl = { | |
319 properties: { | |
320 /** Contains the current route. */ | |
321 currentRoute: { | |
322 type: Object, | |
323 notify: true, | |
324 observer: 'currentRouteChanged_', | |
325 }, | |
326 }, | |
327 | |
328 /** @private */ | |
329 currentRouteChanged_: function(newRoute, oldRoute) { | |
330 // route.section is only non-empty when the user is within a subpage. | |
331 // When the user is not in a subpage, but on the Basic page, route.section | |
332 // is an empty string. | |
333 var newRouteIsSubpage = newRoute && newRoute.section; | |
334 var oldRouteIsSubpage = oldRoute && oldRoute.section; | |
335 | |
336 if (!oldRoute && newRouteIsSubpage) { | |
337 // Allow the page to load before expanding the section. TODO(michaelpg): | |
338 // Time this better when refactoring settings-animated-pages. | |
339 setTimeout(function() { | |
340 var section = this.getSection_(newRoute.section); | |
341 if (section) | |
342 this.expandSection(section); | |
343 }.bind(this)); | |
344 return; | |
345 } | |
346 | |
347 if (!newRouteIsSubpage && oldRouteIsSubpage) { | |
348 var section = this.getSection_(oldRoute.section); | |
349 if (section) | |
350 this.collapseSection(section); | |
351 } else if (newRouteIsSubpage && | |
352 (!oldRouteIsSubpage || newRoute.section != oldRoute.section)) { | |
353 var section = this.getSection_(newRoute.section); | |
354 if (section) | |
355 this.expandSection(section); | |
356 } | |
357 }, | |
358 | |
359 /** | |
360 * Helper function to get a section from the local DOM. | |
361 * @param {string} section Section name of the element to get. | |
362 * @return {?SettingsSectionElement} | |
363 * @private | |
364 */ | |
365 getSection_: function(section) { | |
366 return /** @type {?SettingsSectionElement} */( | |
367 this.$$('[section=' + section + ']')); | |
368 }, | |
369 }; | |
370 | |
371 /** @polymerBehavior */ | |
372 var RoutableBehavior = [ | |
373 MainPageBehavior, | |
374 RoutableBehaviorImpl | |
375 ]; | |
OLD | NEW |