Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(180)

Side by Side Diff: third_party/polymer/v1_0/components-chromium/iron-list/iron-list-extracted.js

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

Powered by Google App Engine
This is Rietveld 408576698