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

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

Issue 1351623008: MD Settings: Languages model for language pages (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@SingletonPrefs
Patch Set: Created 5 years, 3 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
(Empty)
1
2
3 (function() {
4
5 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
6 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
7 var DEFAULT_PHYSICAL_COUNT = 20;
8 var MAX_PHYSICAL_COUNT = 500;
9
10 Polymer({
11
12 is: 'iron-list',
13
14 properties: {
15
16 /**
17 * An array containing items determining how many instances of the templat e
18 * to stamp and that that each template instance should bind to.
19 */
20 items: {
21 type: Array
22 },
23
24 /**
25 * The name of the variable to add to the binding scope for the array
26 * element associated with a given template instance.
27 */
28 as: {
29 type: String,
30 value: 'item'
31 },
32
33 /**
34 * The name of the variable to add to the binding scope with the index
35 * for the row. If `sort` is provided, the index will reflect the
36 * sorted order (rather than the original array order).
37 */
38 indexAs: {
39 type: String,
40 value: 'index'
41 },
42
43 /**
44 * The name of the variable to add to the binding scope to indicate
45 * if the row is selected.
46 */
47 selectedAs: {
48 type: String,
49 value: 'selected'
50 },
51
52 /**
53 * When true, tapping a row will select the item, placing its data model
54 * in the set of selected items retrievable via the selection property.
55 *
56 * Note that tapping focusable elements within the list item will not
57 * result in selection, since they are presumed to have their * own action .
58 */
59 selectionEnabled: {
60 type: Boolean,
61 value: false
62 },
63
64 /**
65 * When `multiSelection` is false, this is the currently selected item, or `null`
66 * if no item is selected.
67 */
68 selectedItem: {
69 type: Object,
70 notify: true
71 },
72
73 /**
74 * When `multiSelection` is true, this is an array that contains the selec ted items.
75 */
76 selectedItems: {
77 type: Object,
78 notify: true
79 },
80
81 /**
82 * When `true`, multiple items may be selected at once (in this case,
83 * `selected` is an array of currently selected items). When `false`,
84 * only one item may be selected at a time.
85 */
86 multiSelection: {
87 type: Boolean,
88 value: false
89 }
90 },
91
92 observers: [
93 '_itemsChanged(items.*)',
94 '_selectionEnabledChanged(selectionEnabled)',
95 '_multiSelectionChanged(multiSelection)'
96 ],
97
98 behaviors: [
99 Polymer.Templatizer,
100 Polymer.IronResizableBehavior
101 ],
102
103 listeners: {
104 'iron-resize': '_resizeHandler'
105 },
106
107 /**
108 * The ratio of hidden tiles that should remain in the scroll direction.
109 * Recommended value ~0.5, so it will distribute tiles evely in both directi ons.
110 */
111 _ratio: 0.5,
112
113 /**
114 * The element that controls the scroll
115 */
116 _scroller: null,
117
118 /**
119 * The padding-top value of the `scroller` element
120 */
121 _scrollerPaddingTop: 0,
122
123 /**
124 * This value is the same as `scrollTop`.
125 */
126 _scrollPosition: 0,
127
128 /**
129 * The number of tiles in the DOM.
130 */
131 _physicalCount: 0,
132
133 /**
134 * The k-th tile that is at the top of the scrolling list.
135 */
136 _physicalStart: 0,
137
138 /**
139 * The k-th tile that is at the bottom of the scrolling list.
140 */
141 _physicalEnd: 0,
142
143 /**
144 * The sum of the heights of all the tiles in the DOM.
145 */
146 _physicalSize: 0,
147
148 /**
149 * The average `offsetHeight` of the tiles observed till now.
150 */
151 _physicalAverage: 0,
152
153 /**
154 * The number of tiles which `offsetHeight` > 0 observed until now.
155 */
156 _physicalAverageCount: 0,
157
158 /**
159 * The Y position of the item rendered in the `_physicalStart`
160 * tile relative to the scrolling list.
161 */
162 _physicalTop: 0,
163
164 /**
165 * The number of items in the list.
166 */
167 _virtualCount: 0,
168
169 /**
170 * The n-th item rendered in the `_physicalStart` tile.
171 */
172 _virtualStartVal: 0,
173
174 /**
175 * A map between an item key and its physical item index
176 */
177 _physicalIndexForKey: null,
178
179 /**
180 * The estimated scroll height based on `_physicalAverage`
181 */
182 _estScrollHeight: 0,
183
184 /**
185 * The scroll height of the dom node
186 */
187 _scrollHeight: 0,
188
189 /**
190 * The size of the viewport
191 */
192 _viewportSize: 0,
193
194 /**
195 * An array of DOM nodes that are currently in the tree
196 */
197 _physicalItems: null,
198
199 /**
200 * An array of heights for each item in `_physicalItems`
201 */
202 _physicalSizes: null,
203
204 /**
205 * A cached value for the visible index.
206 * See `firstVisibleIndex`
207 */
208 _firstVisibleIndexVal: null,
209
210 /**
211 * A Polymer collection for the items.
212 */
213 _collection: null,
214
215 /**
216 * True if the current item list was rendered for the first time
217 * after attached.
218 */
219 _itemsRendered: false,
220
221 /**
222 * The bottom of the physical content.
223 */
224 get _physicalBottom() {
225 return this._physicalTop + this._physicalSize;
226 },
227
228 /**
229 * The n-th item rendered in the last physical item.
230 */
231 get _virtualEnd() {
232 return this._virtualStartVal + this._physicalCount - 1;
233 },
234
235 /**
236 * The lowest n-th value for an item such that it can be rendered in `_physi calStart`.
237 */
238 _minVirtualStart: 0,
239
240 /**
241 * The largest n-th value for an item such that it can be rendered in `_phys icalStart`.
242 */
243 get _maxVirtualStart() {
244 return this._virtualCount < this._physicalCount ?
245 this._virtualCount : this._virtualCount - this._physicalCount;
246 },
247
248 /**
249 * The height of the physical content that isn't on the screen.
250 */
251 get _hiddenContentSize() {
252 return this._physicalSize - this._viewportSize;
253 },
254
255 /**
256 * The maximum scroll top value.
257 */
258 get _maxScrollTop() {
259 return this._estScrollHeight - this._viewportSize;
260 },
261
262 /**
263 * Sets the n-th item rendered in `_physicalStart`
264 */
265 set _virtualStart(val) {
266 // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart
267 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min VirtualStart, val));
268 this._physicalStart = this._virtualStartVal % this._physicalCount;
269 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this ._physicalCount;
270 },
271
272 /**
273 * Gets the n-th item rendered in `_physicalStart`
274 */
275 get _virtualStart() {
276 return this._virtualStartVal;
277 },
278
279 /**
280 * An optimal physical size such that we will have enough physical items
281 * to fill up the viewport and recycle when the user scrolls.
282 *
283 * This default value assumes that we will at least have the equivalent
284 * to a viewport of physical items above and below the user's viewport.
285 */
286 get _optPhysicalSize() {
287 return this._viewportSize * 3;
288 },
289
290 /**
291 * True if the current list is visible.
292 */
293 get _isVisible() {
294 return this._scroller && Boolean(this._scroller.offsetWidth || this._scrol ler.offsetHeight);
295 },
296
297 /**
298 * Gets the first visible item in the viewport.
299 *
300 * @property firstVisibleIndex
301 */
302 get firstVisibleIndex() {
303 var physicalOffset;
304
305 if (this._firstVisibleIndexVal === null) {
306 physicalOffset = this._physicalTop;
307
308 this._firstVisibleIndexVal = this._iterateItems(
309 function(pidx, vidx) {
310 physicalOffset += this._physicalSizes[pidx];
311
312 if (physicalOffset > this._scrollPosition) {
313 return vidx;
314 }
315 }) || 0;
316 }
317
318 return this._firstVisibleIndexVal;
319 },
320
321 ready: function() {
322 if (IOS_TOUCH_SCROLLING) {
323 this._scrollListener = function() {
324 requestAnimationFrame(this._scrollHandler.bind(this));
325 }.bind(this);
326 } else {
327 this._scrollListener = this._scrollHandler.bind(this);
328 }
329 },
330
331 /**
332 * When the element has been attached to the DOM tree.
333 */
334 attached: function() {
335 // delegate to the parent's scroller
336 // e.g. paper-scroll-header-panel
337 var el = Polymer.dom(this);
338
339 if (el.parentNode && el.parentNode.scroller) {
340 this._scroller = el.parentNode.scroller;
341 } else {
342 this._scroller = this;
343 this.classList.add('has-scroller');
344 }
345
346 if (IOS_TOUCH_SCROLLING) {
347 this._scroller.style.webkitOverflowScrolling = 'touch';
348 }
349
350 this._scroller.addEventListener('scroll', this._scrollListener);
351
352 this.updateViewportBoundaries();
353 this._render();
354 },
355
356 /**
357 * When the element has been removed from the DOM tree.
358 */
359 detached: function() {
360 this._itemsRendered = false;
361 if (this._scroller) {
362 this._scroller.removeEventListener('scroll', this._scrollListener);
363 }
364 },
365
366 /**
367 * Invoke this method if you dynamically update the viewport's
368 * size or CSS padding.
369 *
370 * @method updateViewportBoundaries
371 */
372 updateViewportBoundaries: function() {
373 var scrollerStyle = window.getComputedStyle(this._scroller);
374 this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top']);
375 this._viewportSize = this._scroller.offsetHeight;
376 },
377
378 /**
379 * Update the models, the position of the
380 * items in the viewport and recycle tiles as needed.
381 */
382 _refresh: function() {
383 var SCROLL_DIRECTION_UP = -1;
384 var SCROLL_DIRECTION_DOWN = 1;
385 var SCROLL_DIRECTION_NONE = 0;
386
387 // clamp the `scrollTop` value
388 // IE 10|11 scrollTop may go above `_maxScrollTop`
389 // iOS `scrollTop` may go below 0 and above `_maxScrollTop`
390 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.sc rollTop));
391
392 var tileHeight, kth, recycledTileSet;
393 var ratio = this._ratio;
394 var delta = scrollTop - this._scrollPosition;
395 var direction = SCROLL_DIRECTION_NONE;
396 var recycledTiles = 0;
397 var hiddenContentSize = this._hiddenContentSize;
398 var currentRatio = ratio;
399 var movingUp = [];
400
401 // track the last `scrollTop`
402 this._scrollPosition = scrollTop;
403
404 // clear cached visible index
405 this._firstVisibleIndexVal = null;
406
407 // random access
408 if (Math.abs(delta) > this._physicalSize) {
409 this._physicalTop += delta;
410 direction = SCROLL_DIRECTION_NONE;
411 recycledTiles = Math.round(delta / this._physicalAverage);
412 }
413 // scroll up
414 else if (delta < 0) {
415 var topSpace = scrollTop - this._physicalTop;
416 var virtualStart = this._virtualStart;
417
418 direction = SCROLL_DIRECTION_UP;
419 recycledTileSet = [];
420
421 kth = this._physicalEnd;
422 currentRatio = topSpace / hiddenContentSize;
423
424 // move tiles from bottom to top
425 while (
426 // approximate `currentRatio` to `ratio`
427 currentRatio < ratio &&
428 // recycle less physical items than the total
429 recycledTiles < this._physicalCount &&
430 // ensure that these recycled tiles are needed
431 virtualStart - recycledTiles > 0
432 ) {
433
434 tileHeight = this._physicalSizes[kth] || this._physicalAverage;
435 currentRatio += tileHeight / hiddenContentSize;
436
437 recycledTileSet.push(kth);
438 recycledTiles++;
439 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
440 }
441
442 movingUp = recycledTileSet;
443 recycledTiles = -recycledTiles;
444
445 }
446 // scroll down
447 else if (delta > 0) {
448 var bottomSpace = this._physicalBottom - (scrollTop + this._viewportSize );
449 var virtualEnd = this._virtualEnd;
450 var lastVirtualItemIndex = this._virtualCount-1;
451
452 direction = SCROLL_DIRECTION_DOWN;
453 recycledTileSet = [];
454
455 kth = this._physicalStart;
456 currentRatio = bottomSpace / hiddenContentSize;
457
458 // move tiles from top to bottom
459 while (
460 // approximate `currentRatio` to `ratio`
461 currentRatio < ratio &&
462 // recycle less physical items than the total
463 recycledTiles < this._physicalCount &&
464 // ensure that these recycled tiles are needed
465 virtualEnd + recycledTiles < lastVirtualItemIndex
466 ) {
467
468 tileHeight = this._physicalSizes[kth] || this._physicalAverage;
469 currentRatio += tileHeight / hiddenContentSize;
470
471 this._physicalTop += tileHeight;
472 recycledTileSet.push(kth);
473 recycledTiles++;
474 kth = (kth + 1) % this._physicalCount;
475 }
476 }
477
478 if (recycledTiles !== 0) {
479 this._virtualStart = this._virtualStart + recycledTiles;
480 this._update(recycledTileSet, movingUp);
481 }
482 },
483
484 /**
485 * Update the list of items, starting from the `_virtualStartVal` item.
486 */
487 _update: function(itemSet, movingUp) {
488 // update models
489 this._assignModels(itemSet);
490
491 // measure heights
492 // TODO(blasten) pass `recycledTileSet`
493 this._updateMetrics();
494
495 // adjust offset after measuring
496 if (movingUp) {
497 while (movingUp.length) {
498 this._physicalTop -= this._physicalSizes[movingUp.pop()];
499 }
500 }
501
502 // update the position of the items
503 this._positionItems();
504
505 // set the scroller size
506 this._updateScrollerSize();
507
508 // increase the pool of physical items if needed
509 if (itemSet = this._increasePoolIfNeeded()) {
510 // set models to the new items
511 this.async(this._update.bind(this, itemSet));
512 }
513 },
514
515 /**
516 * Creates a pool of DOM elements and attaches them to the local dom.
517 */
518 _createPool: function(size) {
519 var physicalItems = new Array(size);
520
521 this._ensureTemplatized();
522
523 for (var i = 0; i < size; i++) {
524 var inst = this.stamp(null);
525
526 // First element child is item; Safari doesn't support children[0]
527 // on a doc fragment
528 physicalItems[i] = inst.root.querySelector('*');
529 Polymer.dom(this).appendChild(inst.root);
530 }
531
532 return physicalItems;
533 },
534
535 /**
536 * Increases the pool size. That is, the physical items in the DOM.
537 * This function will allocate additional physical items
538 * (limited by `MAX_PHYSICAL_COUNT`) if the content size is shorter than
539 * `_optPhysicalSize`
540 *
541 * @return Array
542 */
543 _increasePoolIfNeeded: function() {
544 if (this._physicalSize >= this._optPhysicalSize || this._physicalAverage = == 0) {
545 return null;
546 }
547
548 // the estimated number of physical items that we will need to reach
549 // the cap established by `_optPhysicalSize`.
550 var missingItems = Math.round(
551 (this._optPhysicalSize - this._physicalSize) * 1.2 / this._physicalAve rage
552 );
553
554 // limit the size
555 var nextPhysicalCount = Math.min(
556 this._physicalCount + missingItems,
557 this._virtualCount,
558 MAX_PHYSICAL_COUNT
559 );
560
561 var prevPhysicalCount = this._physicalCount;
562 var delta = nextPhysicalCount - prevPhysicalCount;
563
564 if (delta <= 0) {
565 return null;
566 }
567
568 var newPhysicalItems = this._createPool(delta);
569 var emptyArray = new Array(delta);
570
571 [].push.apply(this._physicalItems, newPhysicalItems);
572 [].push.apply(this._physicalSizes, emptyArray);
573
574 this._physicalCount = prevPhysicalCount + delta;
575
576 // fill the array with the new item pos
577 while (delta > 0) {
578 emptyArray[--delta] = prevPhysicalCount + delta;
579 }
580
581 return emptyArray;
582 },
583
584 /**
585 * Render a new list of items. This method does exactly the same as `update` ,
586 * but it also ensures that only one `update` cycle is created.
587 */
588 _render: function() {
589 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
590
591 if (this.isAttached && !this._itemsRendered && this._isVisible && requires Update) {
592 this._update();
593 this._itemsRendered = true;
594 }
595 },
596
597 /**
598 * Templetizes the user template.
599 */
600 _ensureTemplatized: function() {
601 if (!this.ctor) {
602 // Template instance props that should be excluded from forwarding
603 var props = {};
604
605 props.__key__ = true;
606 props[this.as] = true;
607 props[this.indexAs] = true;
608 props[this.selectedAs] = true;
609
610 this._instanceProps = props;
611 this._userTemplate = Polymer.dom(this).querySelector('template');
612
613 if (this._userTemplate) {
614 this.templatize(this._userTemplate);
615 } else {
616 console.warn('iron-list requires a template to be provided in light-do m');
617 }
618 }
619 },
620
621 /**
622 * Implements extension point from Templatizer mixin.
623 */
624 _getStampedChildren: function() {
625 return this._physicalItems;
626 },
627
628 /**
629 * Implements extension point from Templatizer
630 * Called as a side effect of a template instance path change, responsible
631 * for notifying items.<key-for-instance>.<path> change up to host.
632 */
633 _forwardInstancePath: function(inst, path, value) {
634 if (path.indexOf(this.as + '.') === 0) {
635 this.notifyPath('items.' + inst.__key__ + '.' +
636 path.slice(this.as.length + 1), value);
637 }
638 },
639
640 /**
641 * Implements extension point from Templatizer mixin
642 * Called as side-effect of a host property change, responsible for
643 * notifying parent path change on each row.
644 */
645 _forwardParentProp: function(prop, value) {
646 if (this._physicalItems) {
647 this._physicalItems.forEach(function(item) {
648 item._templateInstance[prop] = value;
649 }, this);
650 }
651 },
652
653 /**
654 * Implements extension point from Templatizer
655 * Called as side-effect of a host path change, responsible for
656 * notifying parent.<path> path change on each row.
657 */
658 _forwardParentPath: function(path, value) {
659 if (this._physicalItems) {
660 this._physicalItems.forEach(function(item) {
661 item._templateInstance.notifyPath(path, value, true);
662 }, this);
663 }
664 },
665
666 /**
667 * Called as a side effect of a host items.<key>.<path> path change,
668 * responsible for notifying item.<path> changes to row for key.
669 */
670 _forwardItemPath: function(path, value) {
671 if (this._physicalIndexForKey) {
672 var dot = path.indexOf('.');
673 var key = path.substring(0, dot < 0 ? path.length : dot);
674 var idx = this._physicalIndexForKey[key];
675 var row = this._physicalItems[idx];
676 if (row) {
677 var inst = row._templateInstance;
678 if (dot >= 0) {
679 path = this.as + '.' + path.substring(dot+1);
680 inst.notifyPath(path, value, true);
681 } else {
682 inst[this.as] = value;
683 }
684 }
685 }
686 },
687
688 /**
689 * Called when the items have changed. That is, ressignments
690 * to `items`, splices or updates to a single item.
691 */
692 _itemsChanged: function(change) {
693 if (change.path === 'items') {
694 // render the new set
695 this._itemsRendered = false;
696
697 // update the whole set
698 this._virtualStartVal = 0;
699 this._physicalTop = 0;
700 this._virtualCount = this.items ? this.items.length : 0;
701 this._collection = this.items ? Polymer.Collection.get(this.items) : nul l;
702 this._physicalIndexForKey = {};
703
704 // scroll to the top
705 this._resetScrollPosition(0);
706
707 // create the initial physical items
708 if (!this._physicalItems) {
709 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi s._virtualCount));
710 this._physicalItems = this._createPool(this._physicalCount);
711 this._physicalSizes = new Array(this._physicalCount);
712 }
713
714 this.debounce('refresh', this._render);
715
716 } else if (change.path === 'items.splices') {
717 // render the new set
718 this._itemsRendered = false;
719
720 this._adjustVirtualIndex(change.value.indexSplices);
721 this._virtualCount = this.items ? this.items.length : 0;
722
723 this.debounce('refresh', this._render);
724
725 } else {
726 // update a single item
727 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change. value);
728 }
729 },
730
731 _adjustVirtualIndex: function(splices) {
732 var i, splice, idx;
733
734 for (i = 0; i < splices.length; i++) {
735 splice = splices[i];
736
737 // deselect removed items
738 splice.removed.forEach(this.$.selector.deselect, this.$.selector);
739
740 idx = splice.index;
741 // We only need to care about changes happening above the current positi on
742 if (idx >= this._virtualStartVal) {
743 break;
744 }
745
746 this._virtualStart = this._virtualStart +
747 Math.max(splice.addedCount - splice.removed.length, idx - this._virt ualStartVal);
748 }
749 },
750
751 _scrollHandler: function() {
752 this._refresh();
753 },
754
755 /**
756 * Executes a provided function per every physical index in `itemSet`
757 * `itemSet` default value is equivalent to the entire set of physical index es.
758 */
759 _iterateItems: function(fn, itemSet) {
760 var pidx, vidx, rtn, i;
761
762 if (arguments.length === 2 && itemSet) {
763 for (i = 0; i < itemSet.length; i++) {
764 pidx = itemSet[i];
765 if (pidx >= this._physicalStart) {
766 vidx = this._virtualStartVal + (pidx - this._physicalStart);
767 } else {
768 vidx = this._virtualStartVal + (this._physicalCount - this._physical Start) + pidx;
769 }
770 if ((rtn = fn.call(this, pidx, vidx)) != null) {
771 return rtn;
772 }
773 }
774 } else {
775 pidx = this._physicalStart;
776 vidx = this._virtualStartVal;
777
778 for (; pidx < this._physicalCount; pidx++, vidx++) {
779 if ((rtn = fn.call(this, pidx, vidx)) != null) {
780 return rtn;
781 }
782 }
783
784 pidx = 0;
785
786 for (; pidx < this._physicalStart; pidx++, vidx++) {
787 if ((rtn = fn.call(this, pidx, vidx)) != null) {
788 return rtn;
789 }
790 }
791 }
792 },
793
794 /**
795 * Assigns the data models to a given set of items.
796 */
797 _assignModels: function(itemSet) {
798 this._iterateItems(function(pidx, vidx) {
799 var el = this._physicalItems[pidx];
800 var inst = el._templateInstance;
801 var item = this.items && this.items[vidx];
802
803 if (item) {
804 inst[this.as] = item;
805 inst.__key__ = this._collection.getKey(item);
806 inst[this.selectedAs] = this.$.selector.isSelected(item);
807 inst[this.indexAs] = vidx;
808 el.removeAttribute('hidden');
809 this._physicalIndexForKey[inst.__key__] = pidx;
810 } else {
811 inst.__key__ = null;
812 el.setAttribute('hidden', '');
813 }
814
815 }, itemSet);
816 },
817
818 /**
819 * Updates the height for a given set of items.
820 */
821 _updateMetrics: function() {
822 var total = 0;
823 var prevAvgCount = this._physicalAverageCount;
824 var prevPhysicalAvg = this._physicalAverage;
825
826 // Make sure we distributed all the physical items
827 // so we can measure them
828 Polymer.dom.flush();
829
830 for (var i = 0; i < this._physicalCount; i++) {
831 this._physicalSizes[i] = this._physicalItems[i].offsetHeight;
832 total += this._physicalSizes[i];
833 this._physicalAverageCount += this._physicalSizes[i] ? 1 : 0;
834 }
835
836 this._physicalSize = total;
837 this._viewportSize = this._scroller.offsetHeight;
838
839 if (this._physicalAverageCount !== prevAvgCount) {
840 this._physicalAverage = Math.round(
841 ((prevPhysicalAvg * prevAvgCount) + total) /
842 this._physicalAverageCount);
843 }
844 },
845
846 /**
847 * Updates the position of the physical items.
848 */
849 _positionItems: function(itemSet) {
850 this._adjustScrollPosition();
851
852 var y = this._physicalTop;
853
854 this._iterateItems(function(pidx) {
855
856 this.transform('translate3d(0, ' + y + 'px, 0)', this._physicalItems[pid x]);
857 y += this._physicalSizes[pidx];
858
859 }, itemSet);
860 },
861
862 /**
863 * Adjusts the scroll position when it was overestimated.
864 */
865 _adjustScrollPosition: function() {
866 var deltaHeight = this._virtualStartVal === 0 ? this._physicalTop :
867 Math.min(this._scrollPosition + this._physicalTop, 0);
868
869 if (deltaHeight) {
870 this._physicalTop = this._physicalTop - deltaHeight;
871
872 // juking scroll position during interial scrolling on iOS is no bueno
873 if (!IOS_TOUCH_SCROLLING) {
874 this._resetScrollPosition(this._scroller.scrollTop - deltaHeight);
875 }
876 }
877 },
878
879 /**
880 * Sets the position of the scroll.
881 */
882 _resetScrollPosition: function(pos) {
883 if (this._scroller) {
884 this._scroller.scrollTop = pos;
885 this._scrollPosition = this._scroller.scrollTop;
886 }
887 },
888
889 /**
890 * Sets the scroll height, that's the height of the content,
891 */
892 _updateScrollerSize: function(forceUpdate) {
893 this._estScrollHeight = (this._physicalBottom +
894 Math.max(this._virtualCount - this._physicalCount - this._virtualStart Val, 0) * this._physicalAverage);
895
896 forceUpdate = forceUpdate || this._scrollHeight === 0;
897 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
898
899 // amortize height adjustment, so it won't trigger repaints very often
900 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
901 this.$.items.style.height = this._estScrollHeight + 'px';
902 this._scrollHeight = this._estScrollHeight;
903 }
904 },
905
906 /**
907 * Scroll to a specific item in the virtual list regardless
908 * of the physical items in the DOM tree.
909 *
910 * @method scrollToIndex
911 * @param {number} idx The index of the item
912 */
913 scrollToIndex: function(idx) {
914 if (typeof idx !== 'number') {
915 return;
916 }
917
918 var itemSet;
919 var firstVisible = this.firstVisibleIndex;
920
921 idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
922
923 // start at the previous virtual item
924 // so we have a item above the first visible item
925 this._virtualStart = idx - 1;
926
927 // assign new models
928 this._assignModels();
929
930 // measure the new sizes
931 this._updateMetrics();
932
933 // estimate new physical offset
934 this._physicalTop = this._virtualStart * this._physicalAverage;
935
936 var currentTopItem = this._physicalStart;
937 var currentVirtualItem = this._virtualStart;
938 var targetOffsetTop = 0;
939 var hiddenContentSize = this._hiddenContentSize;
940
941 // scroll to the item as much as we can
942 while (currentVirtualItem !== idx && targetOffsetTop < hiddenContentSize) {
943 targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem];
944 currentTopItem = (currentTopItem + 1) % this._physicalCount;
945 currentVirtualItem++;
946 }
947
948 // update the scroller size
949 this._updateScrollerSize(true);
950
951 // update the position of the items
952 this._positionItems();
953
954 // set the new scroll position
955 this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1);
956
957 // increase the pool of physical items if needed
958 if (itemSet = this._increasePoolIfNeeded()) {
959 // set models to the new items
960 this.async(this._update.bind(this, itemSet));
961 }
962
963 // clear cached visible index
964 this._firstVisibleIndexVal = null;
965 },
966
967 /**
968 * Reset the physical average and the average count.
969 */
970 _resetAverage: function() {
971 this._physicalAverage = 0;
972 this._physicalAverageCount = 0;
973 },
974
975 /**
976 * A handler for the `resize` event triggered by `IronResizableBehavior`
977 * when the element is resized.
978 */
979 _resizeHandler: function() {
980 this.debounce('resize', function() {
981 this._render();
982 if (this._itemsRendered && this._physicalItems && this._isVisible) {
983 this._resetAverage();
984 this.updateViewportBoundaries();
985 this.scrollToIndex(this.firstVisibleIndex);
986 }
987 });
988 },
989
990 _getModelFromItem: function(item) {
991 var key = this._collection.getKey(item);
992 var pidx = this._physicalIndexForKey[key];
993
994 if (pidx !== undefined) {
995 return this._physicalItems[pidx]._templateInstance;
996 }
997 return null;
998 },
999
1000 /**
1001 * Select the list item at the given index.
1002 *
1003 * @method selectItem
1004 * @param {(Object|number)} item the item object or its index
1005 */
1006 selectItem: function(item) {
1007 if (typeof item === 'number') {
1008 item = this.items[item];
1009 if (!item) {
1010 throw new RangeError('<item> not found');
1011 }
1012 } else {
1013 if (this._collection.getKey(item) === undefined) {
1014 throw new TypeError('<item> should be a valid item');
1015 }
1016 }
1017
1018 var model = this._getModelFromItem(item);
1019
1020 if (!this.multiSelection && this.selectedItem) {
1021 this.deselectItem(this.selectedItem);
1022 }
1023 if (model) {
1024 model[this.selectedAs] = true;
1025 }
1026 this.$.selector.select(item);
1027 },
1028
1029 /**
1030 * Deselects the given item list if it is already selected.
1031 *
1032 * @method deselect
1033 * @param {(Object|number)} item the item object or its index
1034 */
1035 deselectItem: function(item) {
1036 if (typeof item === 'number') {
1037 item = this.items[item];
1038 if (!item) {
1039 throw new RangeError('<item> not found');
1040 }
1041 } else {
1042 if (this._collection.getKey(item) === undefined) {
1043 throw new TypeError('<item> should be a valid item');
1044 }
1045 }
1046
1047 var model = this._getModelFromItem(item);
1048
1049 if (model) {
1050 model[this.selectedAs] = false;
1051 }
1052 this.$.selector.deselect(item);
1053 },
1054
1055 /**
1056 * Select or deselect a given item depending on whether the item
1057 * has already been selected.
1058 *
1059 * @method toggleSelectionForItem
1060 * @param {(Object|number)} item the item object or its index
1061 */
1062 toggleSelectionForItem: function(item) {
1063 var item = typeof item === 'number' ? this.items[item] : item;
1064 if (this.$.selector.isSelected(item)) {
1065 this.deselectItem(item);
1066 } else {
1067 this.selectItem(item);
1068 }
1069 },
1070
1071 /**
1072 * Clears the current selection state of the list.
1073 *
1074 * @method clearSelection
1075 */
1076 clearSelection: function() {
1077 function unselect(item) {
1078 var model = this._getModelFromItem(item);
1079 if (model) {
1080 model[this.selectedAs] = false;
1081 }
1082 }
1083
1084 if (Array.isArray(this.selectedItems)) {
1085 this.selectedItems.forEach(unselect, this);
1086 } else if (this.selectedItem) {
1087 unselect.call(this, this.selectedItem);
1088 }
1089
1090 this.$.selector.clearSelection();
1091 },
1092
1093 /**
1094 * Add an event listener to `tap` if `selectionEnabled` is true,
1095 * it will remove the listener otherwise.
1096 */
1097 _selectionEnabledChanged: function(selectionEnabled) {
1098 if (selectionEnabled) {
1099 this.listen(this, 'tap', '_selectionHandler');
1100 } else {
1101 this.unlisten(this, 'tap', '_selectionHandler');
1102 }
1103 },
1104
1105 /**
1106 * Select an item from an event object.
1107 */
1108 _selectionHandler: function(e) {
1109 var model = this.modelForElement(e.target);
1110 if (model) {
1111 this.toggleSelectionForItem(model[this.as]);
1112 }
1113 },
1114
1115 _multiSelectionChanged: function(multiSelection) {
1116 this.clearSelection();
1117 this.$.selector.multi = multiSelection;
1118 }
1119 });
1120
1121 })();
1122
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698