| 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 |