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} sectionName The section to keep visible. |
| 28 * @param {boolean} hidden Whether the sections should be hidden. |
| 29 * @private |
| 30 */ |
| 31 toggleOtherSectionsHidden_: function(sectionName, hidden) { |
| 32 var sections = Polymer.dom(this.root).querySelectorAll( |
| 33 this.sectionSelector + ':not([section=' + sectionName + '])'); |
| 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 = /** @type {!KeyframeEffectOptions} */({ |
| 233 duration: EXPAND_DURATION |
| 234 }); |
| 235 // TODO(michaelpg): Change elevation of sections. |
| 236 var promise; |
| 237 if (keyframes[0].top && keyframes[0].minHeight) |
| 238 promise = this.animateElement('section', card, keyframes, options); |
| 239 else |
| 240 promise = Promise.resolve(); |
| 241 |
| 242 promise.then(function() { |
| 243 section.classList.add('expanded'); |
| 244 card.style.top = ''; |
| 245 this.style.margin = 'auto'; |
| 246 section.$.header.hidden = true; |
| 247 section.style.height = ''; |
| 248 }.bind(this), function() { |
| 249 // The animation was canceled; catch the error and continue. |
| 250 }).then(function() { |
| 251 // Whether finished or canceled, clean up the animation. |
| 252 section.classList.remove('expanding'); |
| 253 card.style.height = ''; |
| 254 }); |
| 255 |
| 256 return promise; |
| 257 }, |
| 258 |
| 259 /** |
| 260 * Collapses the card in |section| back to its normal position. |
| 261 * @param {!SettingsSectionElement} section |
| 262 * @return {!Promise} |
| 263 * @private |
| 264 */ |
| 265 playCollapseSection_: function(section) { |
| 266 var card = section.$.card; |
| 267 var cardStyle = getComputedStyle(card); |
| 268 |
| 269 this.style.margin = ''; |
| 270 section.$.header.hidden = false; |
| 271 |
| 272 var startingTop = this.parentElement.getBoundingClientRect().top; |
| 273 |
| 274 var cardHeightStart = card.clientHeight; |
| 275 |
| 276 section.classList.add('collapsing'); |
| 277 section.classList.remove('expanding', 'expanded'); |
| 278 |
| 279 // If we navigated here directly, we don't know the original height of the |
| 280 // section, so we skip the animation. |
| 281 // TODO(michaelpg): remove this condition once sliding is implemented. |
| 282 if (isNaN(card.origHeight_)) |
| 283 return Promise.resolve(); |
| 284 |
| 285 // Restore the section to its proper height to make room for the card. |
| 286 section.style.height = section.clientHeight + card.origHeight_ + 'px'; |
| 287 |
| 288 // TODO(michaelpg): this should be in collapseSection(), but we need to wait |
| 289 // until the full page height is available (setting the section height). |
| 290 this.scroller.scrollTop = this.listScrollTop_; |
| 291 |
| 292 // The card is unpositioned, so use its position as the ending state, |
| 293 // but account for scroll. |
| 294 var targetTop = card.getBoundingClientRect().top - this.scroller.scrollTop; |
| 295 |
| 296 var keyframes = [{ |
| 297 top: startingTop + 'px', |
| 298 minHeight: cardHeightStart + 'px', |
| 299 easing: EASING_FUNCTION, |
| 300 }, { |
| 301 top: targetTop + 'px', |
| 302 minHeight: card.origHeight_ + 'px', |
| 303 }]; |
| 304 var options = /** @type {!KeyframeEffectOptions} */({ |
| 305 duration: EXPAND_DURATION |
| 306 }); |
| 307 var promise = this.animateElement('section', card, keyframes, options); |
| 308 return promise; |
| 309 }, |
| 310 }; |
| 311 |
| 312 /** @polymerBehavior */ |
| 313 var MainPageBehavior = [ |
| 314 TransitionBehavior, |
| 315 MainPageBehaviorImpl |
| 316 ]; |
| 317 |
| 318 /** |
| 319 * TODO(michaelpg): integrate slide animations. |
| 320 * @polymerBehavior RoutableBehavior |
| 321 */ |
| 322 var RoutableBehaviorImpl = { |
| 323 properties: { |
| 324 /** Contains the current route. */ |
| 325 currentRoute: { |
| 326 type: Object, |
| 327 notify: true, |
| 328 observer: 'currentRouteChanged_', |
| 329 }, |
| 330 }, |
| 331 |
| 332 /** @private */ |
| 333 currentRouteChanged_: function(newRoute, oldRoute) { |
| 334 // route.section is only non-empty when the user is within a subpage. |
| 335 // When the user is not in a subpage, but on the Basic page, route.section |
| 336 // is an empty string. |
| 337 var newRouteIsSubpage = newRoute && newRoute.section; |
| 338 var oldRouteIsSubpage = oldRoute && oldRoute.section; |
| 339 |
| 340 if (!oldRoute && newRouteIsSubpage) { |
| 341 // Allow the page to load before expanding the section. TODO(michaelpg): |
| 342 // Time this better when refactoring settings-animated-pages. |
| 343 setTimeout(function() { |
| 344 var section = this.getSection_(newRoute.section); |
| 345 if (section) |
| 346 this.expandSection(section); |
| 347 }.bind(this)); |
| 348 return; |
| 349 } |
| 350 |
| 351 if (!newRouteIsSubpage && oldRouteIsSubpage) { |
| 352 var section = this.getSection_(oldRoute.section); |
| 353 if (section) |
| 354 this.collapseSection(section); |
| 355 } else if (newRouteIsSubpage && |
| 356 (!oldRouteIsSubpage || newRoute.section != oldRoute.section)) { |
| 357 var section = this.getSection_(newRoute.section); |
| 358 if (section) |
| 359 this.expandSection(section); |
| 360 } |
| 361 }, |
| 362 |
| 363 /** |
| 364 * Helper function to get a section from the local DOM. |
| 365 * @param {string} section Section name of the element to get. |
| 366 * @return {?SettingsSectionElement} |
| 367 * @private |
| 368 */ |
| 369 getSection_: function(section) { |
| 370 return /** @type {?SettingsSectionElement} */( |
| 371 this.$$('[section=' + section + ']')); |
| 372 }, |
| 373 }; |
| 374 |
| 375 /** @polymerBehavior */ |
| 376 var RoutableBehavior = [ |
| 377 MainPageBehavior, |
| 378 RoutableBehaviorImpl |
| 379 ]; |
OLD | NEW |