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

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

Powered by Google App Engine
This is Rietveld 408576698