| OLD | NEW |
| (Empty) |
| 1 Polymer({ | |
| 2 is: 'app-header', | |
| 3 | |
| 4 behaviors: [ | |
| 5 Polymer.AppScrollEffectsBehavior, | |
| 6 Polymer.IronResizableBehavior | |
| 7 ], | |
| 8 | |
| 9 properties: { | |
| 10 /** | |
| 11 * If true, the header will automatically collapse when scrolling down. | |
| 12 * That is, the `primary` element remains visible when the header is ful
ly condensed | |
| 13 * whereas the rest of the elements will collapse below `primary` elemen
t. | |
| 14 * | |
| 15 * By default, the `primary` element is the first toolbar in the light D
OM: | |
| 16 * | |
| 17 *```html | |
| 18 * <app-header condenses> | |
| 19 * <app-toolbar>This toolbar remains on top</app-toolbar> | |
| 20 * <app-toolbar></app-toolbar> | |
| 21 * <app-toolbar></app-toolbar> | |
| 22 * </app-header> | |
| 23 * ``` | |
| 24 * | |
| 25 * Additionally, you can specify which toolbar or element remains visibl
e in condensed mode | |
| 26 * by adding the `primary` attribute to that element. For example: if we
want the last | |
| 27 * toolbar to remain visible, we can add the `primary` attribute to it. | |
| 28 * | |
| 29 *```html | |
| 30 * <app-header condenses> | |
| 31 * <app-toolbar></app-toolbar> | |
| 32 * <app-toolbar></app-toolbar> | |
| 33 * <app-toolbar primary>This toolbar remains on top</app-toolbar> | |
| 34 * </app-header> | |
| 35 * ``` | |
| 36 * | |
| 37 * Note the `primary` element must be a child of `app-header`. | |
| 38 */ | |
| 39 condenses: { | |
| 40 type: Boolean, | |
| 41 value: false | |
| 42 }, | |
| 43 | |
| 44 /** | |
| 45 * Mantains the header fixed at the top so it never moves away. | |
| 46 */ | |
| 47 fixed: { | |
| 48 type: Boolean, | |
| 49 value: false | |
| 50 }, | |
| 51 | |
| 52 /** | |
| 53 * Slides back the header when scrolling back up. | |
| 54 */ | |
| 55 reveals: { | |
| 56 type: Boolean, | |
| 57 value: false | |
| 58 }, | |
| 59 | |
| 60 /** | |
| 61 * Displays a shadow below the header. | |
| 62 */ | |
| 63 shadow: { | |
| 64 type: Boolean, | |
| 65 reflectToAttribute: true, | |
| 66 value: false | |
| 67 } | |
| 68 }, | |
| 69 | |
| 70 observers: [ | |
| 71 'resetLayout(isAttached, condenses, fixed)' | |
| 72 ], | |
| 73 | |
| 74 listeners: { | |
| 75 'iron-resize': '_resizeHandler' | |
| 76 }, | |
| 77 | |
| 78 /** | |
| 79 * A cached offsetHeight of the current element. | |
| 80 * | |
| 81 * @type {number} | |
| 82 */ | |
| 83 _height: 0, | |
| 84 | |
| 85 /** | |
| 86 * The distance in pixels the header will be translated to when scrolling. | |
| 87 * | |
| 88 * @type {number} | |
| 89 */ | |
| 90 _dHeight: 0, | |
| 91 | |
| 92 /** | |
| 93 * The offsetTop of `_primaryEl` | |
| 94 * | |
| 95 * @type {number} | |
| 96 */ | |
| 97 _primaryElTop: 0, | |
| 98 | |
| 99 /** | |
| 100 * The element that remains visible when the header condenses. | |
| 101 * | |
| 102 * @type {HTMLElement} | |
| 103 */ | |
| 104 _primaryEl: null, | |
| 105 | |
| 106 /** | |
| 107 * The header's top value used for the `transformY` | |
| 108 * | |
| 109 * @type {number} | |
| 110 */ | |
| 111 _top: 0, | |
| 112 | |
| 113 /** | |
| 114 * The current scroll progress. | |
| 115 * | |
| 116 * @type {number} | |
| 117 */ | |
| 118 _progress: 0, | |
| 119 | |
| 120 _wasScrollingDown: false, | |
| 121 _initScrollTop: 0, | |
| 122 _initTimestamp: 0, | |
| 123 _lastTimestamp: 0, | |
| 124 _lastScrollTop: 0, | |
| 125 | |
| 126 /** | |
| 127 * The distance the header is allowed to move away. | |
| 128 * | |
| 129 * @type {number} | |
| 130 */ | |
| 131 get _maxHeaderTop() { | |
| 132 return this.fixed ? this._dHeight : this._height + 5; | |
| 133 }, | |
| 134 | |
| 135 /** | |
| 136 * Returns a reference to the element that remains visible when the header
condenses. | |
| 137 * | |
| 138 * @return {HTMLElement}? | |
| 139 */ | |
| 140 _getPrimaryEl: function() { | |
| 141 /** @type {HTMLElement} */ | |
| 142 var primaryEl; | |
| 143 var nodes = Polymer.dom(this.$.content).getDistributedNodes(); | |
| 144 | |
| 145 for (var i = 0; i < nodes.length; i++) { | |
| 146 if (nodes[i].nodeType === Node.ELEMENT_NODE) { | |
| 147 var node = /** @type {HTMLElement} */ (nodes[i]); | |
| 148 if (node.hasAttribute('primary')) { | |
| 149 primaryEl = node; | |
| 150 break; | |
| 151 } else if (!primaryEl) { | |
| 152 primaryEl = node; | |
| 153 } | |
| 154 } | |
| 155 } | |
| 156 return primaryEl; | |
| 157 }, | |
| 158 | |
| 159 /** | |
| 160 * Resets the layout. If you changed the size of app-header via CSS | |
| 161 * you can notify the changes by either firing the `iron-resize` event | |
| 162 * or calling `resetLayout` directly. | |
| 163 * | |
| 164 * @method resetLayout | |
| 165 */ | |
| 166 resetLayout: function() { | |
| 167 this.fire('app-header-reset-layout'); | |
| 168 | |
| 169 this.debounce('_resetLayout', function() { | |
| 170 // noop if the header isn't visible | |
| 171 if (this.offsetWidth === 0 && this.offsetHeight === 0) { | |
| 172 return; | |
| 173 } | |
| 174 | |
| 175 var scrollTop = this._clampedScrollTop; | |
| 176 var firstSetup = this._height === 0 || scrollTop === 0; | |
| 177 var currentDisabled = this.disabled; | |
| 178 | |
| 179 this._height = this.offsetHeight; | |
| 180 this._primaryEl = this._getPrimaryEl(); | |
| 181 this.disabled = true; | |
| 182 | |
| 183 // prepare for measurement | |
| 184 if (!firstSetup) { | |
| 185 this._updateScrollState(0, true); | |
| 186 } | |
| 187 | |
| 188 if (this._mayMove()) { | |
| 189 this._dHeight = this._primaryEl ? this._height - this._primaryEl.off
setHeight : 0; | |
| 190 } else { | |
| 191 this._dHeight = 0; | |
| 192 } | |
| 193 | |
| 194 this._primaryElTop = this._primaryEl ? this._primaryEl.offsetTop : 0; | |
| 195 this._setUpEffect(); | |
| 196 | |
| 197 if (firstSetup) { | |
| 198 this._updateScrollState(scrollTop, true); | |
| 199 } else { | |
| 200 this._updateScrollState(this._lastScrollTop, true); | |
| 201 this._layoutIfDirty(); | |
| 202 } | |
| 203 // restore no transition | |
| 204 this.disabled = currentDisabled; | |
| 205 }); | |
| 206 }, | |
| 207 | |
| 208 /** | |
| 209 * Updates the scroll state. | |
| 210 * | |
| 211 * @param {number} scrollTop | |
| 212 * @param {boolean=} forceUpdate (default: false) | |
| 213 */ | |
| 214 _updateScrollState: function(scrollTop, forceUpdate) { | |
| 215 if (this._height === 0) { | |
| 216 return; | |
| 217 } | |
| 218 | |
| 219 var progress = 0; | |
| 220 var top = 0; | |
| 221 var lastTop = this._top; | |
| 222 var lastScrollTop = this._lastScrollTop; | |
| 223 var maxHeaderTop = this._maxHeaderTop; | |
| 224 var dScrollTop = scrollTop - this._lastScrollTop; | |
| 225 var absDScrollTop = Math.abs(dScrollTop); | |
| 226 var isScrollingDown = scrollTop > this._lastScrollTop; | |
| 227 var now = Date.now(); | |
| 228 | |
| 229 if (this._mayMove()) { | |
| 230 top = this._clamp(this.reveals ? lastTop + dScrollTop : scrollTop, 0,
maxHeaderTop); | |
| 231 } | |
| 232 | |
| 233 if (scrollTop >= this._dHeight) { | |
| 234 top = this.condenses && !this.fixed ? Math.max(this._dHeight, top) : t
op; | |
| 235 this.style.transitionDuration = '0ms'; | |
| 236 } | |
| 237 | |
| 238 if (this.reveals && !this.disabled && absDScrollTop < 100) { | |
| 239 // set the initial scroll position | |
| 240 if (now - this._initTimestamp > 300 || this._wasScrollingDown !== isSc
rollingDown) { | |
| 241 this._initScrollTop = scrollTop; | |
| 242 this._initTimestamp = now; | |
| 243 } | |
| 244 | |
| 245 if (scrollTop >= maxHeaderTop) { | |
| 246 // check if the header is allowed to snap | |
| 247 if (Math.abs(this._initScrollTop - scrollTop) > 30 || absDScrollTop
> 10) { | |
| 248 if (isScrollingDown && scrollTop >= maxHeaderTop) { | |
| 249 top = maxHeaderTop; | |
| 250 } else if (!isScrollingDown && scrollTop >= this._dHeight) { | |
| 251 top = this.condenses && !this.fixed ? this._dHeight : 0; | |
| 252 } | |
| 253 var scrollVelocity = dScrollTop / (now - this._lastTimestamp); | |
| 254 this.style.transitionDuration = this._clamp((top - lastTop) / scro
llVelocity, 0, 300) + 'ms'; | |
| 255 } else { | |
| 256 top = this._top; | |
| 257 } | |
| 258 } | |
| 259 } | |
| 260 | |
| 261 if (this._dHeight === 0) { | |
| 262 progress = scrollTop > 0 ? 1 : 0; | |
| 263 } else { | |
| 264 progress = top / this._dHeight; | |
| 265 } | |
| 266 | |
| 267 if (!forceUpdate) { | |
| 268 this._lastScrollTop = scrollTop; | |
| 269 this._top = top; | |
| 270 this._wasScrollingDown = isScrollingDown; | |
| 271 this._lastTimestamp = now; | |
| 272 } | |
| 273 | |
| 274 if (forceUpdate || progress !== this._progress || lastTop !== top || scr
ollTop === 0) { | |
| 275 this._progress = progress; | |
| 276 this._runEffects(progress, top); | |
| 277 this._transformHeader(top); | |
| 278 } | |
| 279 }, | |
| 280 | |
| 281 /** | |
| 282 * Returns true if the current header is allowed to move as the user scrol
ls. | |
| 283 * | |
| 284 * @return {boolean} | |
| 285 */ | |
| 286 _mayMove: function() { | |
| 287 return this.condenses || !this.fixed; | |
| 288 }, | |
| 289 | |
| 290 /** | |
| 291 * Returns true if the current header will condense based on the size of t
he header | |
| 292 * and the `consenses` property. | |
| 293 * | |
| 294 * @return {boolean} | |
| 295 */ | |
| 296 willCondense: function() { | |
| 297 return this._dHeight > 0 && this.condenses; | |
| 298 }, | |
| 299 | |
| 300 /** | |
| 301 * Returns true if the current element is on the screen. | |
| 302 * That is, visible in the current viewport. | |
| 303 * | |
| 304 * @method isOnScreen | |
| 305 * @return {boolean} | |
| 306 */ | |
| 307 isOnScreen: function() { | |
| 308 return this._height !== 0 && this._top < this._height; | |
| 309 }, | |
| 310 | |
| 311 /** | |
| 312 * Returns true if there's content below the current element. | |
| 313 * | |
| 314 * @method isContentBelow | |
| 315 * @return {boolean} | |
| 316 */ | |
| 317 isContentBelow: function() { | |
| 318 if (this._top === 0) { | |
| 319 return this._clampedScrollTop > 0; | |
| 320 } | |
| 321 return this._clampedScrollTop - this._maxHeaderTop >= 0; | |
| 322 }, | |
| 323 | |
| 324 /** | |
| 325 * Transforms the header. | |
| 326 * | |
| 327 * @param {number} y | |
| 328 */ | |
| 329 _transformHeader: function(y) { | |
| 330 this.translate3d(0, (-y) + 'px', 0); | |
| 331 if (this._primaryEl && this.condenses && y >= this._primaryElTop) { | |
| 332 this.translate3d(0, (Math.min(y, this._dHeight) - this._primaryElTop)
+ 'px', 0, | |
| 333 this._primaryEl); | |
| 334 } | |
| 335 }, | |
| 336 | |
| 337 _resizeHandler: function() { | |
| 338 this.resetLayout(); | |
| 339 }, | |
| 340 | |
| 341 _clamp: function(v, min, max) { | |
| 342 return Math.min(max, Math.max(min, v)); | |
| 343 }, | |
| 344 | |
| 345 _ensureBgContainers: function() { | |
| 346 if (!this._bgContainer) { | |
| 347 this._bgContainer = document.createElement('div'); | |
| 348 this._bgContainer.id = 'background'; | |
| 349 | |
| 350 this._bgRear = document.createElement('div'); | |
| 351 this._bgRear.id = 'backgroundRearLayer'; | |
| 352 this._bgContainer.appendChild(this._bgRear); | |
| 353 | |
| 354 this._bgFront = document.createElement('div'); | |
| 355 this._bgFront.id = 'backgroundFrontLayer'; | |
| 356 this._bgContainer.appendChild(this._bgFront); | |
| 357 | |
| 358 Polymer.dom(this.root).insertBefore(this._bgContainer, this.$.contentC
ontainer); | |
| 359 } | |
| 360 }, | |
| 361 | |
| 362 _getDOMRef: function(id) { | |
| 363 switch (id) { | |
| 364 case 'backgroundFrontLayer': | |
| 365 this._ensureBgContainers(); | |
| 366 return this._bgFront; | |
| 367 case 'backgroundRearLayer': | |
| 368 this._ensureBgContainers(); | |
| 369 return this._bgRear; | |
| 370 case 'background': | |
| 371 this._ensureBgContainers(); | |
| 372 return this._bgContainer; | |
| 373 case 'title': | |
| 374 return Polymer.dom(this).querySelector('[title]'); | |
| 375 case 'condensedTitle': | |
| 376 return Polymer.dom(this).querySelector('[condensed-title]'); | |
| 377 } | |
| 378 return null; | |
| 379 }, | |
| 380 | |
| 381 /** | |
| 382 * Returns an object containing the progress value of the scroll effects | |
| 383 * and the top position of the header. | |
| 384 * | |
| 385 * @method getScrollState | |
| 386 * @return {Object} | |
| 387 */ | |
| 388 getScrollState: function() { | |
| 389 return { progress: this._progress, top: this._top }; | |
| 390 } | |
| 391 | |
| 392 /** | |
| 393 * Fires when the layout of `app-header` changed. | |
| 394 * | |
| 395 * @event app-header-reset-layout | |
| 396 */ | |
| 397 }); | |
| OLD | NEW |