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 var HIDDEN_Y = '-10000px'; |
8 | 8 |
9 Polymer({ | 9 Polymer({ |
10 | 10 |
(...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
111 'enter': '_didEnter' | 111 'enter': '_didEnter' |
112 }, | 112 }, |
113 | 113 |
114 /** | 114 /** |
115 * 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. |
116 * 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. |
117 */ | 117 */ |
118 _ratio: 0.5, | 118 _ratio: 0.5, |
119 | 119 |
120 /** | 120 /** |
121 * The padding-top value of the `scroller` element | 121 * The padding-top value for the list. |
122 */ | 122 */ |
123 _scrollerPaddingTop: 0, | 123 _scrollerPaddingTop: 0, |
124 | 124 |
125 /** | 125 /** |
126 * This value is the same as `scrollTop`. | 126 * This value is the same as `scrollTop`. |
127 */ | 127 */ |
128 _scrollPosition: 0, | 128 _scrollPosition: 0, |
129 | 129 |
130 /** | 130 /** |
131 * The number of tiles in the DOM. | |
132 */ | |
133 _physicalCount: 0, | |
134 | |
135 /** | |
136 * The k-th tile that is at the top of the scrolling list. | |
137 */ | |
138 _physicalStart: 0, | |
139 | |
140 /** | |
141 * The k-th tile that is at the bottom of the scrolling list. | |
142 */ | |
143 _physicalEnd: 0, | |
144 | |
145 /** | |
146 * The sum of the heights of all the tiles in the DOM. | 131 * The sum of the heights of all the tiles in the DOM. |
147 */ | 132 */ |
148 _physicalSize: 0, | 133 _physicalSize: 0, |
149 | 134 |
150 /** | 135 /** |
151 * The average `F` of the tiles observed till now. | 136 * The average `F` of the tiles observed till now. |
152 */ | 137 */ |
153 _physicalAverage: 0, | 138 _physicalAverage: 0, |
154 | 139 |
155 /** | 140 /** |
156 * The number of tiles which `offsetHeight` > 0 observed until now. | 141 * The number of tiles which `offsetHeight` > 0 observed until now. |
157 */ | 142 */ |
158 _physicalAverageCount: 0, | 143 _physicalAverageCount: 0, |
159 | 144 |
160 /** | 145 /** |
161 * The Y position of the item rendered in the `_physicalStart` | 146 * The Y position of the item rendered in the `_physicalStart` |
162 * tile relative to the scrolling list. | 147 * tile relative to the scrolling list. |
163 */ | 148 */ |
164 _physicalTop: 0, | 149 _physicalTop: 0, |
165 | 150 |
166 /** | 151 /** |
167 * The number of items in the list. | 152 * The number of items in the list. |
168 */ | 153 */ |
169 _virtualCount: 0, | 154 _virtualCount: 0, |
170 | 155 |
171 /** | 156 /** |
172 * The n-th item rendered in the `_physicalStart` tile. | |
173 */ | |
174 _virtualStartVal: 0, | |
175 | |
176 /** | |
177 * A map between an item key and its physical item index | 157 * A map between an item key and its physical item index |
178 */ | 158 */ |
179 _physicalIndexForKey: null, | 159 _physicalIndexForKey: null, |
180 | 160 |
181 /** | 161 /** |
182 * The estimated scroll height based on `_physicalAverage` | 162 * The estimated scroll height based on `_physicalAverage` |
183 */ | 163 */ |
184 _estScrollHeight: 0, | 164 _estScrollHeight: 0, |
185 | 165 |
186 /** | 166 /** |
(...skipping 25 matching lines...) Expand all Loading... |
212 */ | 192 */ |
213 _firstVisibleIndexVal: null, | 193 _firstVisibleIndexVal: null, |
214 | 194 |
215 /** | 195 /** |
216 * A cached value for the last visible index. | 196 * A cached value for the last visible index. |
217 * See `lastVisibleIndex` | 197 * See `lastVisibleIndex` |
218 * @type {?number} | 198 * @type {?number} |
219 */ | 199 */ |
220 _lastVisibleIndexVal: null, | 200 _lastVisibleIndexVal: null, |
221 | 201 |
222 | |
223 /** | 202 /** |
224 * A Polymer collection for the items. | 203 * A Polymer collection for the items. |
225 * @type {?Polymer.Collection} | 204 * @type {?Polymer.Collection} |
226 */ | 205 */ |
227 _collection: null, | 206 _collection: null, |
228 | 207 |
229 /** | 208 /** |
230 * True if the current item list was rendered for the first time | 209 * True if the current item list was rendered for the first time |
231 * after attached. | 210 * after attached. |
232 */ | 211 */ |
233 _itemsRendered: false, | 212 _itemsRendered: false, |
234 | 213 |
235 /** | 214 /** |
236 * The page that is currently rendered. | 215 * The page that is currently rendered. |
237 */ | 216 */ |
238 _lastPage: null, | 217 _lastPage: null, |
239 | 218 |
240 /** | 219 /** |
241 * The max number of pages to render. One page is equivalent to the height o
f the list. | 220 * The max number of pages to render. One page is equivalent to the height o
f the list. |
242 */ | 221 */ |
243 _maxPages: 3, | 222 _maxPages: 3, |
244 | 223 |
245 /** | 224 /** |
246 * The currently focused item index. | 225 * The currently focused physical item. |
247 */ | 226 */ |
248 _focusedIndex: 0, | 227 _focusedItem: null, |
| 228 |
| 229 /** |
| 230 * The index of the `_focusedItem`. |
| 231 */ |
| 232 _focusedIndex: -1, |
249 | 233 |
250 /** | 234 /** |
251 * The the item that is focused if it is moved offscreen. | 235 * The the item that is focused if it is moved offscreen. |
252 * @private {?TemplatizerNode} | 236 * @private {?TemplatizerNode} |
253 */ | 237 */ |
254 _offscreenFocusedItem: null, | 238 _offscreenFocusedItem: null, |
255 | 239 |
256 /** | 240 /** |
257 * The item that backfills the `_offscreenFocusedItem` in the physical items | 241 * The item that backfills the `_offscreenFocusedItem` in the physical items |
258 * list when that item is moved offscreen. | 242 * list when that item is moved offscreen. |
(...skipping 15 matching lines...) Expand all Loading... |
274 }, | 258 }, |
275 | 259 |
276 /** | 260 /** |
277 * The n-th item rendered in the last physical item. | 261 * The n-th item rendered in the last physical item. |
278 */ | 262 */ |
279 get _virtualEnd() { | 263 get _virtualEnd() { |
280 return this._virtualStart + this._physicalCount - 1; | 264 return this._virtualStart + this._physicalCount - 1; |
281 }, | 265 }, |
282 | 266 |
283 /** | 267 /** |
| 268 * The height of the physical content that isn't on the screen. |
| 269 */ |
| 270 get _hiddenContentSize() { |
| 271 return this._physicalSize - this._viewportSize; |
| 272 }, |
| 273 |
| 274 /** |
| 275 * The maximum scroll top value. |
| 276 */ |
| 277 get _maxScrollTop() { |
| 278 return this._estScrollHeight - this._viewportSize + this._scrollerPaddingT
op; |
| 279 }, |
| 280 |
| 281 /** |
284 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | 282 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. |
285 */ | 283 */ |
286 _minVirtualStart: 0, | 284 _minVirtualStart: 0, |
287 | 285 |
288 /** | 286 /** |
289 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | 287 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. |
290 */ | 288 */ |
291 get _maxVirtualStart() { | 289 get _maxVirtualStart() { |
292 return Math.max(0, this._virtualCount - this._physicalCount); | 290 return Math.max(0, this._virtualCount - this._physicalCount); |
293 }, | 291 }, |
294 | 292 |
295 /** | 293 /** |
296 * The height of the physical content that isn't on the screen. | 294 * The n-th item rendered in the `_physicalStart` tile. |
297 */ | 295 */ |
298 get _hiddenContentSize() { | 296 _virtualStartVal: 0, |
299 return this._physicalSize - this._viewportSize; | 297 |
| 298 set _virtualStart(val) { |
| 299 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
| 300 }, |
| 301 |
| 302 get _virtualStart() { |
| 303 return this._virtualStartVal || 0; |
300 }, | 304 }, |
301 | 305 |
302 /** | 306 /** |
303 * The maximum scroll top value. | 307 * The k-th tile that is at the top of the scrolling list. |
304 */ | 308 */ |
305 get _maxScrollTop() { | 309 _physicalStartVal: 0, |
306 return this._estScrollHeight - this._viewportSize; | 310 |
| 311 set _physicalStart(val) { |
| 312 this._physicalStartVal = val % this._physicalCount; |
| 313 if (this._physicalStartVal < 0) { |
| 314 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
| 315 } |
| 316 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
| 317 }, |
| 318 |
| 319 get _physicalStart() { |
| 320 return this._physicalStartVal || 0; |
307 }, | 321 }, |
308 | 322 |
309 /** | 323 /** |
310 * Sets the n-th item rendered in `_physicalStart` | 324 * The number of tiles in the DOM. |
311 */ | 325 */ |
312 set _virtualStart(val) { | 326 _physicalCountVal: 0, |
313 // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart | 327 |
314 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | 328 set _physicalCount(val) { |
315 if (this._physicalCount === 0) { | 329 this._physicalCountVal = val; |
316 this._physicalStart = 0; | 330 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
317 this._physicalEnd = 0; | 331 }, |
318 } else { | 332 |
319 this._physicalStart = this._virtualStartVal % this._physicalCount; | 333 get _physicalCount() { |
320 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % th
is._physicalCount; | 334 return this._physicalCountVal; |
321 } | |
322 }, | 335 }, |
323 | 336 |
324 /** | 337 /** |
325 * Gets the n-th item rendered in `_physicalStart` | 338 * The k-th tile that is at the bottom of the scrolling list. |
326 */ | 339 */ |
327 get _virtualStart() { | 340 _physicalEnd: 0, |
328 return this._virtualStartVal; | |
329 }, | |
330 | 341 |
331 /** | 342 /** |
332 * An optimal physical size such that we will have enough physical items | 343 * An optimal physical size such that we will have enough physical items |
333 * to fill up the viewport and recycle when the user scrolls. | 344 * to fill up the viewport and recycle when the user scrolls. |
334 * | 345 * |
335 * This default value assumes that we will at least have the equivalent | 346 * This default value assumes that we will at least have the equivalent |
336 * to a viewport of physical items above and below the user's viewport. | 347 * to a viewport of physical items above and below the user's viewport. |
337 */ | 348 */ |
338 get _optPhysicalSize() { | 349 get _optPhysicalSize() { |
339 return this._viewportSize * this._maxPages; | 350 return this._viewportSize * this._maxPages; |
340 }, | 351 }, |
341 | 352 |
342 /** | 353 /** |
343 * True if the current list is visible. | 354 * True if the current list is visible. |
344 */ | 355 */ |
345 get _isVisible() { | 356 get _isVisible() { |
346 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | 357 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
347 }, | 358 }, |
348 | 359 |
349 /** | 360 /** |
350 * Gets the index of the first visible item in the viewport. | 361 * Gets the index of the first visible item in the viewport. |
351 * | 362 * |
352 * @type {number} | 363 * @type {number} |
353 */ | 364 */ |
354 get firstVisibleIndex() { | 365 get firstVisibleIndex() { |
355 if (this._firstVisibleIndexVal === null) { | 366 if (this._firstVisibleIndexVal === null) { |
356 var physicalOffset = this._physicalTop; | 367 var physicalOffset = this._physicalTop + this._scrollerPaddingTop; |
357 | 368 |
358 this._firstVisibleIndexVal = this._iterateItems( | 369 this._firstVisibleIndexVal = this._iterateItems( |
359 function(pidx, vidx) { | 370 function(pidx, vidx) { |
360 physicalOffset += this._physicalSizes[pidx]; | 371 physicalOffset += this._physicalSizes[pidx]; |
361 | |
362 if (physicalOffset > this._scrollPosition) { | 372 if (physicalOffset > this._scrollPosition) { |
363 return vidx; | 373 return vidx; |
364 } | 374 } |
365 }) || 0; | 375 }) || 0; |
366 } | 376 } |
367 return this._firstVisibleIndexVal; | 377 return this._firstVisibleIndexVal; |
368 }, | 378 }, |
369 | 379 |
370 /** | 380 /** |
371 * Gets the index of the last visible item in the viewport. | 381 * Gets the index of the last visible item in the viewport. |
372 * | 382 * |
373 * @type {number} | 383 * @type {number} |
374 */ | 384 */ |
375 get lastVisibleIndex() { | 385 get lastVisibleIndex() { |
376 if (this._lastVisibleIndexVal === null) { | 386 if (this._lastVisibleIndexVal === null) { |
377 var physicalOffset = this._physicalTop; | 387 var physicalOffset = this._physicalTop; |
378 | 388 |
379 this._iterateItems(function(pidx, vidx) { | 389 this._iterateItems(function(pidx, vidx) { |
380 physicalOffset += this._physicalSizes[pidx]; | 390 physicalOffset += this._physicalSizes[pidx]; |
381 | 391 |
382 if(physicalOffset <= this._scrollBottom) { | 392 if (physicalOffset <= this._scrollBottom) { |
383 this._lastVisibleIndexVal = vidx; | 393 this._lastVisibleIndexVal = vidx; |
384 } | 394 } |
385 }); | 395 }); |
386 } | 396 } |
387 return this._lastVisibleIndexVal; | 397 return this._lastVisibleIndexVal; |
388 }, | 398 }, |
389 | 399 |
| 400 get _defaultScrollTarget() { |
| 401 return this; |
| 402 }, |
| 403 |
390 ready: function() { | 404 ready: function() { |
391 this.addEventListener('focus', this._didFocus.bind(this), true); | 405 this.addEventListener('focus', this._didFocus.bind(this), true); |
392 }, | 406 }, |
393 | 407 |
394 attached: function() { | 408 attached: function() { |
395 this.updateViewportBoundaries(); | 409 this.updateViewportBoundaries(); |
396 this._render(); | 410 this._render(); |
397 }, | 411 }, |
398 | 412 |
399 detached: function() { | 413 detached: function() { |
400 this._itemsRendered = false; | 414 this._itemsRendered = false; |
401 }, | 415 }, |
402 | 416 |
403 get _defaultScrollTarget() { | |
404 return this; | |
405 }, | |
406 | |
407 /** | 417 /** |
408 * Set the overflow property if this element has its own scrolling region | 418 * Set the overflow property if this element has its own scrolling region |
409 */ | 419 */ |
410 _setOverflow: function(scrollTarget) { | 420 _setOverflow: function(scrollTarget) { |
411 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | 421 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
412 this.style.overflow = scrollTarget === this ? 'auto' : ''; | 422 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
413 }, | 423 }, |
414 | 424 |
415 /** | 425 /** |
416 * Invoke this method if you dynamically update the viewport's | 426 * Invoke this method if you dynamically update the viewport's |
417 * size or CSS padding. | 427 * size or CSS padding. |
418 * | 428 * |
419 * @method updateViewportBoundaries | 429 * @method updateViewportBoundaries |
420 */ | 430 */ |
421 updateViewportBoundaries: function() { | 431 updateViewportBoundaries: function() { |
422 var scrollerStyle = window.getComputedStyle(this.scrollTarget); | 432 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : |
423 this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10); | 433 parseInt(window.getComputedStyle(this)['padding-top'], 10); |
| 434 |
424 this._viewportSize = this._scrollTargetHeight; | 435 this._viewportSize = this._scrollTargetHeight; |
425 }, | 436 }, |
426 | 437 |
427 /** | 438 /** |
428 * Update the models, the position of the | 439 * Update the models, the position of the |
429 * items in the viewport and recycle tiles as needed. | 440 * items in the viewport and recycle tiles as needed. |
430 */ | 441 */ |
431 _scrollHandler: function() { | 442 _scrollHandler: function() { |
432 // clamp the `scrollTop` value | 443 // clamp the `scrollTop` value |
433 // IE 10|11 scrollTop may go above `_maxScrollTop` | |
434 // iOS `scrollTop` may go below 0 and above `_maxScrollTop` | |
435 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | 444 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
| 445 var delta = scrollTop - this._scrollPosition; |
436 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | 446 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
437 var ratio = this._ratio; | 447 var ratio = this._ratio; |
438 var delta = scrollTop - this._scrollPosition; | |
439 var recycledTiles = 0; | 448 var recycledTiles = 0; |
440 var hiddenContentSize = this._hiddenContentSize; | 449 var hiddenContentSize = this._hiddenContentSize; |
441 var currentRatio = ratio; | 450 var currentRatio = ratio; |
442 var movingUp = []; | 451 var movingUp = []; |
443 | 452 |
444 // track the last `scrollTop` | 453 // track the last `scrollTop` |
445 this._scrollPosition = scrollTop; | 454 this._scrollPosition = scrollTop; |
446 | 455 |
447 // clear cached visible index | 456 // clear cached visible indexes |
448 this._firstVisibleIndexVal = null; | 457 this._firstVisibleIndexVal = null; |
449 this._lastVisibleIndexVal = null; | 458 this._lastVisibleIndexVal = null; |
450 | 459 |
451 scrollBottom = this._scrollBottom; | 460 scrollBottom = this._scrollBottom; |
452 physicalBottom = this._physicalBottom; | 461 physicalBottom = this._physicalBottom; |
453 | 462 |
454 // random access | 463 // random access |
455 if (Math.abs(delta) > this._physicalSize) { | 464 if (Math.abs(delta) > this._physicalSize) { |
456 this._physicalTop += delta; | 465 this._physicalTop += delta; |
457 recycledTiles = Math.round(delta / this._physicalAverage); | 466 recycledTiles = Math.round(delta / this._physicalAverage); |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
524 | 533 |
525 if (recycledTiles === 0) { | 534 if (recycledTiles === 0) { |
526 // If the list ever reach this case, the physical average is not signifi
cant enough | 535 // If the list ever reach this case, the physical average is not signifi
cant enough |
527 // to create all the items needed to cover the entire viewport. | 536 // to create all the items needed to cover the entire viewport. |
528 // e.g. A few items have a height that differs from the average by serve
ral order of magnitude. | 537 // e.g. A few items have a height that differs from the average by serve
ral order of magnitude. |
529 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 538 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
530 this.async(this._increasePool.bind(this, 1)); | 539 this.async(this._increasePool.bind(this, 1)); |
531 } | 540 } |
532 } else { | 541 } else { |
533 this._virtualStart = this._virtualStart + recycledTiles; | 542 this._virtualStart = this._virtualStart + recycledTiles; |
| 543 this._physicalStart = this._physicalStart + recycledTiles; |
534 this._update(recycledTileSet, movingUp); | 544 this._update(recycledTileSet, movingUp); |
535 } | 545 } |
536 }, | 546 }, |
537 | 547 |
538 /** | 548 /** |
539 * Update the list of items, starting from the `_virtualStart` item. | 549 * Update the list of items, starting from the `_virtualStart` item. |
540 * @param {!Array<number>=} itemSet | 550 * @param {!Array<number>=} itemSet |
541 * @param {!Array<number>=} movingUp | 551 * @param {!Array<number>=} movingUp |
542 */ | 552 */ |
543 _update: function(itemSet, movingUp) { | 553 _update: function(itemSet, movingUp) { |
544 // manage focus | 554 // manage focus |
545 if (this._isIndexRendered(this._focusedIndex)) { | 555 this._manageFocus(); |
546 this._restoreFocusedItem(); | |
547 } else { | |
548 this._createFocusBackfillItem(); | |
549 } | |
550 // update models | 556 // update models |
551 this._assignModels(itemSet); | 557 this._assignModels(itemSet); |
552 // measure heights | 558 // measure heights |
553 this._updateMetrics(itemSet); | 559 this._updateMetrics(itemSet); |
554 // adjust offset after measuring | 560 // adjust offset after measuring |
555 if (movingUp) { | 561 if (movingUp) { |
556 while (movingUp.length) { | 562 while (movingUp.length) { |
557 this._physicalTop -= this._physicalSizes[movingUp.pop()]; | 563 this._physicalTop -= this._physicalSizes[movingUp.pop()]; |
558 } | 564 } |
559 } | 565 } |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
606 this._debounceTemplate(this._increasePool.bind(this, 1)); | 612 this._debounceTemplate(this._increasePool.bind(this, 1)); |
607 } | 613 } |
608 this._lastPage = currentPage; | 614 this._lastPage = currentPage; |
609 return true; | 615 return true; |
610 }, | 616 }, |
611 | 617 |
612 /** | 618 /** |
613 * Increases the pool size. | 619 * Increases the pool size. |
614 */ | 620 */ |
615 _increasePool: function(missingItems) { | 621 _increasePool: function(missingItems) { |
616 // limit the size | |
617 var nextPhysicalCount = Math.min( | 622 var nextPhysicalCount = Math.min( |
618 this._physicalCount + missingItems, | 623 this._physicalCount + missingItems, |
619 this._virtualCount - this._virtualStart, | 624 this._virtualCount - this._virtualStart, |
620 MAX_PHYSICAL_COUNT | 625 MAX_PHYSICAL_COUNT |
621 ); | 626 ); |
622 var prevPhysicalCount = this._physicalCount; | 627 var prevPhysicalCount = this._physicalCount; |
623 var delta = nextPhysicalCount - prevPhysicalCount; | 628 var delta = nextPhysicalCount - prevPhysicalCount; |
624 | 629 |
625 if (delta > 0) { | 630 if (delta <= 0) { |
626 [].push.apply(this._physicalItems, this._createPool(delta)); | 631 return; |
627 [].push.apply(this._physicalSizes, new Array(delta)); | 632 } |
628 | 633 |
629 this._physicalCount = prevPhysicalCount + delta; | 634 [].push.apply(this._physicalItems, this._createPool(delta)); |
630 // tail call | 635 [].push.apply(this._physicalSizes, new Array(delta)); |
631 return this._update(); | 636 |
| 637 this._physicalCount = prevPhysicalCount + delta; |
| 638 |
| 639 // update the physical start if we need to preserve the model of the focus
ed item. |
| 640 // In this situation, the focused item is currently rendered and its model
would |
| 641 // have changed after increasing the pool if the physical start remained u
nchanged. |
| 642 if (this._physicalStart > this._physicalEnd && |
| 643 this._isIndexRendered(this._focusedIndex) && |
| 644 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { |
| 645 this._physicalStart = this._physicalStart + delta; |
632 } | 646 } |
| 647 this._update(); |
633 }, | 648 }, |
634 | 649 |
635 /** | 650 /** |
636 * Render a new list of items. This method does exactly the same as `update`
, | 651 * Render a new list of items. This method does exactly the same as `update`
, |
637 * but it also ensures that only one `update` cycle is created. | 652 * but it also ensures that only one `update` cycle is created. |
638 */ | 653 */ |
639 _render: function() { | 654 _render: function() { |
640 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 655 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
641 | 656 |
642 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 657 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
711 _forwardParentPath: function(path, value) { | 726 _forwardParentPath: function(path, value) { |
712 if (this._physicalItems) { | 727 if (this._physicalItems) { |
713 this._physicalItems.forEach(function(item) { | 728 this._physicalItems.forEach(function(item) { |
714 item._templateInstance.notifyPath(path, value, true); | 729 item._templateInstance.notifyPath(path, value, true); |
715 }, this); | 730 }, this); |
716 } | 731 } |
717 }, | 732 }, |
718 | 733 |
719 /** | 734 /** |
720 * Called as a side effect of a host items.<key>.<path> path change, | 735 * Called as a side effect of a host items.<key>.<path> path change, |
721 * responsible for notifying item.<path> changes to row for key. | 736 * responsible for notifying item.<path> changes. |
722 */ | 737 */ |
723 _forwardItemPath: function(path, value) { | 738 _forwardItemPath: function(path, value) { |
724 if (this._physicalIndexForKey) { | 739 if (!this._physicalIndexForKey) { |
725 var dot = path.indexOf('.'); | 740 return; |
726 var key = path.substring(0, dot < 0 ? path.length : dot); | 741 } |
727 var idx = this._physicalIndexForKey[key]; | 742 var inst; |
728 var row = this._physicalItems[idx]; | 743 var dot = path.indexOf('.'); |
| 744 var key = path.substring(0, dot < 0 ? path.length : dot); |
| 745 var idx = this._physicalIndexForKey[key]; |
| 746 var el = this._physicalItems[idx]; |
729 | 747 |
730 if (idx === this._focusedIndex && this._offscreenFocusedItem) { | 748 |
731 row = this._offscreenFocusedItem; | 749 if (idx === this._focusedIndex && this._offscreenFocusedItem) { |
732 } | 750 el = this._offscreenFocusedItem; |
733 if (row) { | 751 } |
734 var inst = row._templateInstance; | 752 if (!el) { |
735 if (dot >= 0) { | 753 return; |
736 path = this.as + '.' + path.substring(dot+1); | 754 } |
737 inst.notifyPath(path, value, true); | 755 |
738 } else { | 756 inst = el._templateInstance; |
739 inst[this.as] = value; | 757 |
740 } | 758 if (inst.__key__ !== key) { |
741 } | 759 return; |
| 760 } |
| 761 if (dot >= 0) { |
| 762 path = this.as + '.' + path.substring(dot+1); |
| 763 inst.notifyPath(path, value, true); |
| 764 } else { |
| 765 inst[this.as] = value; |
742 } | 766 } |
743 }, | 767 }, |
744 | 768 |
745 /** | 769 /** |
746 * Called when the items have changed. That is, ressignments | 770 * Called when the items have changed. That is, ressignments |
747 * to `items`, splices or updates to a single item. | 771 * to `items`, splices or updates to a single item. |
748 */ | 772 */ |
749 _itemsChanged: function(change) { | 773 _itemsChanged: function(change) { |
750 if (change.path === 'items') { | 774 if (change.path === 'items') { |
751 | 775 // reset items |
752 this._restoreFocusedItem(); | |
753 // render the new set | |
754 this._itemsRendered = false; | |
755 // update the whole set | |
756 this._virtualStart = 0; | 776 this._virtualStart = 0; |
757 this._physicalTop = 0; | 777 this._physicalTop = 0; |
758 this._virtualCount = this.items ? this.items.length : 0; | 778 this._virtualCount = this.items ? this.items.length : 0; |
759 this._focusedIndex = 0; | |
760 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | 779 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
761 this._physicalIndexForKey = {}; | 780 this._physicalIndexForKey = {}; |
762 | 781 |
763 this._resetScrollPosition(0); | 782 this._resetScrollPosition(0); |
| 783 this._removeFocusedItem(); |
764 | 784 |
765 // create the initial physical items | 785 // create the initial physical items |
766 if (!this._physicalItems) { | 786 if (!this._physicalItems) { |
767 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | 787 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
768 this._physicalItems = this._createPool(this._physicalCount); | 788 this._physicalItems = this._createPool(this._physicalCount); |
769 this._physicalSizes = new Array(this._physicalCount); | 789 this._physicalSizes = new Array(this._physicalCount); |
770 } | 790 } |
771 this._debounceTemplate(this._render); | 791 |
| 792 this._physicalStart = 0; |
772 | 793 |
773 } else if (change.path === 'items.splices') { | 794 } else if (change.path === 'items.splices') { |
774 // render the new set | |
775 this._itemsRendered = false; | |
776 this._adjustVirtualIndex(change.value.indexSplices); | 795 this._adjustVirtualIndex(change.value.indexSplices); |
777 this._virtualCount = this.items ? this.items.length : 0; | 796 this._virtualCount = this.items ? this.items.length : 0; |
778 | 797 |
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); | |
785 | |
786 } else { | 798 } else { |
787 // update a single item | 799 // update a single item |
788 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | 800 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
| 801 return; |
789 } | 802 } |
| 803 |
| 804 this._itemsRendered = false; |
| 805 this._debounceTemplate(this._render); |
790 }, | 806 }, |
791 | 807 |
792 /** | 808 /** |
793 * @param {!Array<!PolymerSplice>} splices | 809 * @param {!Array<!PolymerSplice>} splices |
794 */ | 810 */ |
795 _adjustVirtualIndex: function(splices) { | 811 _adjustVirtualIndex: function(splices) { |
796 var i, splice, idx; | 812 splices.forEach(function(splice) { |
| 813 // deselect removed items |
| 814 splice.removed.forEach(this._removeItem, this); |
| 815 // We only need to care about changes happening above the current positi
on |
| 816 if (splice.index < this._virtualStart) { |
| 817 var delta = Math.max( |
| 818 splice.addedCount - splice.removed.length, |
| 819 splice.index - this._virtualStart); |
797 | 820 |
798 for (i = 0; i < splices.length; i++) { | 821 this._virtualStart = this._virtualStart + delta; |
799 splice = splices[i]; | |
800 | 822 |
801 // deselect removed items | 823 if (this._focusedIndex >= 0) { |
802 splice.removed.forEach(this.$.selector.deselect, this.$.selector); | 824 this._focusedIndex = this._focusedIndex + delta; |
| 825 } |
| 826 } |
| 827 }, this); |
| 828 }, |
803 | 829 |
804 idx = splice.index; | 830 _removeItem: function(item) { |
805 // We only need to care about changes happening above the current positi
on | 831 this.$.selector.deselect(item); |
806 if (idx >= this._virtualStart) { | 832 // remove the current focused item |
807 break; | 833 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
808 } | 834 this._removeFocusedItem(); |
809 | |
810 this._virtualStart = this._virtualStart + | |
811 Math.max(splice.addedCount - splice.removed.length, idx - this._virt
ualStart); | |
812 } | 835 } |
813 }, | 836 }, |
814 | 837 |
815 /** | 838 /** |
816 * Executes a provided function per every physical index in `itemSet` | 839 * Executes a provided function per every physical index in `itemSet` |
817 * `itemSet` default value is equivalent to the entire set of physical index
es. | 840 * `itemSet` default value is equivalent to the entire set of physical index
es. |
818 * | 841 * |
819 * @param {!function(number, number)} fn | 842 * @param {!function(number, number)} fn |
820 * @param {!Array<number>=} itemSet | 843 * @param {!Array<number>=} itemSet |
821 */ | 844 */ |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
854 /** | 877 /** |
855 * Assigns the data models to a given set of items. | 878 * Assigns the data models to a given set of items. |
856 * @param {!Array<number>=} itemSet | 879 * @param {!Array<number>=} itemSet |
857 */ | 880 */ |
858 _assignModels: function(itemSet) { | 881 _assignModels: function(itemSet) { |
859 this._iterateItems(function(pidx, vidx) { | 882 this._iterateItems(function(pidx, vidx) { |
860 var el = this._physicalItems[pidx]; | 883 var el = this._physicalItems[pidx]; |
861 var inst = el._templateInstance; | 884 var inst = el._templateInstance; |
862 var item = this.items && this.items[vidx]; | 885 var item = this.items && this.items[vidx]; |
863 | 886 |
864 if (item !== undefined && item !== null) { | 887 if (item != null) { |
865 inst[this.as] = item; | 888 inst[this.as] = item; |
866 inst.__key__ = this._collection.getKey(item); | 889 inst.__key__ = this._collection.getKey(item); |
867 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | 890 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); |
868 inst[this.indexAs] = vidx; | 891 inst[this.indexAs] = vidx; |
869 inst.tabIndex = vidx === this._focusedIndex ? 0 : -1; | 892 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
| 893 this._physicalIndexForKey[inst.__key__] = pidx; |
870 el.removeAttribute('hidden'); | 894 el.removeAttribute('hidden'); |
871 this._physicalIndexForKey[inst.__key__] = pidx; | |
872 } else { | 895 } else { |
873 inst.__key__ = null; | 896 inst.__key__ = null; |
874 el.setAttribute('hidden', ''); | 897 el.setAttribute('hidden', ''); |
875 } | 898 } |
876 | |
877 }, itemSet); | 899 }, itemSet); |
878 }, | 900 }, |
879 | 901 |
880 /** | 902 /** |
881 * Updates the height for a given set of items. | 903 * Updates the height for a given set of items. |
882 * | 904 * |
883 * @param {!Array<number>=} itemSet | 905 * @param {!Array<number>=} itemSet |
884 */ | 906 */ |
885 _updateMetrics: function(itemSet) { | 907 _updateMetrics: function(itemSet) { |
886 // Make sure we distributed all the physical items | 908 // Make sure we distributed all the physical items |
(...skipping 27 matching lines...) Expand all Loading... |
914 | 936 |
915 /** | 937 /** |
916 * Updates the position of the physical items. | 938 * Updates the position of the physical items. |
917 */ | 939 */ |
918 _positionItems: function() { | 940 _positionItems: function() { |
919 this._adjustScrollPosition(); | 941 this._adjustScrollPosition(); |
920 | 942 |
921 var y = this._physicalTop; | 943 var y = this._physicalTop; |
922 | 944 |
923 this._iterateItems(function(pidx) { | 945 this._iterateItems(function(pidx) { |
924 | |
925 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); | 946 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
926 y += this._physicalSizes[pidx]; | 947 y += this._physicalSizes[pidx]; |
927 | |
928 }); | 948 }); |
929 }, | 949 }, |
930 | 950 |
931 /** | 951 /** |
932 * Adjusts the scroll position when it was overestimated. | 952 * Adjusts the scroll position when it was overestimated. |
933 */ | 953 */ |
934 _adjustScrollPosition: function() { | 954 _adjustScrollPosition: function() { |
935 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | 955 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : |
936 Math.min(this._scrollPosition + this._physicalTop, 0); | 956 Math.min(this._scrollPosition + this._physicalTop, 0); |
937 | 957 |
(...skipping 27 matching lines...) Expand all Loading... |
965 | 985 |
966 forceUpdate = forceUpdate || this._scrollHeight === 0; | 986 forceUpdate = forceUpdate || this._scrollHeight === 0; |
967 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | 987 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
968 | 988 |
969 // amortize height adjustment, so it won't trigger repaints very often | 989 // amortize height adjustment, so it won't trigger repaints very often |
970 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | 990 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
971 this.$.items.style.height = this._estScrollHeight + 'px'; | 991 this.$.items.style.height = this._estScrollHeight + 'px'; |
972 this._scrollHeight = this._estScrollHeight; | 992 this._scrollHeight = this._estScrollHeight; |
973 } | 993 } |
974 }, | 994 }, |
975 | |
976 /** | 995 /** |
977 * Scroll to a specific item in the virtual list regardless | 996 * Scroll to a specific item in the virtual list regardless |
978 * of the physical items in the DOM tree. | 997 * of the physical items in the DOM tree. |
979 * | 998 * |
980 * @method scrollToIndex | 999 * @method scrollToIndex |
981 * @param {number} idx The index of the item | 1000 * @param {number} idx The index of the item |
982 */ | 1001 */ |
983 scrollToIndex: function(idx) { | 1002 scrollToIndex: function(idx) { |
984 if (typeof idx !== 'number') { | 1003 if (typeof idx !== 'number') { |
985 return; | 1004 return; |
986 } | 1005 } |
987 | 1006 |
988 Polymer.dom.flush(); | 1007 Polymer.dom.flush(); |
989 | 1008 |
990 var firstVisible = this.firstVisibleIndex; | |
991 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | 1009 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); |
992 | 1010 // update the virtual start only when needed |
993 // start at the previous virtual item | 1011 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
994 // so we have a item above the first visible item | 1012 this._virtualStart = idx - 1; |
995 this._virtualStart = idx - 1; | 1013 } |
| 1014 // manage focus |
| 1015 this._manageFocus(); |
996 // assign new models | 1016 // assign new models |
997 this._assignModels(); | 1017 this._assignModels(); |
998 // measure the new sizes | 1018 // measure the new sizes |
999 this._updateMetrics(); | 1019 this._updateMetrics(); |
1000 // estimate new physical offset | 1020 // estimate new physical offset |
1001 this._physicalTop = this._virtualStart * this._physicalAverage; | 1021 this._physicalTop = this._virtualStart * this._physicalAverage; |
1002 | 1022 |
1003 var currentTopItem = this._physicalStart; | 1023 var currentTopItem = this._physicalStart; |
1004 var currentVirtualItem = this._virtualStart; | 1024 var currentVirtualItem = this._virtualStart; |
1005 var targetOffsetTop = 0; | 1025 var targetOffsetTop = 0; |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1048 this.updateViewportBoundaries(); | 1068 this.updateViewportBoundaries(); |
1049 this.scrollToIndex(this.firstVisibleIndex); | 1069 this.scrollToIndex(this.firstVisibleIndex); |
1050 } | 1070 } |
1051 }); | 1071 }); |
1052 }, | 1072 }, |
1053 | 1073 |
1054 _getModelFromItem: function(item) { | 1074 _getModelFromItem: function(item) { |
1055 var key = this._collection.getKey(item); | 1075 var key = this._collection.getKey(item); |
1056 var pidx = this._physicalIndexForKey[key]; | 1076 var pidx = this._physicalIndexForKey[key]; |
1057 | 1077 |
1058 if (pidx !== undefined) { | 1078 if (pidx != null) { |
1059 return this._physicalItems[pidx]._templateInstance; | 1079 return this._physicalItems[pidx]._templateInstance; |
1060 } | 1080 } |
1061 return null; | 1081 return null; |
1062 }, | 1082 }, |
1063 | 1083 |
1064 /** | 1084 /** |
1065 * Gets a valid item instance from its index or the object value. | 1085 * Gets a valid item instance from its index or the object value. |
1066 * | 1086 * |
1067 * @param {(Object|number)} item The item object or its index | 1087 * @param {(Object|number)} item The item object or its index |
1068 */ | 1088 */ |
(...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1186 * Updates the size of an item. | 1206 * Updates the size of an item. |
1187 * | 1207 * |
1188 * @method updateSizeForItem | 1208 * @method updateSizeForItem |
1189 * @param {(Object|number)} item The item object or its index | 1209 * @param {(Object|number)} item The item object or its index |
1190 */ | 1210 */ |
1191 updateSizeForItem: function(item) { | 1211 updateSizeForItem: function(item) { |
1192 item = this._getNormalizedItem(item); | 1212 item = this._getNormalizedItem(item); |
1193 var key = this._collection.getKey(item); | 1213 var key = this._collection.getKey(item); |
1194 var pidx = this._physicalIndexForKey[key]; | 1214 var pidx = this._physicalIndexForKey[key]; |
1195 | 1215 |
1196 if (pidx !== undefined) { | 1216 if (pidx != null) { |
1197 this._updateMetrics([pidx]); | 1217 this._updateMetrics([pidx]); |
1198 this._positionItems(); | 1218 this._positionItems(); |
1199 } | 1219 } |
1200 }, | 1220 }, |
1201 | 1221 |
| 1222 /** |
| 1223 * Creates a temporary backfill item in the rendered pool of physical items |
| 1224 * to replace the main focused item. The focused item has tabIndex = 0 |
| 1225 * and might be currently focused by the user. |
| 1226 * |
| 1227 * This dynamic replacement helps to preserve the focus state. |
| 1228 */ |
| 1229 _manageFocus: function() { |
| 1230 var fidx = this._focusedIndex; |
| 1231 |
| 1232 if (fidx >= 0 && fidx < this._virtualCount) { |
| 1233 // if it's a valid index, check if that index is rendered |
| 1234 // in a physical item. |
| 1235 if (this._isIndexRendered(fidx)) { |
| 1236 this._restoreFocusedItem(); |
| 1237 } else { |
| 1238 this._createFocusBackfillItem(); |
| 1239 } |
| 1240 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
| 1241 // otherwise, assign the initial focused index. |
| 1242 this._focusedIndex = this._virtualStart; |
| 1243 this._focusedItem = this._physicalItems[this._physicalStart]; |
| 1244 } |
| 1245 }, |
| 1246 |
1202 _isIndexRendered: function(idx) { | 1247 _isIndexRendered: function(idx) { |
1203 return idx >= this._virtualStart && idx <= this._virtualEnd; | 1248 return idx >= this._virtualStart && idx <= this._virtualEnd; |
1204 }, | 1249 }, |
1205 | 1250 |
1206 _getPhysicalItemForIndex: function(idx, force) { | 1251 _isIndexVisible: function(idx) { |
1207 if (!this._collection) { | 1252 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
1208 return null; | 1253 }, |
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 | 1254 |
1220 return physicalItem || null; | 1255 _getPhysicalIndex: function(idx) { |
| 1256 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
1221 }, | 1257 }, |
1222 | 1258 |
1223 _focusPhysicalItem: function(idx) { | 1259 _focusPhysicalItem: function(idx) { |
1224 this._restoreFocusedItem(); | 1260 if (idx < 0 || idx >= this._virtualCount) { |
1225 | |
1226 var physicalItem = this._getPhysicalItemForIndex(idx, true); | |
1227 if (!physicalItem) { | |
1228 return; | 1261 return; |
1229 } | 1262 } |
| 1263 this._restoreFocusedItem(); |
| 1264 // scroll to index to make sure it's rendered |
| 1265 if (!this._isIndexRendered(idx)) { |
| 1266 this.scrollToIndex(idx); |
| 1267 } |
| 1268 |
| 1269 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
1230 var SECRET = ~(Math.random() * 100); | 1270 var SECRET = ~(Math.random() * 100); |
1231 var model = physicalItem._templateInstance; | 1271 var model = physicalItem._templateInstance; |
1232 var focusable; | 1272 var focusable; |
1233 | 1273 |
| 1274 // set a secret tab index |
1234 model.tabIndex = SECRET; | 1275 model.tabIndex = SECRET; |
1235 // the focusable element could be the entire physical item | 1276 // check if focusable element is the physical item |
1236 if (physicalItem.tabIndex === SECRET) { | 1277 if (physicalItem.tabIndex === SECRET) { |
1237 focusable = physicalItem; | 1278 focusable = physicalItem; |
1238 } | 1279 } |
1239 // the focusable element could be somewhere within the physical item | 1280 // search for the element which tabindex is bound to the secret tab index |
1240 if (!focusable) { | 1281 if (!focusable) { |
1241 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET + '"]'); | 1282 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET + '"]'); |
1242 } | 1283 } |
1243 // restore the tab index | 1284 // restore the tab index |
1244 model.tabIndex = 0; | 1285 model.tabIndex = 0; |
| 1286 // focus the focusable element |
| 1287 this._focusedIndex = idx; |
1245 focusable && focusable.focus(); | 1288 focusable && focusable.focus(); |
1246 }, | 1289 }, |
1247 | 1290 |
1248 _restoreFocusedItem: function() { | 1291 _removeFocusedItem: function() { |
1249 if (!this._offscreenFocusedItem) { | 1292 if (this._offscreenFocusedItem) { |
1250 return; | 1293 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
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 } | 1294 } |
1259 this._offscreenFocusedItem = null; | 1295 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; | 1296 this._focusBackfillItem = null; |
| 1297 this._focusedItem = null; |
| 1298 this._focusedIndex = -1; |
1269 }, | 1299 }, |
1270 | 1300 |
1271 _createFocusBackfillItem: function() { | 1301 _createFocusBackfillItem: function() { |
1272 if (this._offscreenFocusedItem) { | 1302 var pidx, fidx = this._focusedIndex; |
| 1303 if (this._offscreenFocusedItem || fidx < 0) { |
1273 return; | 1304 return; |
1274 } | 1305 } |
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) { | 1306 if (!this._focusBackfillItem) { |
| 1307 // create a physical item, so that it backfills the focused item. |
1282 var stampedTemplate = this.stamp(null); | 1308 var stampedTemplate = this.stamp(null); |
1283 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | 1309 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
1284 Polymer.dom(this).appendChild(stampedTemplate.root); | 1310 Polymer.dom(this).appendChild(stampedTemplate.root); |
1285 } | 1311 } |
1286 this._physicalItems[pidx] = this._focusBackfillItem; | 1312 // get the physical index for the focused index |
| 1313 pidx = this._getPhysicalIndex(fidx); |
| 1314 |
| 1315 if (pidx != null) { |
| 1316 // set the offcreen focused physical item |
| 1317 this._offscreenFocusedItem = this._physicalItems[pidx]; |
| 1318 // backfill the focused physical item |
| 1319 this._physicalItems[pidx] = this._focusBackfillItem; |
| 1320 // hide the focused physical |
| 1321 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
| 1322 } |
| 1323 }, |
| 1324 |
| 1325 _restoreFocusedItem: function() { |
| 1326 var pidx, fidx = this._focusedIndex; |
| 1327 |
| 1328 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
| 1329 return; |
| 1330 } |
| 1331 // assign models to the focused index |
| 1332 this._assignModels(); |
| 1333 // get the new physical index for the focused index |
| 1334 pidx = this._getPhysicalIndex(fidx); |
| 1335 |
| 1336 if (pidx != null) { |
| 1337 // flip the focus backfill |
| 1338 this._focusBackfillItem = this._physicalItems[pidx]; |
| 1339 // restore the focused physical item |
| 1340 this._physicalItems[pidx] = this._offscreenFocusedItem; |
| 1341 // reset the offscreen focused item |
| 1342 this._offscreenFocusedItem = null; |
| 1343 // hide the physical item that backfills |
| 1344 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
| 1345 } |
1287 }, | 1346 }, |
1288 | 1347 |
1289 _didFocus: function(e) { | 1348 _didFocus: function(e) { |
1290 var targetModel = this.modelForElement(e.target); | 1349 var targetModel = this.modelForElement(e.target); |
| 1350 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
| 1351 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
1291 var fidx = this._focusedIndex; | 1352 var fidx = this._focusedIndex; |
1292 | 1353 |
1293 if (!targetModel) { | 1354 if (!targetModel || !focusedModel) { |
1294 return; | 1355 return; |
1295 } | 1356 } |
1296 this._restoreFocusedItem(); | 1357 if (focusedModel === targetModel) { |
1297 | 1358 // if the user focused the same item, then bring it into view if it's no
t visible |
1298 if (this.modelForElement(this._offscreenFocusedItem) === targetModel) { | 1359 if (!this._isIndexVisible(fidx)) { |
1299 this.scrollToIndex(fidx); | 1360 this.scrollToIndex(fidx); |
| 1361 } |
1300 } else { | 1362 } else { |
| 1363 this._restoreFocusedItem(); |
1301 // restore tabIndex for the currently focused item | 1364 // restore tabIndex for the currently focused item |
1302 this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1; | 1365 focusedModel.tabIndex = -1; |
1303 // set the tabIndex for the next focused item | 1366 // set the tabIndex for the next focused item |
1304 targetModel.tabIndex = 0; | 1367 targetModel.tabIndex = 0; |
1305 fidx = /** @type {{index: number}} */(targetModel).index; | 1368 fidx = targetModel[this.indexAs]; |
1306 this._focusedIndex = fidx; | 1369 this._focusedIndex = fidx; |
1307 // bring the item into view | 1370 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
1308 if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) { | 1371 |
1309 this.scrollToIndex(fidx); | 1372 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
1310 } else { | |
1311 this._update(); | 1373 this._update(); |
1312 } | 1374 } |
1313 } | 1375 } |
1314 }, | 1376 }, |
1315 | 1377 |
1316 _didMoveUp: function() { | 1378 _didMoveUp: function() { |
1317 this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1)); | 1379 this._focusPhysicalItem(this._focusedIndex - 1); |
1318 }, | 1380 }, |
1319 | 1381 |
1320 _didMoveDown: function() { | 1382 _didMoveDown: function() { |
1321 this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex +
1)); | 1383 this._focusPhysicalItem(this._focusedIndex + 1); |
1322 }, | 1384 }, |
1323 | 1385 |
1324 _didEnter: function(e) { | 1386 _didEnter: function(e) { |
1325 // focus the currently focused physical item | |
1326 this._focusPhysicalItem(this._focusedIndex); | 1387 this._focusPhysicalItem(this._focusedIndex); |
1327 // toggle selection | 1388 this._selectionHandler(e.detail.keyboardEvent); |
1328 this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).key
boardEvent); | |
1329 } | 1389 } |
1330 }); | 1390 }); |
1331 | 1391 |
1332 })(); | 1392 })(); |
OLD | NEW |