Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(131)

Side by Side Diff: chrome/browser/resources/settings/settings_page/main_page_behavior.js

Issue 1736733003: MD Settings: Custom expand/collapse section animations. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: rebase Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 ];
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698