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 = 3; | 5 var DEFAULT_PHYSICAL_COUNT = 3; |
6 var MAX_PHYSICAL_COUNT = 500; | 6 var MAX_PHYSICAL_COUNT = 500; |
7 var HIDDEN_Y = '-10000px'; | |
7 | 8 |
8 Polymer({ | 9 Polymer({ |
9 | 10 |
10 is: 'iron-list', | 11 is: 'iron-list', |
11 | 12 |
12 properties: { | 13 properties: { |
13 | 14 |
14 /** | 15 /** |
15 * An array containing items determining how many instances of the templat e | 16 * An array containing items determining how many instances of the templat e |
16 * to stamp and that that each template instance should bind to. | 17 * to stamp and that that each template instance should bind to. |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
82 */ | 83 */ |
83 multiSelection: { | 84 multiSelection: { |
84 type: Boolean, | 85 type: Boolean, |
85 value: false | 86 value: false |
86 } | 87 } |
87 }, | 88 }, |
88 | 89 |
89 observers: [ | 90 observers: [ |
90 '_itemsChanged(items.*)', | 91 '_itemsChanged(items.*)', |
91 '_selectionEnabledChanged(selectionEnabled)', | 92 '_selectionEnabledChanged(selectionEnabled)', |
92 '_multiSelectionChanged(multiSelection)' | 93 '_multiSelectionChanged(multiSelection)', |
94 '_setOverflow(scrollTarget)' | |
93 ], | 95 ], |
94 | 96 |
95 behaviors: [ | 97 behaviors: [ |
96 Polymer.Templatizer, | 98 Polymer.Templatizer, |
97 Polymer.IronResizableBehavior | 99 Polymer.IronResizableBehavior, |
100 Polymer.IronA11yKeysBehavior, | |
101 Polymer.IronScrollTargetBehavior | |
98 ], | 102 ], |
99 | 103 |
100 listeners: { | 104 listeners: { |
101 'iron-resize': '_resizeHandler' | 105 'iron-resize': '_resizeHandler' |
102 }, | 106 }, |
103 | 107 |
108 keyBindings: { | |
109 'up': '_didMoveUp', | |
110 'down': '_didMoveDown', | |
111 'enter': '_didEnter' | |
112 }, | |
113 | |
104 /** | 114 /** |
105 * The ratio of hidden tiles that should remain in the scroll direction. | 115 * The ratio of hidden tiles that should remain in the scroll direction. |
106 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons. | 116 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons. |
107 */ | 117 */ |
108 _ratio: 0.5, | 118 _ratio: 0.5, |
109 | 119 |
110 /** | 120 /** |
111 * The element that controls the scroll | |
112 * @type {?Element} | |
113 */ | |
114 _scroller: null, | |
115 | |
116 /** | |
117 * The padding-top value of the `scroller` element | 121 * The padding-top value of the `scroller` element |
118 */ | 122 */ |
119 _scrollerPaddingTop: 0, | 123 _scrollerPaddingTop: 0, |
120 | 124 |
121 /** | 125 /** |
122 * This value is the same as `scrollTop`. | 126 * This value is the same as `scrollTop`. |
123 */ | 127 */ |
124 _scrollPosition: 0, | 128 _scrollPosition: 0, |
125 | 129 |
126 /** | 130 /** |
(...skipping 10 matching lines...) Expand all Loading... | |
137 * The k-th tile that is at the bottom of the scrolling list. | 141 * The k-th tile that is at the bottom of the scrolling list. |
138 */ | 142 */ |
139 _physicalEnd: 0, | 143 _physicalEnd: 0, |
140 | 144 |
141 /** | 145 /** |
142 * The sum of the heights of all the tiles in the DOM. | 146 * The sum of the heights of all the tiles in the DOM. |
143 */ | 147 */ |
144 _physicalSize: 0, | 148 _physicalSize: 0, |
145 | 149 |
146 /** | 150 /** |
147 * The average `offsetHeight` of the tiles observed till now. | 151 * The average `F` of the tiles observed till now. |
148 */ | 152 */ |
149 _physicalAverage: 0, | 153 _physicalAverage: 0, |
150 | 154 |
151 /** | 155 /** |
152 * The number of tiles which `offsetHeight` > 0 observed until now. | 156 * The number of tiles which `offsetHeight` > 0 observed until now. |
153 */ | 157 */ |
154 _physicalAverageCount: 0, | 158 _physicalAverageCount: 0, |
155 | 159 |
156 /** | 160 /** |
157 * The Y position of the item rendered in the `_physicalStart` | 161 * The Y position of the item rendered in the `_physicalStart` |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
195 */ | 199 */ |
196 _physicalItems: null, | 200 _physicalItems: null, |
197 | 201 |
198 /** | 202 /** |
199 * An array of heights for each item in `_physicalItems` | 203 * An array of heights for each item in `_physicalItems` |
200 * @type {?Array<number>} | 204 * @type {?Array<number>} |
201 */ | 205 */ |
202 _physicalSizes: null, | 206 _physicalSizes: null, |
203 | 207 |
204 /** | 208 /** |
205 * A cached value for the visible index. | 209 * A cached value for the first visible index. |
206 * See `firstVisibleIndex` | 210 * See `firstVisibleIndex` |
207 * @type {?number} | 211 * @type {?number} |
208 */ | 212 */ |
209 _firstVisibleIndexVal: null, | 213 _firstVisibleIndexVal: null, |
210 | 214 |
211 /** | 215 /** |
216 * A cached value for the last visible index. | |
217 * See `lastVisibleIndex` | |
218 * @type {?number} | |
219 */ | |
220 _lastVisibleIndexVal: null, | |
221 | |
222 | |
223 /** | |
212 * A Polymer collection for the items. | 224 * A Polymer collection for the items. |
213 * @type {?Polymer.Collection} | 225 * @type {?Polymer.Collection} |
214 */ | 226 */ |
215 _collection: null, | 227 _collection: null, |
216 | 228 |
217 /** | 229 /** |
218 * True if the current item list was rendered for the first time | 230 * True if the current item list was rendered for the first time |
219 * after attached. | 231 * after attached. |
220 */ | 232 */ |
221 _itemsRendered: false, | 233 _itemsRendered: false, |
222 | 234 |
223 /** | 235 /** |
224 * The page that is currently rendered. | 236 * The page that is currently rendered. |
225 */ | 237 */ |
226 _lastPage: null, | 238 _lastPage: null, |
227 | 239 |
228 /** | 240 /** |
229 * The max number of pages to render. One page is equivalent to the height o f the list. | 241 * The max number of pages to render. One page is equivalent to the height o f the list. |
230 */ | 242 */ |
231 _maxPages: 3, | 243 _maxPages: 3, |
232 | 244 |
233 /** | 245 /** |
246 * The currently focused item index. | |
247 */ | |
248 _focusedIndex: 0, | |
249 | |
250 /** | |
251 * The the item that is focused if it is moved offscreen. | |
252 * @private {?TemplatizerNode} | |
253 */ | |
254 _offscreenFocusedItem: null, | |
255 | |
256 /** | |
257 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
258 * list when that item is moved offscreen. | |
259 */ | |
260 _focusBackfillItem: null, | |
261 | |
262 /** | |
234 * The bottom of the physical content. | 263 * The bottom of the physical content. |
235 */ | 264 */ |
236 get _physicalBottom() { | 265 get _physicalBottom() { |
237 return this._physicalTop + this._physicalSize; | 266 return this._physicalTop + this._physicalSize; |
238 }, | 267 }, |
239 | 268 |
240 /** | 269 /** |
241 * The bottom of the scroll. | 270 * The bottom of the scroll. |
242 */ | 271 */ |
243 get _scrollBottom() { | 272 get _scrollBottom() { |
244 return this._scrollPosition + this._viewportSize; | 273 return this._scrollPosition + this._viewportSize; |
245 }, | 274 }, |
246 | 275 |
247 /** | 276 /** |
248 * The n-th item rendered in the last physical item. | 277 * The n-th item rendered in the last physical item. |
249 */ | 278 */ |
250 get _virtualEnd() { | 279 get _virtualEnd() { |
251 return this._virtualStartVal + this._physicalCount - 1; | 280 return this._virtualStart + this._physicalCount - 1; |
252 }, | 281 }, |
253 | 282 |
254 /** | 283 /** |
255 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`. | 284 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`. |
256 */ | 285 */ |
257 _minVirtualStart: 0, | 286 _minVirtualStart: 0, |
258 | 287 |
259 /** | 288 /** |
260 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`. | 289 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`. |
261 */ | 290 */ |
(...skipping 14 matching lines...) Expand all Loading... | |
276 get _maxScrollTop() { | 305 get _maxScrollTop() { |
277 return this._estScrollHeight - this._viewportSize; | 306 return this._estScrollHeight - this._viewportSize; |
278 }, | 307 }, |
279 | 308 |
280 /** | 309 /** |
281 * Sets the n-th item rendered in `_physicalStart` | 310 * Sets the n-th item rendered in `_physicalStart` |
282 */ | 311 */ |
283 set _virtualStart(val) { | 312 set _virtualStart(val) { |
284 // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart | 313 // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart |
285 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val)); | 314 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val)); |
286 this._physicalStart = this._virtualStartVal % this._physicalCount; | 315 if (this._physicalCount === 0) { |
287 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount; | 316 this._physicalStart = 0; |
317 this._physicalEnd = 0; | |
318 } else { | |
319 this._physicalStart = this._virtualStartVal % this._physicalCount; | |
320 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % th is._physicalCount; | |
321 } | |
288 }, | 322 }, |
289 | 323 |
290 /** | 324 /** |
291 * Gets the n-th item rendered in `_physicalStart` | 325 * Gets the n-th item rendered in `_physicalStart` |
292 */ | 326 */ |
293 get _virtualStart() { | 327 get _virtualStart() { |
294 return this._virtualStartVal; | 328 return this._virtualStartVal; |
295 }, | 329 }, |
296 | 330 |
297 /** | 331 /** |
298 * An optimal physical size such that we will have enough physical items | 332 * An optimal physical size such that we will have enough physical items |
299 * to fill up the viewport and recycle when the user scrolls. | 333 * to fill up the viewport and recycle when the user scrolls. |
300 * | 334 * |
301 * This default value assumes that we will at least have the equivalent | 335 * This default value assumes that we will at least have the equivalent |
302 * to a viewport of physical items above and below the user's viewport. | 336 * to a viewport of physical items above and below the user's viewport. |
303 */ | 337 */ |
304 get _optPhysicalSize() { | 338 get _optPhysicalSize() { |
305 return this._viewportSize * this._maxPages; | 339 return this._viewportSize * this._maxPages; |
306 }, | 340 }, |
307 | 341 |
308 /** | 342 /** |
309 * True if the current list is visible. | 343 * True if the current list is visible. |
310 */ | 344 */ |
311 get _isVisible() { | 345 get _isVisible() { |
312 return this._scroller && Boolean(this._scroller.offsetWidth || this._scrol ler.offsetHeight); | 346 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this. scrollTarget.offsetHeight); |
313 }, | 347 }, |
314 | 348 |
315 /** | 349 /** |
316 * Gets the index of the first visible item in the viewport. | 350 * Gets the index of the first visible item in the viewport. |
317 * | 351 * |
318 * @type {number} | 352 * @type {number} |
319 */ | 353 */ |
320 get firstVisibleIndex() { | 354 get firstVisibleIndex() { |
321 var physicalOffset; | |
322 | |
323 if (this._firstVisibleIndexVal === null) { | 355 if (this._firstVisibleIndexVal === null) { |
324 physicalOffset = this._physicalTop; | 356 var physicalOffset = this._physicalTop; |
325 | 357 |
326 this._firstVisibleIndexVal = this._iterateItems( | 358 this._firstVisibleIndexVal = this._iterateItems( |
327 function(pidx, vidx) { | 359 function(pidx, vidx) { |
328 physicalOffset += this._physicalSizes[pidx]; | 360 physicalOffset += this._physicalSizes[pidx]; |
329 | 361 |
330 if (physicalOffset > this._scrollPosition) { | 362 if (physicalOffset > this._scrollPosition) { |
331 return vidx; | 363 return vidx; |
332 } | 364 } |
333 }) || 0; | 365 }) || 0; |
334 } | 366 } |
335 | |
336 return this._firstVisibleIndexVal; | 367 return this._firstVisibleIndexVal; |
337 }, | 368 }, |
338 | 369 |
339 ready: function() { | 370 /** |
340 if (IOS_TOUCH_SCROLLING) { | 371 * Gets the index of the last visible item in the viewport. |
341 this._scrollListener = function() { | 372 * |
342 requestAnimationFrame(this._scrollHandler.bind(this)); | 373 * @type {number} |
343 }.bind(this); | 374 */ |
344 } else { | 375 get lastVisibleIndex() { |
345 this._scrollListener = this._scrollHandler.bind(this); | 376 if (this._lastVisibleIndexVal === null) { |
377 var physicalOffset = this._physicalTop; | |
378 | |
379 this._iterateItems(function(pidx, vidx) { | |
380 physicalOffset += this._physicalSizes[pidx]; | |
381 | |
382 if(physicalOffset <= this._scrollBottom) { | |
383 this._lastVisibleIndexVal = vidx; | |
384 } | |
385 }); | |
346 } | 386 } |
387 return this._lastVisibleIndexVal; | |
347 }, | 388 }, |
348 | 389 |
349 /** | 390 ready: function() { |
350 * When the element has been attached to the DOM tree. | 391 this.addEventListener('focus', this._didFocus.bind(this), true); |
351 */ | 392 }, |
393 | |
352 attached: function() { | 394 attached: function() { |
353 // delegate to the parent's scroller | |
354 // e.g. paper-scroll-header-panel | |
355 var el = Polymer.dom(this); | |
356 | |
357 var parentNode = /** @type {?{scroller: ?Element}} */ (el.parentNode); | |
358 if (parentNode && parentNode.scroller) { | |
359 this._scroller = parentNode.scroller; | |
360 } else { | |
361 this._scroller = this; | |
362 this.classList.add('has-scroller'); | |
363 } | |
364 | |
365 if (IOS_TOUCH_SCROLLING) { | |
366 this._scroller.style.webkitOverflowScrolling = 'touch'; | |
367 } | |
368 | |
369 this._scroller.addEventListener('scroll', this._scrollListener); | |
370 | |
371 this.updateViewportBoundaries(); | 395 this.updateViewportBoundaries(); |
372 this._render(); | 396 this._render(); |
373 }, | 397 }, |
374 | 398 |
375 /** | |
376 * When the element has been removed from the DOM tree. | |
377 */ | |
378 detached: function() { | 399 detached: function() { |
379 this._itemsRendered = false; | 400 this._itemsRendered = false; |
380 if (this._scroller) { | 401 }, |
381 this._scroller.removeEventListener('scroll', this._scrollListener); | 402 |
382 } | 403 get _defaultScrollTarget() { |
404 return this; | |
383 }, | 405 }, |
384 | 406 |
385 /** | 407 /** |
408 * Set the overflow property if this element has its own scrolling region | |
409 */ | |
410 _setOverflow: function(scrollTarget) { | |
411 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | |
412 this.style.overflow = scrollTarget === this ? 'auto' : ''; | |
413 }, | |
414 | |
415 /** | |
386 * Invoke this method if you dynamically update the viewport's | 416 * Invoke this method if you dynamically update the viewport's |
387 * size or CSS padding. | 417 * size or CSS padding. |
388 * | 418 * |
389 * @method updateViewportBoundaries | 419 * @method updateViewportBoundaries |
390 */ | 420 */ |
391 updateViewportBoundaries: function() { | 421 updateViewportBoundaries: function() { |
392 var scrollerStyle = window.getComputedStyle(this._scroller); | 422 var scrollerStyle = window.getComputedStyle(this.scrollTarget); |
393 this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10); | 423 this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10); |
394 this._viewportSize = this._scroller.offsetHeight; | 424 this._viewportSize = this._scrollTargetHeight; |
395 }, | 425 }, |
396 | 426 |
397 /** | 427 /** |
398 * Update the models, the position of the | 428 * Update the models, the position of the |
399 * items in the viewport and recycle tiles as needed. | 429 * items in the viewport and recycle tiles as needed. |
400 */ | 430 */ |
401 _refresh: function() { | 431 _scrollHandler: function() { |
402 // clamp the `scrollTop` value | 432 // clamp the `scrollTop` value |
403 // IE 10|11 scrollTop may go above `_maxScrollTop` | 433 // IE 10|11 scrollTop may go above `_maxScrollTop` |
404 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` | 434 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` |
405 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.sc rollTop)); | 435 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)) ; |
406 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m; | 436 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto m; |
407 var ratio = this._ratio; | 437 var ratio = this._ratio; |
408 var delta = scrollTop - this._scrollPosition; | 438 var delta = scrollTop - this._scrollPosition; |
409 var recycledTiles = 0; | 439 var recycledTiles = 0; |
410 var hiddenContentSize = this._hiddenContentSize; | 440 var hiddenContentSize = this._hiddenContentSize; |
411 var currentRatio = ratio; | 441 var currentRatio = ratio; |
412 var movingUp = []; | 442 var movingUp = []; |
413 | 443 |
414 // track the last `scrollTop` | 444 // track the last `scrollTop` |
415 this._scrollPosition = scrollTop; | 445 this._scrollPosition = scrollTop; |
416 | 446 |
417 // clear cached visible index | 447 // clear cached visible index |
418 this._firstVisibleIndexVal = null; | 448 this._firstVisibleIndexVal = null; |
449 this._lastVisibleIndexVal = null; | |
419 | 450 |
420 scrollBottom = this._scrollBottom; | 451 scrollBottom = this._scrollBottom; |
421 physicalBottom = this._physicalBottom; | 452 physicalBottom = this._physicalBottom; |
422 | 453 |
423 // random access | 454 // random access |
424 if (Math.abs(delta) > this._physicalSize) { | 455 if (Math.abs(delta) > this._physicalSize) { |
425 this._physicalTop += delta; | 456 this._physicalTop += delta; |
426 recycledTiles = Math.round(delta / this._physicalAverage); | 457 recycledTiles = Math.round(delta / this._physicalAverage); |
427 } | 458 } |
428 // scroll up | 459 // scroll up |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
498 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 529 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
499 this.async(this._increasePool.bind(this, 1)); | 530 this.async(this._increasePool.bind(this, 1)); |
500 } | 531 } |
501 } else { | 532 } else { |
502 this._virtualStart = this._virtualStart + recycledTiles; | 533 this._virtualStart = this._virtualStart + recycledTiles; |
503 this._update(recycledTileSet, movingUp); | 534 this._update(recycledTileSet, movingUp); |
504 } | 535 } |
505 }, | 536 }, |
506 | 537 |
507 /** | 538 /** |
508 * Update the list of items, starting from the `_virtualStartVal` item. | 539 * Update the list of items, starting from the `_virtualStart` item. |
509 * @param {!Array<number>=} itemSet | 540 * @param {!Array<number>=} itemSet |
510 * @param {!Array<number>=} movingUp | 541 * @param {!Array<number>=} movingUp |
511 */ | 542 */ |
512 _update: function(itemSet, movingUp) { | 543 _update: function(itemSet, movingUp) { |
544 // manage focus | |
545 if (this._isIndexRendered(this._focusedIndex)) { | |
546 this._restoreFocusedItem(); | |
547 } else { | |
548 this._createFocusBackfillItem(); | |
549 } | |
513 // update models | 550 // update models |
514 this._assignModels(itemSet); | 551 this._assignModels(itemSet); |
515 | |
516 // measure heights | 552 // measure heights |
517 this._updateMetrics(itemSet); | 553 this._updateMetrics(itemSet); |
518 | |
519 // adjust offset after measuring | 554 // adjust offset after measuring |
520 if (movingUp) { | 555 if (movingUp) { |
521 while (movingUp.length) { | 556 while (movingUp.length) { |
522 this._physicalTop -= this._physicalSizes[movingUp.pop()]; | 557 this._physicalTop -= this._physicalSizes[movingUp.pop()]; |
523 } | 558 } |
524 } | 559 } |
525 // update the position of the items | 560 // update the position of the items |
526 this._positionItems(); | 561 this._positionItems(); |
527 | |
528 // set the scroller size | 562 // set the scroller size |
529 this._updateScrollerSize(); | 563 this._updateScrollerSize(); |
530 | |
531 // increase the pool of physical items | 564 // increase the pool of physical items |
532 this._increasePoolIfNeeded(); | 565 this._increasePoolIfNeeded(); |
533 }, | 566 }, |
534 | 567 |
535 /** | 568 /** |
536 * Creates a pool of DOM elements and attaches them to the local dom. | 569 * Creates a pool of DOM elements and attaches them to the local dom. |
537 */ | 570 */ |
538 _createPool: function(size) { | 571 _createPool: function(size) { |
539 var physicalItems = new Array(size); | 572 var physicalItems = new Array(size); |
540 | 573 |
541 this._ensureTemplatized(); | 574 this._ensureTemplatized(); |
542 | 575 |
543 for (var i = 0; i < size; i++) { | 576 for (var i = 0; i < size; i++) { |
544 var inst = this.stamp(null); | 577 var inst = this.stamp(null); |
545 // First element child is item; Safari doesn't support children[0] | 578 // First element child is item; Safari doesn't support children[0] |
546 // on a doc fragment | 579 // on a doc fragment |
547 physicalItems[i] = inst.root.querySelector('*'); | 580 physicalItems[i] = inst.root.querySelector('*'); |
548 Polymer.dom(this).appendChild(inst.root); | 581 Polymer.dom(this).appendChild(inst.root); |
549 } | 582 } |
550 | |
551 return physicalItems; | 583 return physicalItems; |
552 }, | 584 }, |
553 | 585 |
554 /** | 586 /** |
555 * Increases the pool of physical items only if needed. | 587 * Increases the pool of physical items only if needed. |
556 * This function will allocate additional physical items | 588 * This function will allocate additional physical items |
557 * if the physical size is shorter than `_optPhysicalSize` | 589 * if the physical size is shorter than `_optPhysicalSize` |
558 */ | 590 */ |
559 _increasePoolIfNeeded: function() { | 591 _increasePoolIfNeeded: function() { |
560 if (this._viewportSize !== 0 && this._physicalSize < this._optPhysicalSize ) { | 592 if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSiz e) { |
561 // 0 <= `currentPage` <= `_maxPages` | 593 return false; |
562 var currentPage = Math.floor(this._physicalSize / this._viewportSize); | |
563 | |
564 if (currentPage === 0) { | |
565 // fill the first page | |
566 this.async(this._increasePool.bind(this, Math.round(this._physicalCoun t * 0.5))); | |
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; | |
576 } | 594 } |
577 return false; | 595 // 0 <= `currentPage` <= `_maxPages` |
596 var currentPage = Math.floor(this._physicalSize / this._viewportSize); | |
597 if (currentPage === 0) { | |
598 // fill the first page | |
599 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph ysicalCount * 0.5))); | |
600 } else if (this._lastPage !== currentPage) { | |
601 // paint the page and defer the next increase | |
602 // wait 16ms which is rough enough to get paint cycle. | |
603 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa sePool.bind(this, 1), 16)); | |
604 } else { | |
605 // fill the rest of the pages | |
606 this._debounceTemplate(this._increasePool.bind(this, 1)); | |
607 } | |
608 this._lastPage = currentPage; | |
609 return true; | |
578 }, | 610 }, |
579 | 611 |
580 /** | 612 /** |
581 * Increases the pool size. | 613 * Increases the pool size. |
582 */ | 614 */ |
583 _increasePool: function(missingItems) { | 615 _increasePool: function(missingItems) { |
584 // limit the size | 616 // limit the size |
585 var nextPhysicalCount = Math.min( | 617 var nextPhysicalCount = Math.min( |
586 this._physicalCount + missingItems, | 618 this._physicalCount + missingItems, |
587 this._virtualCount, | 619 this._virtualCount - this._virtualStart, |
588 MAX_PHYSICAL_COUNT | 620 MAX_PHYSICAL_COUNT |
589 ); | 621 ); |
590 var prevPhysicalCount = this._physicalCount; | 622 var prevPhysicalCount = this._physicalCount; |
591 var delta = nextPhysicalCount - prevPhysicalCount; | 623 var delta = nextPhysicalCount - prevPhysicalCount; |
592 | 624 |
593 if (delta > 0) { | 625 if (delta > 0) { |
594 [].push.apply(this._physicalItems, this._createPool(delta)); | 626 [].push.apply(this._physicalItems, this._createPool(delta)); |
595 [].push.apply(this._physicalSizes, new Array(delta)); | 627 [].push.apply(this._physicalSizes, new Array(delta)); |
596 | 628 |
597 this._physicalCount = prevPhysicalCount + delta; | 629 this._physicalCount = prevPhysicalCount + delta; |
598 // tail call | 630 // tail call |
599 return this._update(); | 631 return this._update(); |
600 } | 632 } |
601 }, | 633 }, |
602 | 634 |
603 /** | 635 /** |
604 * Render a new list of items. This method does exactly the same as `update` , | 636 * Render a new list of items. This method does exactly the same as `update` , |
605 * but it also ensures that only one `update` cycle is created. | 637 * but it also ensures that only one `update` cycle is created. |
606 */ | 638 */ |
607 _render: function() { | 639 _render: function() { |
608 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 640 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
609 | 641 |
610 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) { | 642 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) { |
611 this._lastPage = 0; | 643 this._lastPage = 0; |
612 this._update(); | 644 this._update(); |
645 this._scrollHandler(); | |
613 this._itemsRendered = true; | 646 this._itemsRendered = true; |
614 } | 647 } |
615 }, | 648 }, |
616 | 649 |
617 /** | 650 /** |
618 * Templetizes the user template. | 651 * Templetizes the user template. |
619 */ | 652 */ |
620 _ensureTemplatized: function() { | 653 _ensureTemplatized: function() { |
621 if (!this.ctor) { | 654 if (!this.ctor) { |
622 // Template instance props that should be excluded from forwarding | 655 // Template instance props that should be excluded from forwarding |
623 var props = {}; | 656 var props = {}; |
624 | |
625 props.__key__ = true; | 657 props.__key__ = true; |
626 props[this.as] = true; | 658 props[this.as] = true; |
627 props[this.indexAs] = true; | 659 props[this.indexAs] = true; |
628 props[this.selectedAs] = true; | 660 props[this.selectedAs] = true; |
661 props.tabIndex = true; | |
629 | 662 |
630 this._instanceProps = props; | 663 this._instanceProps = props; |
631 this._userTemplate = Polymer.dom(this).querySelector('template'); | 664 this._userTemplate = Polymer.dom(this).querySelector('template'); |
632 | 665 |
633 if (this._userTemplate) { | 666 if (this._userTemplate) { |
634 this.templatize(this._userTemplate); | 667 this.templatize(this._userTemplate); |
635 } else { | 668 } else { |
636 console.warn('iron-list requires a template to be provided in light-do m'); | 669 console.warn('iron-list requires a template to be provided in light-do m'); |
637 } | 670 } |
638 } | 671 } |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
686 /** | 719 /** |
687 * Called as a side effect of a host items.<key>.<path> path change, | 720 * Called as a side effect of a host items.<key>.<path> path change, |
688 * responsible for notifying item.<path> changes to row for key. | 721 * responsible for notifying item.<path> changes to row for key. |
689 */ | 722 */ |
690 _forwardItemPath: function(path, value) { | 723 _forwardItemPath: function(path, value) { |
691 if (this._physicalIndexForKey) { | 724 if (this._physicalIndexForKey) { |
692 var dot = path.indexOf('.'); | 725 var dot = path.indexOf('.'); |
693 var key = path.substring(0, dot < 0 ? path.length : dot); | 726 var key = path.substring(0, dot < 0 ? path.length : dot); |
694 var idx = this._physicalIndexForKey[key]; | 727 var idx = this._physicalIndexForKey[key]; |
695 var row = this._physicalItems[idx]; | 728 var row = this._physicalItems[idx]; |
729 | |
730 if (idx === this._focusedIndex && this._offscreenFocusedItem) { | |
731 row = this._offscreenFocusedItem; | |
732 } | |
696 if (row) { | 733 if (row) { |
697 var inst = row._templateInstance; | 734 var inst = row._templateInstance; |
698 if (dot >= 0) { | 735 if (dot >= 0) { |
699 path = this.as + '.' + path.substring(dot+1); | 736 path = this.as + '.' + path.substring(dot+1); |
700 inst.notifyPath(path, value, true); | 737 inst.notifyPath(path, value, true); |
701 } else { | 738 } else { |
702 inst[this.as] = value; | 739 inst[this.as] = value; |
703 } | 740 } |
704 } | 741 } |
705 } | 742 } |
706 }, | 743 }, |
707 | 744 |
708 /** | 745 /** |
709 * Called when the items have changed. That is, ressignments | 746 * Called when the items have changed. That is, ressignments |
710 * to `items`, splices or updates to a single item. | 747 * to `items`, splices or updates to a single item. |
711 */ | 748 */ |
712 _itemsChanged: function(change) { | 749 _itemsChanged: function(change) { |
713 if (change.path === 'items') { | 750 if (change.path === 'items') { |
751 | |
752 this._restoreFocusedItem(); | |
714 // render the new set | 753 // render the new set |
715 this._itemsRendered = false; | 754 this._itemsRendered = false; |
716 | |
717 // update the whole set | 755 // update the whole set |
718 this._virtualStartVal = 0; | 756 this._virtualStart = 0; |
719 this._physicalTop = 0; | 757 this._physicalTop = 0; |
720 this._virtualCount = this.items ? this.items.length : 0; | 758 this._virtualCount = this.items ? this.items.length : 0; |
759 this._focusedIndex = 0; | |
721 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l; | 760 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l; |
722 this._physicalIndexForKey = {}; | 761 this._physicalIndexForKey = {}; |
723 | 762 |
724 // scroll to the top | |
725 this._resetScrollPosition(0); | 763 this._resetScrollPosition(0); |
726 | 764 |
727 // create the initial physical items | 765 // create the initial physical items |
728 if (!this._physicalItems) { | 766 if (!this._physicalItems) { |
729 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount)); | 767 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount)); |
730 this._physicalItems = this._createPool(this._physicalCount); | 768 this._physicalItems = this._createPool(this._physicalCount); |
731 this._physicalSizes = new Array(this._physicalCount); | 769 this._physicalSizes = new Array(this._physicalCount); |
732 } | 770 } |
733 | 771 this._debounceTemplate(this._render); |
734 this.debounce('refresh', this._render); | |
735 | 772 |
736 } else if (change.path === 'items.splices') { | 773 } else if (change.path === 'items.splices') { |
737 // render the new set | 774 // render the new set |
738 this._itemsRendered = false; | 775 this._itemsRendered = false; |
739 | |
740 this._adjustVirtualIndex(change.value.indexSplices); | 776 this._adjustVirtualIndex(change.value.indexSplices); |
741 this._virtualCount = this.items ? this.items.length : 0; | 777 this._virtualCount = this.items ? this.items.length : 0; |
742 | 778 |
743 this.debounce('refresh', this._render); | 779 this._debounceTemplate(this._render); |
780 | |
781 if (this._focusedIndex < 0 || this._focusedIndex >= this._virtualCount) { | |
782 this._focusedIndex = 0; | |
783 } | |
784 this._debounceTemplate(this._render); | |
744 | 785 |
745 } else { | 786 } else { |
746 // update a single item | 787 // update a single item |
747 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value); | 788 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value); |
748 } | 789 } |
749 }, | 790 }, |
750 | 791 |
751 /** | 792 /** |
752 * @param {!Array<!PolymerSplice>} splices | 793 * @param {!Array<!PolymerSplice>} splices |
753 */ | 794 */ |
754 _adjustVirtualIndex: function(splices) { | 795 _adjustVirtualIndex: function(splices) { |
755 var i, splice, idx; | 796 var i, splice, idx; |
756 | 797 |
757 for (i = 0; i < splices.length; i++) { | 798 for (i = 0; i < splices.length; i++) { |
758 splice = splices[i]; | 799 splice = splices[i]; |
759 | 800 |
760 // deselect removed items | 801 // deselect removed items |
761 splice.removed.forEach(this.$.selector.deselect, this.$.selector); | 802 splice.removed.forEach(this.$.selector.deselect, this.$.selector); |
762 | 803 |
763 idx = splice.index; | 804 idx = splice.index; |
764 // We only need to care about changes happening above the current positi on | 805 // We only need to care about changes happening above the current positi on |
765 if (idx >= this._virtualStartVal) { | 806 if (idx >= this._virtualStart) { |
766 break; | 807 break; |
767 } | 808 } |
768 | 809 |
769 this._virtualStart = this._virtualStart + | 810 this._virtualStart = this._virtualStart + |
770 Math.max(splice.addedCount - splice.removed.length, idx - this._virt ualStartVal); | 811 Math.max(splice.addedCount - splice.removed.length, idx - this._virt ualStart); |
771 } | 812 } |
772 }, | 813 }, |
773 | 814 |
774 _scrollHandler: function() { | |
775 this._refresh(); | |
776 }, | |
777 | |
778 /** | 815 /** |
779 * Executes a provided function per every physical index in `itemSet` | 816 * Executes a provided function per every physical index in `itemSet` |
780 * `itemSet` default value is equivalent to the entire set of physical index es. | 817 * `itemSet` default value is equivalent to the entire set of physical index es. |
781 * | 818 * |
782 * @param {!function(number, number)} fn | 819 * @param {!function(number, number)} fn |
783 * @param {!Array<number>=} itemSet | 820 * @param {!Array<number>=} itemSet |
784 */ | 821 */ |
785 _iterateItems: function(fn, itemSet) { | 822 _iterateItems: function(fn, itemSet) { |
786 var pidx, vidx, rtn, i; | 823 var pidx, vidx, rtn, i; |
787 | 824 |
788 if (arguments.length === 2 && itemSet) { | 825 if (arguments.length === 2 && itemSet) { |
789 for (i = 0; i < itemSet.length; i++) { | 826 for (i = 0; i < itemSet.length; i++) { |
790 pidx = itemSet[i]; | 827 pidx = itemSet[i]; |
791 if (pidx >= this._physicalStart) { | 828 if (pidx >= this._physicalStart) { |
792 vidx = this._virtualStartVal + (pidx - this._physicalStart); | 829 vidx = this._virtualStart + (pidx - this._physicalStart); |
793 } else { | 830 } else { |
794 vidx = this._virtualStartVal + (this._physicalCount - this._physical Start) + pidx; | 831 vidx = this._virtualStart + (this._physicalCount - this._physicalSta rt) + pidx; |
795 } | 832 } |
796 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 833 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
797 return rtn; | 834 return rtn; |
798 } | 835 } |
799 } | 836 } |
800 } else { | 837 } else { |
801 pidx = this._physicalStart; | 838 pidx = this._physicalStart; |
802 vidx = this._virtualStartVal; | 839 vidx = this._virtualStart; |
803 | 840 |
804 for (; pidx < this._physicalCount; pidx++, vidx++) { | 841 for (; pidx < this._physicalCount; pidx++, vidx++) { |
805 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 842 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
806 return rtn; | 843 return rtn; |
807 } | 844 } |
808 } | 845 } |
809 | 846 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
810 pidx = 0; | |
811 | |
812 for (; pidx < this._physicalStart; pidx++, vidx++) { | |
813 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 847 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
814 return rtn; | 848 return rtn; |
815 } | 849 } |
816 } | 850 } |
817 } | 851 } |
818 }, | 852 }, |
819 | 853 |
820 /** | 854 /** |
821 * Assigns the data models to a given set of items. | 855 * Assigns the data models to a given set of items. |
822 * @param {!Array<number>=} itemSet | 856 * @param {!Array<number>=} itemSet |
823 */ | 857 */ |
824 _assignModels: function(itemSet) { | 858 _assignModels: function(itemSet) { |
825 this._iterateItems(function(pidx, vidx) { | 859 this._iterateItems(function(pidx, vidx) { |
826 var el = this._physicalItems[pidx]; | 860 var el = this._physicalItems[pidx]; |
827 var inst = el._templateInstance; | 861 var inst = el._templateInstance; |
828 var item = this.items && this.items[vidx]; | 862 var item = this.items && this.items[vidx]; |
829 | 863 |
830 if (item) { | 864 if (item !== undefined && item !== null) { |
831 inst[this.as] = item; | 865 inst[this.as] = item; |
832 inst.__key__ = this._collection.getKey(item); | 866 inst.__key__ = this._collection.getKey(item); |
833 inst[this.selectedAs] = | 867 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s elector).isSelected(item); |
834 /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(it em); | |
835 inst[this.indexAs] = vidx; | 868 inst[this.indexAs] = vidx; |
869 inst.tabIndex = vidx === this._focusedIndex ? 0 : -1; | |
836 el.removeAttribute('hidden'); | 870 el.removeAttribute('hidden'); |
837 this._physicalIndexForKey[inst.__key__] = pidx; | 871 this._physicalIndexForKey[inst.__key__] = pidx; |
838 } else { | 872 } else { |
839 inst.__key__ = null; | 873 inst.__key__ = null; |
840 el.setAttribute('hidden', ''); | 874 el.setAttribute('hidden', ''); |
841 } | 875 } |
842 | 876 |
843 }, itemSet); | 877 }, itemSet); |
844 }, | 878 }, |
845 | 879 |
846 /** | 880 /** |
847 * Updates the height for a given set of items. | 881 * Updates the height for a given set of items. |
848 * | 882 * |
849 * @param {!Array<number>=} itemSet | 883 * @param {!Array<number>=} itemSet |
850 */ | 884 */ |
851 _updateMetrics: function(itemSet) { | 885 _updateMetrics: function(itemSet) { |
886 // Make sure we distributed all the physical items | |
887 // so we can measure them | |
888 Polymer.dom.flush(); | |
889 | |
852 var newPhysicalSize = 0; | 890 var newPhysicalSize = 0; |
853 var oldPhysicalSize = 0; | 891 var oldPhysicalSize = 0; |
854 var prevAvgCount = this._physicalAverageCount; | 892 var prevAvgCount = this._physicalAverageCount; |
855 var prevPhysicalAvg = this._physicalAverage; | 893 var prevPhysicalAvg = this._physicalAverage; |
856 // Make sure we distributed all the physical items | |
857 // so we can measure them | |
858 Polymer.dom.flush(); | |
859 | 894 |
860 this._iterateItems(function(pidx, vidx) { | 895 this._iterateItems(function(pidx, vidx) { |
896 | |
861 oldPhysicalSize += this._physicalSizes[pidx] || 0; | 897 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
862 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | 898 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
863 newPhysicalSize += this._physicalSizes[pidx]; | 899 newPhysicalSize += this._physicalSizes[pidx]; |
864 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | 900 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
901 | |
865 }, itemSet); | 902 }, itemSet); |
866 | 903 |
867 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSiz e; | 904 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSiz e; |
868 this._viewportSize = this._scroller.offsetHeight; | 905 this._viewportSize = this._scrollTargetHeight; |
869 | 906 |
870 // update the average if we measured something | 907 // update the average if we measured something |
871 if (this._physicalAverageCount !== prevAvgCount) { | 908 if (this._physicalAverageCount !== prevAvgCount) { |
872 this._physicalAverage = Math.round( | 909 this._physicalAverage = Math.round( |
873 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | 910 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / |
874 this._physicalAverageCount); | 911 this._physicalAverageCount); |
875 } | 912 } |
876 }, | 913 }, |
877 | 914 |
878 /** | 915 /** |
879 * Updates the position of the physical items. | 916 * Updates the position of the physical items. |
880 */ | 917 */ |
881 _positionItems: function() { | 918 _positionItems: function() { |
882 this._adjustScrollPosition(); | 919 this._adjustScrollPosition(); |
883 | 920 |
884 var y = this._physicalTop; | 921 var y = this._physicalTop; |
885 | 922 |
886 this._iterateItems(function(pidx) { | 923 this._iterateItems(function(pidx) { |
887 | 924 |
888 this.transform('translate3d(0, ' + y + 'px, 0)', this._physicalItems[pid x]); | 925 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
889 y += this._physicalSizes[pidx]; | 926 y += this._physicalSizes[pidx]; |
890 | 927 |
891 }); | 928 }); |
892 }, | 929 }, |
893 | 930 |
894 /** | 931 /** |
895 * Adjusts the scroll position when it was overestimated. | 932 * Adjusts the scroll position when it was overestimated. |
896 */ | 933 */ |
897 _adjustScrollPosition: function() { | 934 _adjustScrollPosition: function() { |
898 var deltaHeight = this._virtualStartVal === 0 ? this._physicalTop : | 935 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : |
899 Math.min(this._scrollPosition + this._physicalTop, 0); | 936 Math.min(this._scrollPosition + this._physicalTop, 0); |
900 | 937 |
901 if (deltaHeight) { | 938 if (deltaHeight) { |
902 this._physicalTop = this._physicalTop - deltaHeight; | 939 this._physicalTop = this._physicalTop - deltaHeight; |
903 | |
904 // juking scroll position during interial scrolling on iOS is no bueno | 940 // juking scroll position during interial scrolling on iOS is no bueno |
905 if (!IOS_TOUCH_SCROLLING) { | 941 if (!IOS_TOUCH_SCROLLING) { |
906 this._resetScrollPosition(this._scroller.scrollTop - deltaHeight); | 942 this._resetScrollPosition(this._scrollTop - deltaHeight); |
907 } | 943 } |
908 } | 944 } |
909 }, | 945 }, |
910 | 946 |
911 /** | 947 /** |
912 * Sets the position of the scroll. | 948 * Sets the position of the scroll. |
913 */ | 949 */ |
914 _resetScrollPosition: function(pos) { | 950 _resetScrollPosition: function(pos) { |
915 if (this._scroller) { | 951 if (this.scrollTarget) { |
916 this._scroller.scrollTop = pos; | 952 this._scrollTop = pos; |
917 this._scrollPosition = this._scroller.scrollTop; | 953 this._scrollPosition = this._scrollTop; |
918 } | 954 } |
919 }, | 955 }, |
920 | 956 |
921 /** | 957 /** |
922 * Sets the scroll height, that's the height of the content, | 958 * Sets the scroll height, that's the height of the content, |
923 * | 959 * |
924 * @param {boolean=} forceUpdate If true, updates the height no matter what. | 960 * @param {boolean=} forceUpdate If true, updates the height no matter what. |
925 */ | 961 */ |
926 _updateScrollerSize: function(forceUpdate) { | 962 _updateScrollerSize: function(forceUpdate) { |
927 this._estScrollHeight = (this._physicalBottom + | 963 this._estScrollHeight = (this._physicalBottom + |
928 Math.max(this._virtualCount - this._physicalCount - this._virtualStart Val, 0) * this._physicalAverage); | 964 Math.max(this._virtualCount - this._physicalCount - this._virtualStart , 0) * this._physicalAverage); |
929 | 965 |
930 forceUpdate = forceUpdate || this._scrollHeight === 0; | 966 forceUpdate = forceUpdate || this._scrollHeight === 0; |
931 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize; | 967 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize; |
932 | 968 |
933 // amortize height adjustment, so it won't trigger repaints very often | 969 // amortize height adjustment, so it won't trigger repaints very often |
934 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) { | 970 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) { |
935 this.$.items.style.height = this._estScrollHeight + 'px'; | 971 this.$.items.style.height = this._estScrollHeight + 'px'; |
936 this._scrollHeight = this._estScrollHeight; | 972 this._scrollHeight = this._estScrollHeight; |
937 } | 973 } |
938 }, | 974 }, |
939 | 975 |
940 /** | 976 /** |
941 * Scroll to a specific item in the virtual list regardless | 977 * Scroll to a specific item in the virtual list regardless |
942 * of the physical items in the DOM tree. | 978 * of the physical items in the DOM tree. |
943 * | 979 * |
944 * @method scrollToIndex | 980 * @method scrollToIndex |
945 * @param {number} idx The index of the item | 981 * @param {number} idx The index of the item |
946 */ | 982 */ |
947 scrollToIndex: function(idx) { | 983 scrollToIndex: function(idx) { |
948 if (typeof idx !== 'number') { | 984 if (typeof idx !== 'number') { |
949 return; | 985 return; |
950 } | 986 } |
951 | 987 |
988 Polymer.dom.flush(); | |
989 | |
952 var firstVisible = this.firstVisibleIndex; | 990 var firstVisible = this.firstVisibleIndex; |
953 | |
954 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | 991 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); |
955 | 992 |
956 // start at the previous virtual item | 993 // start at the previous virtual item |
957 // so we have a item above the first visible item | 994 // so we have a item above the first visible item |
958 this._virtualStart = idx - 1; | 995 this._virtualStart = idx - 1; |
959 | |
960 // assign new models | 996 // assign new models |
961 this._assignModels(); | 997 this._assignModels(); |
962 | |
963 // measure the new sizes | 998 // measure the new sizes |
964 this._updateMetrics(); | 999 this._updateMetrics(); |
965 | |
966 // estimate new physical offset | 1000 // estimate new physical offset |
967 this._physicalTop = this._virtualStart * this._physicalAverage; | 1001 this._physicalTop = this._virtualStart * this._physicalAverage; |
968 | 1002 |
969 var currentTopItem = this._physicalStart; | 1003 var currentTopItem = this._physicalStart; |
970 var currentVirtualItem = this._virtualStart; | 1004 var currentVirtualItem = this._virtualStart; |
971 var targetOffsetTop = 0; | 1005 var targetOffsetTop = 0; |
972 var hiddenContentSize = this._hiddenContentSize; | 1006 var hiddenContentSize = this._hiddenContentSize; |
973 | 1007 |
974 // scroll to the item as much as we can | 1008 // scroll to the item as much as we can |
975 while (currentVirtualItem < idx && targetOffsetTop < hiddenContentSize) { | 1009 while (currentVirtualItem < idx && targetOffsetTop < hiddenContentSize) { |
976 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem]; | 1010 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem]; |
977 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 1011 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
978 currentVirtualItem++; | 1012 currentVirtualItem++; |
979 } | 1013 } |
980 | |
981 // update the scroller size | 1014 // update the scroller size |
982 this._updateScrollerSize(true); | 1015 this._updateScrollerSize(true); |
983 | |
984 // update the position of the items | 1016 // update the position of the items |
985 this._positionItems(); | 1017 this._positionItems(); |
986 | |
987 // set the new scroll position | 1018 // set the new scroll position |
988 this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1); | 1019 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t argetOffsetTop + 1); |
989 | |
990 // increase the pool of physical items if needed | 1020 // increase the pool of physical items if needed |
991 this._increasePoolIfNeeded(); | 1021 this._increasePoolIfNeeded(); |
992 | |
993 // clear cached visible index | 1022 // clear cached visible index |
994 this._firstVisibleIndexVal = null; | 1023 this._firstVisibleIndexVal = null; |
1024 this._lastVisibleIndexVal = null; | |
995 }, | 1025 }, |
996 | 1026 |
997 /** | 1027 /** |
998 * Reset the physical average and the average count. | 1028 * Reset the physical average and the average count. |
999 */ | 1029 */ |
1000 _resetAverage: function() { | 1030 _resetAverage: function() { |
1001 this._physicalAverage = 0; | 1031 this._physicalAverage = 0; |
1002 this._physicalAverageCount = 0; | 1032 this._physicalAverageCount = 0; |
1003 }, | 1033 }, |
1004 | 1034 |
1005 /** | 1035 /** |
1006 * A handler for the `iron-resize` event triggered by `IronResizableBehavior ` | 1036 * A handler for the `iron-resize` event triggered by `IronResizableBehavior ` |
1007 * when the element is resized. | 1037 * when the element is resized. |
1008 */ | 1038 */ |
1009 _resizeHandler: function() { | 1039 _resizeHandler: function() { |
1010 this.debounce('resize', function() { | 1040 // iOS fires the resize event when the address bar slides up |
1041 if (IOS && Math.abs(this._viewportSize - this._scrollTargetHeight) < 100) { | |
1042 return; | |
1043 } | |
1044 this._debounceTemplate(function() { | |
1011 this._render(); | 1045 this._render(); |
1012 if (this._itemsRendered && this._physicalItems && this._isVisible) { | 1046 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
1013 this._resetAverage(); | 1047 this._resetAverage(); |
1014 this.updateViewportBoundaries(); | 1048 this.updateViewportBoundaries(); |
1015 this.scrollToIndex(this.firstVisibleIndex); | 1049 this.scrollToIndex(this.firstVisibleIndex); |
1016 } | 1050 } |
1017 }); | 1051 }); |
1018 }, | 1052 }, |
1019 | 1053 |
1020 _getModelFromItem: function(item) { | 1054 _getModelFromItem: function(item) { |
1021 var key = this._collection.getKey(item); | 1055 var key = this._collection.getKey(item); |
1022 var pidx = this._physicalIndexForKey[key]; | 1056 var pidx = this._physicalIndexForKey[key]; |
1023 | 1057 |
1024 if (pidx !== undefined) { | 1058 if (pidx !== undefined) { |
1025 return this._physicalItems[pidx]._templateInstance; | 1059 return this._physicalItems[pidx]._templateInstance; |
1026 } | 1060 } |
1027 return null; | 1061 return null; |
1028 }, | 1062 }, |
1029 | 1063 |
1030 /** | 1064 /** |
1031 * Gets a valid item instance from its index or the object value. | 1065 * Gets a valid item instance from its index or the object value. |
1032 * | 1066 * |
1033 * @param {(Object|number)} item The item object or its index | 1067 * @param {(Object|number)} item The item object or its index |
1034 */ | 1068 */ |
1035 _getNormalizedItem: function(item) { | 1069 _getNormalizedItem: function(item) { |
1036 if (typeof item === 'number') { | 1070 if (this._collection.getKey(item) === undefined) { |
1037 item = this.items[item]; | 1071 if (typeof item === 'number') { |
1038 if (!item) { | 1072 item = this.items[item]; |
1039 throw new RangeError('<item> not found'); | 1073 if (!item) { |
1074 throw new RangeError('<item> not found'); | |
1075 } | |
1076 return item; | |
1040 } | 1077 } |
1041 } else if (this._collection.getKey(item) === undefined) { | |
1042 throw new TypeError('<item> should be a valid item'); | 1078 throw new TypeError('<item> should be a valid item'); |
1043 } | 1079 } |
1044 return item; | 1080 return item; |
1045 }, | 1081 }, |
1046 | 1082 |
1047 /** | 1083 /** |
1048 * Select the list item at the given index. | 1084 * Select the list item at the given index. |
1049 * | 1085 * |
1050 * @method selectItem | 1086 * @method selectItem |
1051 * @param {(Object|number)} item The item object or its index | 1087 * @param {(Object|number)} item The item object or its index |
1052 */ | 1088 */ |
1053 selectItem: function(item) { | 1089 selectItem: function(item) { |
1054 item = this._getNormalizedItem(item); | 1090 item = this._getNormalizedItem(item); |
1055 var model = this._getModelFromItem(item); | 1091 var model = this._getModelFromItem(item); |
1056 | 1092 |
1057 if (!this.multiSelection && this.selectedItem) { | 1093 if (!this.multiSelection && this.selectedItem) { |
1058 this.deselectItem(this.selectedItem); | 1094 this.deselectItem(this.selectedItem); |
1059 } | 1095 } |
1060 if (model) { | 1096 if (model) { |
1061 model[this.selectedAs] = true; | 1097 model[this.selectedAs] = true; |
1062 } | 1098 } |
1063 this.$.selector.select(item); | 1099 this.$.selector.select(item); |
1100 this.updateSizeForItem(item); | |
1064 }, | 1101 }, |
1065 | 1102 |
1066 /** | 1103 /** |
1067 * Deselects the given item list if it is already selected. | 1104 * Deselects the given item list if it is already selected. |
1068 * | 1105 * |
1069 | 1106 |
1070 * @method deselect | 1107 * @method deselect |
1071 * @param {(Object|number)} item The item object or its index | 1108 * @param {(Object|number)} item The item object or its index |
1072 */ | 1109 */ |
1073 deselectItem: function(item) { | 1110 deselectItem: function(item) { |
1074 item = this._getNormalizedItem(item); | 1111 item = this._getNormalizedItem(item); |
1075 var model = this._getModelFromItem(item); | 1112 var model = this._getModelFromItem(item); |
1076 | 1113 |
1077 if (model) { | 1114 if (model) { |
1078 model[this.selectedAs] = false; | 1115 model[this.selectedAs] = false; |
1079 } | 1116 } |
1080 this.$.selector.deselect(item); | 1117 this.$.selector.deselect(item); |
1118 this.updateSizeForItem(item); | |
1081 }, | 1119 }, |
1082 | 1120 |
1083 /** | 1121 /** |
1084 * Select or deselect a given item depending on whether the item | 1122 * Select or deselect a given item depending on whether the item |
1085 * has already been selected. | 1123 * has already been selected. |
1086 * | 1124 * |
1087 * @method toggleSelectionForItem | 1125 * @method toggleSelectionForItem |
1088 * @param {(Object|number)} item The item object or its index | 1126 * @param {(Object|number)} item The item object or its index |
1089 */ | 1127 */ |
1090 toggleSelectionForItem: function(item) { | 1128 toggleSelectionForItem: function(item) { |
(...skipping 25 matching lines...) Expand all Loading... | |
1116 } | 1154 } |
1117 | 1155 |
1118 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | 1156 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); |
1119 }, | 1157 }, |
1120 | 1158 |
1121 /** | 1159 /** |
1122 * Add an event listener to `tap` if `selectionEnabled` is true, | 1160 * Add an event listener to `tap` if `selectionEnabled` is true, |
1123 * it will remove the listener otherwise. | 1161 * it will remove the listener otherwise. |
1124 */ | 1162 */ |
1125 _selectionEnabledChanged: function(selectionEnabled) { | 1163 _selectionEnabledChanged: function(selectionEnabled) { |
1126 if (selectionEnabled) { | 1164 var handler = selectionEnabled ? this.listen : this.unlisten; |
1127 this.listen(this, 'tap', '_selectionHandler'); | 1165 handler.call(this, this, 'tap', '_selectionHandler'); |
1128 this.listen(this, 'keypress', '_selectionHandler'); | |
1129 } else { | |
1130 this.unlisten(this, 'tap', '_selectionHandler'); | |
1131 this.unlisten(this, 'keypress', '_selectionHandler'); | |
1132 } | |
1133 }, | 1166 }, |
1134 | 1167 |
1135 /** | 1168 /** |
1136 * Select an item from an event object. | 1169 * Select an item from an event object. |
1137 */ | 1170 */ |
1138 _selectionHandler: function(e) { | 1171 _selectionHandler: function(e) { |
1139 if (e.type !== 'keypress' || e.keyCode === 13) { | 1172 if (this.selectionEnabled) { |
1140 var model = this.modelForElement(e.target); | 1173 var model = this.modelForElement(e.target); |
1141 if (model) { | 1174 if (model) { |
1142 this.toggleSelectionForItem(model[this.as]); | 1175 this.toggleSelectionForItem(model[this.as]); |
1143 } | 1176 } |
1144 } | 1177 } |
1145 }, | 1178 }, |
1146 | 1179 |
1147 _multiSelectionChanged: function(multiSelection) { | 1180 _multiSelectionChanged: function(multiSelection) { |
1148 this.clearSelection(); | 1181 this.clearSelection(); |
1149 this.$.selector.multi = multiSelection; | 1182 this.$.selector.multi = multiSelection; |
1150 }, | 1183 }, |
1151 | 1184 |
1152 /** | 1185 /** |
1153 * Updates the size of an item. | 1186 * Updates the size of an item. |
1154 * | 1187 * |
1155 * @method updateSizeForItem | 1188 * @method updateSizeForItem |
1156 * @param {(Object|number)} item The item object or its index | 1189 * @param {(Object|number)} item The item object or its index |
1157 */ | 1190 */ |
1158 updateSizeForItem: function(item) { | 1191 updateSizeForItem: function(item) { |
1159 item = this._getNormalizedItem(item); | 1192 item = this._getNormalizedItem(item); |
1160 var key = this._collection.getKey(item); | 1193 var key = this._collection.getKey(item); |
1161 var pidx = this._physicalIndexForKey[key]; | 1194 var pidx = this._physicalIndexForKey[key]; |
1162 | 1195 |
1163 if (pidx !== undefined) { | 1196 if (pidx !== undefined) { |
1164 this._updateMetrics([pidx]); | 1197 this._updateMetrics([pidx]); |
1165 this._positionItems(); | 1198 this._positionItems(); |
1166 } | 1199 } |
1200 }, | |
1201 | |
1202 _isIndexRendered: function(idx) { | |
1203 return idx >= this._virtualStart && idx <= this._virtualEnd; | |
1204 }, | |
1205 | |
1206 _getPhysicalItemForIndex: function(idx, force) { | |
1207 if (!this._collection) { | |
1208 return null; | |
1209 } | |
1210 if (!this._isIndexRendered(idx)) { | |
1211 if (force) { | |
1212 this.scrollToIndex(idx); | |
1213 return this._getPhysicalItemForIndex(idx, false); | |
1214 } | |
1215 return null; | |
1216 } | |
1217 var item = this._getNormalizedItem(idx); | |
1218 var physicalItem = this._physicalItems[this._physicalIndexForKey[this._col lection.getKey(item)]]; | |
1219 | |
1220 return physicalItem || null; | |
1221 }, | |
1222 | |
1223 _focusPhysicalItem: function(idx) { | |
1224 this._restoreFocusedItem(); | |
1225 | |
1226 var physicalItem = this._getPhysicalItemForIndex(idx, true); | |
1227 if (!physicalItem) { | |
1228 return; | |
1229 } | |
1230 var SECRET = ~(Math.random() * 100); | |
1231 var model = physicalItem._templateInstance; | |
1232 var focusable; | |
1233 | |
1234 model.tabIndex = SECRET; | |
1235 // the focusable element could be the entire physical item | |
1236 if (physicalItem.tabIndex === SECRET) { | |
1237 focusable = physicalItem; | |
1238 } | |
1239 // the focusable element could be somewhere within the physical item | |
1240 if (!focusable) { | |
1241 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR ET + '"]'); | |
1242 } | |
1243 // restore the tab index | |
1244 model.tabIndex = 0; | |
1245 focusable && focusable.focus(); | |
1246 }, | |
1247 | |
1248 _restoreFocusedItem: function() { | |
1249 if (!this._offscreenFocusedItem) { | |
1250 return; | |
1251 } | |
1252 var item = this._getNormalizedItem(this._focusedIndex); | |
1253 var pidx = this._physicalIndexForKey[this._collection.getKey(item)]; | |
1254 | |
1255 if (pidx !== undefined) { | |
1256 this.translate3d(0, HIDDEN_Y, 0, this._physicalItems[pidx]); | |
1257 this._physicalItems[pidx] = this._offscreenFocusedItem; | |
1258 } | |
1259 this._offscreenFocusedItem = null; | |
1260 }, | |
1261 | |
1262 _removeFocusedItem: function() { | |
1263 if (!this._offscreenFocusedItem) { | |
1264 return; | |
1265 } | |
1266 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | |
1267 this._offscreenFocusedItem = null; | |
1268 this._focusBackfillItem = null; | |
1269 }, | |
1270 | |
1271 _createFocusBackfillItem: function() { | |
1272 if (this._offscreenFocusedItem) { | |
1273 return; | |
1274 } | |
1275 var item = this._getNormalizedItem(this._focusedIndex); | |
1276 var pidx = this._physicalIndexForKey[this._collection.getKey(item)]; | |
1277 | |
1278 this._offscreenFocusedItem = this._physicalItems[pidx]; | |
1279 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | |
1280 | |
1281 if (!this._focusBackfillItem) { | |
1282 var stampedTemplate = this.stamp(null); | |
1283 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | |
1284 Polymer.dom(this).appendChild(stampedTemplate.root); | |
1285 } | |
1286 this._physicalItems[pidx] = this._focusBackfillItem; | |
1287 }, | |
1288 | |
1289 _didFocus: function(e) { | |
1290 var targetModel = this.modelForElement(e.target); | |
1291 var fidx = this._focusedIndex; | |
1292 | |
1293 if (!targetModel) { | |
1294 return; | |
1295 } | |
1296 this._restoreFocusedItem(); | |
1297 | |
1298 if (this.modelForElement(this._offscreenFocusedItem) === targetModel) { | |
1299 this.scrollToIndex(fidx); | |
1300 } else { | |
1301 // restore tabIndex for the currently focused item | |
1302 this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1; | |
1303 // set the tabIndex for the next focused item | |
1304 targetModel.tabIndex = 0; | |
1305 fidx = /** @type {{index: number}} */(targetModel).index; | |
1306 this._focusedIndex = fidx; | |
1307 // bring the item into view | |
1308 if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) { | |
1309 this.scrollToIndex(fidx); | |
1310 } else { | |
1311 this._update(); | |
1312 } | |
1313 } | |
1314 }, | |
1315 | |
1316 _didMoveUp: function() { | |
1317 this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1)); | |
1318 }, | |
1319 | |
1320 _didMoveDown: function() { | |
1321 this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex + 1)); | |
1322 }, | |
1323 | |
1324 _didEnter: function(e) { | |
1325 // focus the currently focused physical item | |
1326 this._focusPhysicalItem(this._focusedIndex); | |
1327 // toggle selection | |
1328 this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).key boardEvent); | |
Dan Beam
2016/02/09 04:27:59
this is a local change, but was mentioned on the o
| |
1167 } | 1329 } |
1168 }); | 1330 }); |
1169 | 1331 |
1170 })(); | 1332 })(); |
OLD | NEW |