| OLD | NEW |
| 1 (function() { | 1 (function() { |
| 2 | 2 |
| 3 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 3 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
| 4 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 4 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
| 5 var DEFAULT_PHYSICAL_COUNT = 20; | 5 var DEFAULT_PHYSICAL_COUNT = 3; |
| 6 var MAX_PHYSICAL_COUNT = 500; | 6 var MAX_PHYSICAL_COUNT = 500; |
| 7 | 7 |
| 8 Polymer({ | 8 Polymer({ |
| 9 | 9 |
| 10 is: 'iron-list', | 10 is: 'iron-list', |
| 11 | 11 |
| 12 properties: { | 12 properties: { |
| 13 | 13 |
| 14 /** | 14 /** |
| 15 * An array containing items determining how many instances of the templat
e | 15 * An array containing items determining how many instances of the templat
e |
| (...skipping 162 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 178 * The estimated scroll height based on `_physicalAverage` | 178 * The estimated scroll height based on `_physicalAverage` |
| 179 */ | 179 */ |
| 180 _estScrollHeight: 0, | 180 _estScrollHeight: 0, |
| 181 | 181 |
| 182 /** | 182 /** |
| 183 * The scroll height of the dom node | 183 * The scroll height of the dom node |
| 184 */ | 184 */ |
| 185 _scrollHeight: 0, | 185 _scrollHeight: 0, |
| 186 | 186 |
| 187 /** | 187 /** |
| 188 * The size of the viewport | 188 * The height of the list. This is referred as the viewport in the context o
f list. |
| 189 */ | 189 */ |
| 190 _viewportSize: 0, | 190 _viewportSize: 0, |
| 191 | 191 |
| 192 /** | 192 /** |
| 193 * An array of DOM nodes that are currently in the tree | 193 * An array of DOM nodes that are currently in the tree |
| 194 * @type {?Array<!TemplatizerNode>} | 194 * @type {?Array<!TemplatizerNode>} |
| 195 */ | 195 */ |
| 196 _physicalItems: null, | 196 _physicalItems: null, |
| 197 | 197 |
| 198 /** | 198 /** |
| (...skipping 15 matching lines...) Expand all Loading... |
| 214 */ | 214 */ |
| 215 _collection: null, | 215 _collection: null, |
| 216 | 216 |
| 217 /** | 217 /** |
| 218 * True if the current item list was rendered for the first time | 218 * True if the current item list was rendered for the first time |
| 219 * after attached. | 219 * after attached. |
| 220 */ | 220 */ |
| 221 _itemsRendered: false, | 221 _itemsRendered: false, |
| 222 | 222 |
| 223 /** | 223 /** |
| 224 * The page that is currently rendered. |
| 225 */ |
| 226 _lastPage: null, |
| 227 |
| 228 /** |
| 229 * The max number of pages to render. One page is equivalent to the height o
f the list. |
| 230 */ |
| 231 _maxPages: 3, |
| 232 |
| 233 /** |
| 224 * The bottom of the physical content. | 234 * The bottom of the physical content. |
| 225 */ | 235 */ |
| 226 get _physicalBottom() { | 236 get _physicalBottom() { |
| 227 return this._physicalTop + this._physicalSize; | 237 return this._physicalTop + this._physicalSize; |
| 228 }, | 238 }, |
| 229 | 239 |
| 230 /** | 240 /** |
| 231 * The bottom of the scroll. | 241 * The bottom of the scroll. |
| 232 */ | 242 */ |
| 233 get _scrollBottom() { | 243 get _scrollBottom() { |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 285 }, | 295 }, |
| 286 | 296 |
| 287 /** | 297 /** |
| 288 * An optimal physical size such that we will have enough physical items | 298 * An optimal physical size such that we will have enough physical items |
| 289 * to fill up the viewport and recycle when the user scrolls. | 299 * to fill up the viewport and recycle when the user scrolls. |
| 290 * | 300 * |
| 291 * This default value assumes that we will at least have the equivalent | 301 * This default value assumes that we will at least have the equivalent |
| 292 * to a viewport of physical items above and below the user's viewport. | 302 * to a viewport of physical items above and below the user's viewport. |
| 293 */ | 303 */ |
| 294 get _optPhysicalSize() { | 304 get _optPhysicalSize() { |
| 295 return this._viewportSize * 3; | 305 return this._viewportSize * this._maxPages; |
| 296 }, | 306 }, |
| 297 | 307 |
| 298 /** | 308 /** |
| 299 * True if the current list is visible. | 309 * True if the current list is visible. |
| 300 */ | 310 */ |
| 301 get _isVisible() { | 311 get _isVisible() { |
| 302 return this._scroller && Boolean(this._scroller.offsetWidth || this._scrol
ler.offsetHeight); | 312 return this._scroller && Boolean(this._scroller.offsetWidth || this._scrol
ler.offsetHeight); |
| 303 }, | 313 }, |
| 304 | 314 |
| 305 /** | 315 /** |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 386 | 396 |
| 387 /** | 397 /** |
| 388 * Update the models, the position of the | 398 * Update the models, the position of the |
| 389 * items in the viewport and recycle tiles as needed. | 399 * items in the viewport and recycle tiles as needed. |
| 390 */ | 400 */ |
| 391 _refresh: function() { | 401 _refresh: function() { |
| 392 // clamp the `scrollTop` value | 402 // clamp the `scrollTop` value |
| 393 // IE 10|11 scrollTop may go above `_maxScrollTop` | 403 // IE 10|11 scrollTop may go above `_maxScrollTop` |
| 394 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` | 404 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` |
| 395 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.sc
rollTop)); | 405 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.sc
rollTop)); |
| 396 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom; | 406 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
| 397 var ratio = this._ratio; | 407 var ratio = this._ratio; |
| 398 var delta = scrollTop - this._scrollPosition; | 408 var delta = scrollTop - this._scrollPosition; |
| 399 var recycledTiles = 0; | 409 var recycledTiles = 0; |
| 400 var hiddenContentSize = this._hiddenContentSize; | 410 var hiddenContentSize = this._hiddenContentSize; |
| 401 var currentRatio = ratio; | 411 var currentRatio = ratio; |
| 402 var movingUp = []; | 412 var movingUp = []; |
| 403 | 413 |
| 404 // track the last `scrollTop` | 414 // track the last `scrollTop` |
| 405 this._scrollPosition = scrollTop; | 415 this._scrollPosition = scrollTop; |
| 406 | 416 |
| 407 // clear cached visible index | 417 // clear cached visible index |
| 408 this._firstVisibleIndexVal = null; | 418 this._firstVisibleIndexVal = null; |
| 409 | 419 |
| 410 scrollBottom = this._scrollBottom; | 420 scrollBottom = this._scrollBottom; |
| 421 physicalBottom = this._physicalBottom; |
| 411 | 422 |
| 412 // random access | 423 // random access |
| 413 if (Math.abs(delta) > this._physicalSize) { | 424 if (Math.abs(delta) > this._physicalSize) { |
| 414 this._physicalTop += delta; | 425 this._physicalTop += delta; |
| 415 recycledTiles = Math.round(delta / this._physicalAverage); | 426 recycledTiles = Math.round(delta / this._physicalAverage); |
| 416 } | 427 } |
| 417 // scroll up | 428 // scroll up |
| 418 else if (delta < 0) { | 429 else if (delta < 0) { |
| 419 var topSpace = scrollTop - this._physicalTop; | 430 var topSpace = scrollTop - this._physicalTop; |
| 420 var virtualStart = this._virtualStart; | 431 var virtualStart = this._virtualStart; |
| 421 var physicalBottom = this._physicalBottom; | |
| 422 | 432 |
| 423 recycledTileSet = []; | 433 recycledTileSet = []; |
| 424 | 434 |
| 425 kth = this._physicalEnd; | 435 kth = this._physicalEnd; |
| 426 currentRatio = topSpace / hiddenContentSize; | 436 currentRatio = topSpace / hiddenContentSize; |
| 427 | 437 |
| 428 // move tiles from bottom to top | 438 // move tiles from bottom to top |
| 429 while ( | 439 while ( |
| 430 // approximate `currentRatio` to `ratio` | 440 // approximate `currentRatio` to `ratio` |
| 431 currentRatio < ratio && | 441 currentRatio < ratio && |
| (...skipping 11 matching lines...) Expand all Loading... |
| 443 recycledTileSet.push(kth); | 453 recycledTileSet.push(kth); |
| 444 recycledTiles++; | 454 recycledTiles++; |
| 445 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | 455 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; |
| 446 } | 456 } |
| 447 | 457 |
| 448 movingUp = recycledTileSet; | 458 movingUp = recycledTileSet; |
| 449 recycledTiles = -recycledTiles; | 459 recycledTiles = -recycledTiles; |
| 450 } | 460 } |
| 451 // scroll down | 461 // scroll down |
| 452 else if (delta > 0) { | 462 else if (delta > 0) { |
| 453 var bottomSpace = this._physicalBottom - scrollBottom; | 463 var bottomSpace = physicalBottom - scrollBottom; |
| 454 var virtualEnd = this._virtualEnd; | 464 var virtualEnd = this._virtualEnd; |
| 455 var lastVirtualItemIndex = this._virtualCount-1; | 465 var lastVirtualItemIndex = this._virtualCount-1; |
| 456 | 466 |
| 457 recycledTileSet = []; | 467 recycledTileSet = []; |
| 458 | 468 |
| 459 kth = this._physicalStart; | 469 kth = this._physicalStart; |
| 460 currentRatio = bottomSpace / hiddenContentSize; | 470 currentRatio = bottomSpace / hiddenContentSize; |
| 461 | 471 |
| 462 // move tiles from top to bottom | 472 // move tiles from top to bottom |
| 463 while ( | 473 while ( |
| (...skipping 14 matching lines...) Expand all Loading... |
| 478 recycledTileSet.push(kth); | 488 recycledTileSet.push(kth); |
| 479 recycledTiles++; | 489 recycledTiles++; |
| 480 kth = (kth + 1) % this._physicalCount; | 490 kth = (kth + 1) % this._physicalCount; |
| 481 } | 491 } |
| 482 } | 492 } |
| 483 | 493 |
| 484 if (recycledTiles === 0) { | 494 if (recycledTiles === 0) { |
| 485 // If the list ever reach this case, the physical average is not signifi
cant enough | 495 // If the list ever reach this case, the physical average is not signifi
cant enough |
| 486 // to create all the items needed to cover the entire viewport. | 496 // to create all the items needed to cover the entire viewport. |
| 487 // e.g. A few items have a height that differs from the average by serve
ral order of magnitude. | 497 // e.g. A few items have a height that differs from the average by serve
ral order of magnitude. |
| 488 if (this._increasePoolIfNeeded()) { | 498 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
| 489 // yield and set models to the new items | 499 this.async(this._increasePool.bind(this, 1)); |
| 490 this.async(this._update); | |
| 491 } | 500 } |
| 492 } else { | 501 } else { |
| 493 this._virtualStart = this._virtualStart + recycledTiles; | 502 this._virtualStart = this._virtualStart + recycledTiles; |
| 494 this._update(recycledTileSet, movingUp); | 503 this._update(recycledTileSet, movingUp); |
| 495 } | 504 } |
| 496 }, | 505 }, |
| 497 | 506 |
| 498 /** | 507 /** |
| 499 * Update the list of items, starting from the `_virtualStartVal` item. | 508 * Update the list of items, starting from the `_virtualStartVal` item. |
| 500 * @param {!Array<number>=} itemSet | 509 * @param {!Array<number>=} itemSet |
| (...skipping 11 matching lines...) Expand all Loading... |
| 512 while (movingUp.length) { | 521 while (movingUp.length) { |
| 513 this._physicalTop -= this._physicalSizes[movingUp.pop()]; | 522 this._physicalTop -= this._physicalSizes[movingUp.pop()]; |
| 514 } | 523 } |
| 515 } | 524 } |
| 516 // update the position of the items | 525 // update the position of the items |
| 517 this._positionItems(); | 526 this._positionItems(); |
| 518 | 527 |
| 519 // set the scroller size | 528 // set the scroller size |
| 520 this._updateScrollerSize(); | 529 this._updateScrollerSize(); |
| 521 | 530 |
| 522 // increase the pool of physical items if needed | 531 // increase the pool of physical items |
| 523 if (this._increasePoolIfNeeded()) { | 532 this._increasePoolIfNeeded(); |
| 524 // yield set models to the new items | |
| 525 this.async(this._update); | |
| 526 } | |
| 527 }, | 533 }, |
| 528 | 534 |
| 529 /** | 535 /** |
| 530 * Creates a pool of DOM elements and attaches them to the local dom. | 536 * Creates a pool of DOM elements and attaches them to the local dom. |
| 531 */ | 537 */ |
| 532 _createPool: function(size) { | 538 _createPool: function(size) { |
| 533 var physicalItems = new Array(size); | 539 var physicalItems = new Array(size); |
| 534 | 540 |
| 535 this._ensureTemplatized(); | 541 this._ensureTemplatized(); |
| 536 | 542 |
| 537 for (var i = 0; i < size; i++) { | 543 for (var i = 0; i < size; i++) { |
| 538 var inst = this.stamp(null); | 544 var inst = this.stamp(null); |
| 539 | |
| 540 // First element child is item; Safari doesn't support children[0] | 545 // First element child is item; Safari doesn't support children[0] |
| 541 // on a doc fragment | 546 // on a doc fragment |
| 542 physicalItems[i] = inst.root.querySelector('*'); | 547 physicalItems[i] = inst.root.querySelector('*'); |
| 543 Polymer.dom(this).appendChild(inst.root); | 548 Polymer.dom(this).appendChild(inst.root); |
| 544 } | 549 } |
| 545 | 550 |
| 546 return physicalItems; | 551 return physicalItems; |
| 547 }, | 552 }, |
| 548 | 553 |
| 549 /** | 554 /** |
| 550 * Increases the pool of physical items only if needed. | 555 * Increases the pool of physical items only if needed. |
| 551 * This function will allocate additional physical items | 556 * This function will allocate additional physical items |
| 552 * (limited by `MAX_PHYSICAL_COUNT`) if the content size is shorter than | 557 * if the physical size is shorter than `_optPhysicalSize` |
| 553 * `_optPhysicalSize` | |
| 554 * | |
| 555 * @return boolean | |
| 556 */ | 558 */ |
| 557 _increasePoolIfNeeded: function() { | 559 _increasePoolIfNeeded: function() { |
| 558 if (this._physicalAverage === 0) { | 560 if (this._viewportSize !== 0 && this._physicalSize < this._optPhysicalSize
) { |
| 559 return false; | 561 // 0 <= `currentPage` <= `_maxPages` |
| 560 } | 562 var currentPage = Math.floor(this._physicalSize / this._viewportSize); |
| 561 if (this._physicalBottom < this._scrollBottom || this._physicalTop > this.
_scrollPosition) { | 563 |
| 562 return this._increasePool(1); | 564 if (currentPage === 0) { |
| 563 } | 565 // fill the first page |
| 564 if (this._physicalSize < this._optPhysicalSize) { | 566 this.async(this._increasePool.bind(this, Math.round(this._physicalCoun
t * 0.5))); |
| 565 return this._increasePool(Math.round((this._optPhysicalSize - this._phys
icalSize) * 1.2 / this._physicalAverage)); | 567 } else if (this._lastPage !== currentPage) { |
| 568 // once a page is filled up, paint it and defer the next increase |
| 569 requestAnimationFrame(this._increasePool.bind(this, 1)); |
| 570 } else { |
| 571 // fill the rest of the pages |
| 572 this.async(this._increasePool.bind(this, 1)); |
| 573 } |
| 574 this._lastPage = currentPage; |
| 575 return true; |
| 566 } | 576 } |
| 567 return false; | 577 return false; |
| 568 }, | 578 }, |
| 569 | 579 |
| 570 /** | 580 /** |
| 571 * Increases the pool size. | 581 * Increases the pool size. |
| 572 */ | 582 */ |
| 573 _increasePool: function(missingItems) { | 583 _increasePool: function(missingItems) { |
| 574 // limit the size | 584 // limit the size |
| 575 var nextPhysicalCount = Math.min( | 585 var nextPhysicalCount = Math.min( |
| 576 this._physicalCount + missingItems, | 586 this._physicalCount + missingItems, |
| 577 this._virtualCount, | 587 this._virtualCount, |
| 578 MAX_PHYSICAL_COUNT | 588 MAX_PHYSICAL_COUNT |
| 579 ); | 589 ); |
| 580 | |
| 581 var prevPhysicalCount = this._physicalCount; | 590 var prevPhysicalCount = this._physicalCount; |
| 582 var delta = nextPhysicalCount - prevPhysicalCount; | 591 var delta = nextPhysicalCount - prevPhysicalCount; |
| 583 | 592 |
| 584 if (delta <= 0) { | 593 if (delta > 0) { |
| 585 return false; | 594 [].push.apply(this._physicalItems, this._createPool(delta)); |
| 595 [].push.apply(this._physicalSizes, new Array(delta)); |
| 596 |
| 597 this._physicalCount = prevPhysicalCount + delta; |
| 598 // tail call |
| 599 return this._update(); |
| 586 } | 600 } |
| 587 | |
| 588 [].push.apply(this._physicalItems, this._createPool(delta)); | |
| 589 [].push.apply(this._physicalSizes, new Array(delta)); | |
| 590 | |
| 591 this._physicalCount = prevPhysicalCount + delta; | |
| 592 | |
| 593 return true; | |
| 594 }, | 601 }, |
| 595 | 602 |
| 596 /** | 603 /** |
| 597 * Render a new list of items. This method does exactly the same as `update`
, | 604 * Render a new list of items. This method does exactly the same as `update`
, |
| 598 * but it also ensures that only one `update` cycle is created. | 605 * but it also ensures that only one `update` cycle is created. |
| 599 */ | 606 */ |
| 600 _render: function() { | 607 _render: function() { |
| 601 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 608 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
| 602 | 609 |
| 603 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 610 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
| 611 this._lastPage = 0; |
| 604 this._update(); | 612 this._update(); |
| 605 this._itemsRendered = true; | 613 this._itemsRendered = true; |
| 606 } | 614 } |
| 607 }, | 615 }, |
| 608 | 616 |
| 609 /** | 617 /** |
| 610 * Templetizes the user template. | 618 * Templetizes the user template. |
| 611 */ | 619 */ |
| 612 _ensureTemplatized: function() { | 620 _ensureTemplatized: function() { |
| 613 if (!this.ctor) { | 621 if (!this.ctor) { |
| (...skipping 343 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 957 | 965 |
| 958 // estimate new physical offset | 966 // estimate new physical offset |
| 959 this._physicalTop = this._virtualStart * this._physicalAverage; | 967 this._physicalTop = this._virtualStart * this._physicalAverage; |
| 960 | 968 |
| 961 var currentTopItem = this._physicalStart; | 969 var currentTopItem = this._physicalStart; |
| 962 var currentVirtualItem = this._virtualStart; | 970 var currentVirtualItem = this._virtualStart; |
| 963 var targetOffsetTop = 0; | 971 var targetOffsetTop = 0; |
| 964 var hiddenContentSize = this._hiddenContentSize; | 972 var hiddenContentSize = this._hiddenContentSize; |
| 965 | 973 |
| 966 // scroll to the item as much as we can | 974 // scroll to the item as much as we can |
| 967 while (currentVirtualItem !== idx && targetOffsetTop < hiddenContentSize)
{ | 975 while (currentVirtualItem < idx && targetOffsetTop < hiddenContentSize) { |
| 968 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem]; | 976 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem]; |
| 969 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 977 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
| 970 currentVirtualItem++; | 978 currentVirtualItem++; |
| 971 } | 979 } |
| 972 | 980 |
| 973 // update the scroller size | 981 // update the scroller size |
| 974 this._updateScrollerSize(true); | 982 this._updateScrollerSize(true); |
| 975 | 983 |
| 976 // update the position of the items | 984 // update the position of the items |
| 977 this._positionItems(); | 985 this._positionItems(); |
| 978 | 986 |
| 979 // set the new scroll position | 987 // set the new scroll position |
| 980 this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1); | 988 this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1); |
| 981 | 989 |
| 982 // increase the pool of physical items if needed | 990 // increase the pool of physical items if needed |
| 983 if (this._increasePoolIfNeeded()) { | 991 this._increasePoolIfNeeded(); |
| 984 // yield set models to the new items | 992 |
| 985 this.async(this._update); | |
| 986 } | |
| 987 // clear cached visible index | 993 // clear cached visible index |
| 988 this._firstVisibleIndexVal = null; | 994 this._firstVisibleIndexVal = null; |
| 989 }, | 995 }, |
| 990 | 996 |
| 991 /** | 997 /** |
| 992 * Reset the physical average and the average count. | 998 * Reset the physical average and the average count. |
| 993 */ | 999 */ |
| 994 _resetAverage: function() { | 1000 _resetAverage: function() { |
| 995 this._physicalAverage = 0; | 1001 this._physicalAverage = 0; |
| 996 this._physicalAverageCount = 0; | 1002 this._physicalAverageCount = 0; |
| (...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1155 var pidx = this._physicalIndexForKey[key]; | 1161 var pidx = this._physicalIndexForKey[key]; |
| 1156 | 1162 |
| 1157 if (pidx !== undefined) { | 1163 if (pidx !== undefined) { |
| 1158 this._updateMetrics([pidx]); | 1164 this._updateMetrics([pidx]); |
| 1159 this._positionItems(); | 1165 this._positionItems(); |
| 1160 } | 1166 } |
| 1161 } | 1167 } |
| 1162 }); | 1168 }); |
| 1163 | 1169 |
| 1164 })(); | 1170 })(); |
| OLD | NEW |