text: /**
   * @ngdoc module
   * @name material.components.virtualRepeat
   */
  angular.module('material.components.virtualRepeat', [
      'material.core',
      'material.components.showHide'
  ])
      .directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective)
      .directive('mdVirtualRepeat', VirtualRepeatDirective);
  /**
   * @ngdoc directive
   * @name mdVirtualRepeatContainer
   * @module material.components.virtualRepeat
   * @restrict E
   * @description
   * `md-virtual-repeat-container` provides the scroll container for md-virtual-repeat.
   *
   * Virtual repeat is a limited substitute for ng-repeat that renders only
   * enough dom nodes to fill the container and recycling them as the user scrolls.
   *
   * @usage
   * <hljs lang="html">
   *
   * <md-virtual-repeat-container md-top-index="topIndex">
   *   <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
   * </md-virtual-repeat-container>
   * </hljs>
   *
   * @param {number=} md-top-index Binds the index of the item that is at the top of the scroll
   *     container to $scope. It can both read and set the scroll position.
   * @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally
   *     (defaults to orientation and scrolling vertically).
   * @param {boolean=} md-auto-shrink When present, the container will shrink to fit
   *     the number of items when that number is less than its original size.
   * @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink
   *     will shrink to (default: 0).
   */
  function VirtualRepeatContainerDirective() {
      return {
          controller: VirtualRepeatContainerController,
          template: virtualRepeatContainerTemplate,
          compile: function virtualRepeatContainerCompile($element, $attrs) {
              $element
                  .addClass('md-virtual-repeat-container')
                  .addClass($attrs.hasOwnProperty('mdOrientHorizontal')
                      ? 'md-orient-horizontal'
                      : 'md-orient-vertical');
          }
      };
  }
  function virtualRepeatContainerTemplate($element) {
      return '<div class="md-virtual-repeat-scroller">' +
          '<div class="md-virtual-repeat-sizer"></div>' +
          '<div class="md-virtual-repeat-offsetter">' +
          $element[0].innerHTML +
          '</div></div>';
  }
  /**
   * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies
   * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could
   * *compute* this value, but Firefox always reports an element to have a size of zero if it
   * goes over the max, meaning that we'd have to binary search for the value.
   * @const {number}
   */
  var MAX_ELEMENT_SIZE = 1533917;
  /**
   * Number of additional elements to render above and below the visible area inside
   * of the virtual repeat container. A higher number results in less flicker when scrolling
   * very quickly in Safari, but comes with a higher rendering and dirty-checking cost.
   * @const {number}
   */
  var NUM_EXTRA = 3;
  /** @ngInject */
  function VirtualRepeatContainerController($$rAF, $mdUtil, $parse, $rootScope, $window, $scope, $element, $attrs) {
      this.$rootScope = $rootScope;
      this.$scope = $scope;
      this.$element = $element;
      this.$attrs = $attrs;
      /** @type {number} The width or height of the container */
      this.size = 0;
      /** @type {number} The scroll width or height of the scroller */
      this.scrollSize = 0;
      /** @type {number} The scrollLeft or scrollTop of the scroller */
      this.scrollOffset = 0;
      /** @type {boolean} Whether the scroller is oriented horizontally */
      this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal');
      /** @type {!VirtualRepeatController} The repeater inside of this container */
      this.repeater = null;
      /** @type {boolean} Whether auto-shrink is enabled */
      this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink');
      /** @type {number} Minimum number of items to auto-shrink to */
      this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0;
      /** @type {?number} Original container size when shrank */
      this.originalSize = null;
      /** @type {number} Amount to offset the total scroll size by. */
      this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0;
      /** @type {?string} height or width element style on the container prior to auto-shrinking. */
      this.oldElementSize = null;
      if (this.$attrs.mdTopIndex) {
          /** @type {function(angular.Scope): number} Binds to topIndex on Angular scope */
          this.bindTopIndex = $parse(this.$attrs.mdTopIndex);
          /** @type {number} The index of the item that is at the top of the scroll container */
          this.topIndex = this.bindTopIndex(this.$scope);
          if (!angular.isDefined(this.topIndex)) {
              this.topIndex = 0;
              this.bindTopIndex.assign(this.$scope, 0);
          }
          this.$scope.$watch(this.bindTopIndex, angular.bind(this, function (newIndex) {
              if (newIndex !== this.topIndex) {
                  this.scrollToIndex(newIndex);
              }
          }));
      } else {
          this.topIndex = 0;
      }
      this.scroller = $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0];
      this.sizer = this.scroller.getElementsByClassName('md-virtual-repeat-sizer')[0];
      this.offsetter = this.scroller.getElementsByClassName('md-virtual-repeat-offsetter')[0];
      // After the dom stablizes, measure the initial size of the container and
      // make a best effort at re-measuring as it changes.
      var boundUpdateSize = angular.bind(this, this.updateSize);
      $$rAF(angular.bind(this, function () {
          boundUpdateSize();
          var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false);
          var jWindow = angular.element($window);
          // Make one more attempt to get the size if it is 0.
          // This is not by any means a perfect approach, but there's really no
          // silver bullet here.
          if (!this.size) {
              debouncedUpdateSize();
          }
          jWindow.on('resize', debouncedUpdateSize);
          $scope.$on('$destroy', function () {
              jWindow.off('resize', debouncedUpdateSize);
          });
          $scope.$emit('$md-resize-enable');
          $scope.$on('$md-resize', boundUpdateSize);
      }));
  }
  /** Called by the md-virtual-repeat inside of the container at startup. */
  VirtualRepeatContainerController.prototype.register = function (repeaterCtrl) {
      this.repeater = repeaterCtrl;
      angular.element(this.scroller)
          .on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_));
  };
  /** @return {boolean} Whether the container is configured for horizontal scrolling. */
  VirtualRepeatContainerController.prototype.isHorizontal = function () {
      return this.horizontal;
  };
  /** @return {number} The size (width or height) of the container. */
  VirtualRepeatContainerController.prototype.getSize = function () {
      return this.size;
  };
  /**
   * Resizes the container.
   * @private
   * @param {number} size The new size to set.
   */
  VirtualRepeatContainerController.prototype.setSize_ = function (size) {
      var dimension = this.getDimensionName_();
      this.size = size;
      this.$element[0].style[dimension] = size + 'px';
  };
  VirtualRepeatContainerController.prototype.unsetSize_ = function () {
      this.$element[0].style[this.getDimensionName_()] = this.oldElementSize;
      this.oldElementSize = null;
  };
  /** Instructs the container to re-measure its size. */
  VirtualRepeatContainerController.prototype.updateSize = function () {
      // If the original size is already determined, we can skip the update.
      if (this.originalSize) return;
      this.size = this.isHorizontal()
          ? this.$element[0].clientWidth
          : this.$element[0].clientHeight;
      // Recheck the scroll position after updating the size. This resolves
      // problems that can result if the scroll position was measured while the
      // element was display: none or detached from the document.
      this.handleScroll_();
      this.repeater && this.repeater.containerUpdated();
  };
  /** @return {number} The container's scrollHeight or scrollWidth. */
  VirtualRepeatContainerController.prototype.getScrollSize = function () {
      return this.scrollSize;
  };
  VirtualRepeatContainerController.prototype.getDimensionName_ = function () {
      return this.isHorizontal() ? 'width' : 'height';
  };
  /**
   * Sets the scroller element to the specified size.
   * @private
   * @param {number} size The new size.
   */
  VirtualRepeatContainerController.prototype.sizeScroller_ = function (size) {
      var dimension = this.getDimensionName_();
      var crossDimension = this.isHorizontal() ? 'height' : 'width';
      // Clear any existing dimensions.
      this.sizer.innerHTML = '';
      // If the size falls within the browser's maximum explicit size for a single element, we can
      // set the size and be done. Otherwise, we have to create children that add up the the desired
      // size.
      if (size < MAX_ELEMENT_SIZE) {
          this.sizer.style[dimension] = size + 'px';
      } else {
          this.sizer.style[dimension] = 'auto';
          this.sizer.style[crossDimension] = 'auto';
          // Divide the total size we have to render into N max-size pieces.
          var numChildren = Math.floor(size / MAX_ELEMENT_SIZE);
          // Element template to clone for each max-size piece.
          var sizerChild = document.createElement('div');
          sizerChild.style[dimension] = MAX_ELEMENT_SIZE + 'px';
          sizerChild.style[crossDimension] = '1px';
          for (var i = 0; i < numChildren; i++) {
              this.sizer.appendChild(sizerChild.cloneNode(false));
          }
          // Re-use the element template for the remainder.
          sizerChild.style[dimension] = (size - (numChildren * MAX_ELEMENT_SIZE)) + 'px';
          this.sizer.appendChild(sizerChild);
      }
  };
  /**
   * If auto-shrinking is enabled, shrinks or unshrinks as appropriate.
   * @private
   * @param {number} size The new size.
   */
  VirtualRepeatContainerController.prototype.autoShrink_ = function (size) {
      var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize());
      if (this.autoShrink && shrinkSize !== this.size) {
          if (this.oldElementSize === null) {
              this.oldElementSize = this.$element[0].style[this.getDimensionName_()];
          }
          var currentSize = this.originalSize || this.size;
          if (!currentSize || shrinkSize < currentSize) {
              if (!this.originalSize) {
                  this.originalSize = this.size;
              }
              // Now we update the containers size, because shrinking is enabled.
              this.setSize_(shrinkSize);
          } else if (this.originalSize !== null) {
              // Set the size back to our initial size.
              this.unsetSize_();
              var _originalSize = this.originalSize;
              this.originalSize = null;
              // We determine the repeaters size again, if the original size was zero.
              // The originalSize needs to be null, to be able to determine the size.
              if (!_originalSize) this.updateSize();
              // Apply the original size or the determined size back to the container, because
              // it has been overwritten before, in the shrink block.
              this.setSize_(_originalSize || this.size);
          }
          this.repeater.containerUpdated();
      }
  };
  /**
   * Sets the scrollHeight or scrollWidth. Called by the repeater based on
   * its item count and item size.
   * @param {number} itemsSize The total size of the items.
   */
  VirtualRepeatContainerController.prototype.setScrollSize = function (itemsSize) {
      var size = itemsSize + this.offsetSize;
      if (this.scrollSize === size) return;
      this.sizeScroller_(size);
      this.autoShrink_(size);
      this.scrollSize = size;
  };
  /** @return {number} The container's current scroll offset. */
  VirtualRepeatContainerController.prototype.getScrollOffset = function () {
      return this.scrollOffset;
  };
  /**
   * Scrolls to a given scrollTop position.
   * @param {number} position
   */
  VirtualRepeatContainerController.prototype.scrollTo = function (position) {
      this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position;
      this.handleScroll_();
  };
  /**
   * Scrolls the item with the given index to the top of the scroll container.
   * @param {number} index
   */
  VirtualRepeatContainerController.prototype.scrollToIndex = function (index) {
      var itemSize = this.repeater.getItemSize();
      var itemsLength = this.repeater.itemsLength;
      if (index > itemsLength) {
          index = itemsLength - 1;
      }
      this.scrollTo(itemSize * index);
  };
  VirtualRepeatContainerController.prototype.resetScroll = function () {
      this.scrollTo(0);
  };
  VirtualRepeatContainerController.prototype.handleScroll_ = function () {
      var doc = angular.element(document)[0];
      var ltr = doc.dir != 'rtl' && doc.body.dir != 'rtl';
      if (!ltr && !this.maxSize) {
          this.scroller.scrollLeft = this.scrollSize;
          this.maxSize = this.scroller.scrollLeft;
      }
      var offset = this.isHorizontal() ?
          (ltr ? this.scroller.scrollLeft : this.maxSize - this.scroller.scrollLeft)
          : this.scroller.scrollTop;
      if (offset === this.scrollOffset || offset > this.scrollSize - this.size) return;
      var itemSize = this.repeater.getItemSize();
      if (!itemSize) return;
      var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA);
      var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') +
          (!this.isHorizontal() || ltr ? (numItems * itemSize) : -(numItems * itemSize)) + 'px)';
      this.scrollOffset = offset;
      this.offsetter.style.webkitTransform = transform;
      this.offsetter.style.transform = transform;
      if (this.bindTopIndex) {
          var topIndex = Math.floor(offset / itemSize);
          if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) {
              this.topIndex = topIndex;
              this.bindTopIndex.assign(this.$scope, topIndex);
              if (!this.$rootScope.$$phase) this.$scope.$digest();
          }
      }
      this.repeater.containerUpdated();
  };
  /**
   * @ngdoc directive
   * @name mdVirtualRepeat
   * @module material.components.virtualRepeat
   * @restrict A
   * @priority 1000
   * @description
   * `md-virtual-repeat` specifies an element to repeat using virtual scrolling.
   *
   * Virtual repeat is a limited substitute for ng-repeat that renders only
   * enough dom nodes to fill the container and recycling them as the user scrolls.
   * Arrays, but not objects are supported for iteration.
   * Track by, as alias, and (key, value) syntax are not supported.
   *
   * @usage
   * <hljs lang="html">
   * <md-virtual-repeat-container>
   *   <div md-virtual-repeat="i in items">Hello {{i}}!</div>
   * </md-virtual-repeat-container>
   *
   * <md-virtual-repeat-container md-orient-horizontal>
   *   <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
   * </md-virtual-repeat-container>
   * </hljs>
   *
   * @param {number=} md-item-size The height or width of the repeated elements (which must be
   *   identical for each element). Optional. Will attempt to read the size from the dom if missing,
   *   but still assumes that all repeated nodes have same height or width.
   * @param {string=} md-extra-name Evaluates to an additional name to which the current iterated item
   *   can be assigned on the repeated scope (needed for use in `md-autocomplete`).
   * @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument as an object
   *   that can fetch rows rather than an array.
   *
   *   **NOTE:** This object must implement the following interface with two (2) methods:
   *
   *   - `getItemAtIndex: function(index) [object]` The item at that index or null if it is not yet
   *     loaded (it should start downloading the item in that case).
   *   - `getLength: function() [number]` The data length to which the repeater container
   *     should be sized. Ideally, when the count is known, this method should return it.
   *     Otherwise, return a higher number than the currently loaded items to produce an
   *     infinite-scroll behavior.
   */
  function VirtualRepeatDirective($parse) {
      return {
          controller: VirtualRepeatController,
          priority: 1000,
          require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'],
          restrict: 'A',
          terminal: true,
          transclude: 'element',
          compile: function VirtualRepeatCompile($element, $attrs) {
              var expression = $attrs.mdVirtualRepeat;
              var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);
              var repeatName = match[1];
              var repeatListExpression = $parse(match[2]);
              var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName);
              return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) {
                  ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName);
              };
          }
      };
  }
  /** @ngInject */
  function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope, $$rAF, $mdUtil) {
      this.$scope = $scope;
      this.$element = $element;
      this.$attrs = $attrs;
      this.$browser = $browser;
      this.$document = $document;
      this.$rootScope = $rootScope;
      this.$$rAF = $$rAF;
      /** @type {boolean} Whether we are in on-demand mode. */
      this.onDemand = $mdUtil.parseAttributeBoolean($attrs.mdOnDemand);
      /** @type {!Function} Backup reference to $browser.$$checkUrlChange */
      this.browserCheckUrlChange = $browser.$$checkUrlChange;
      /** @type {number} Most recent starting repeat index (based on scroll offset) */
      this.newStartIndex = 0;
      /** @type {number} Most recent ending repeat index (based on scroll offset) */
      this.newEndIndex = 0;
      /** @type {number} Most recent end visible index (based on scroll offset) */
      this.newVisibleEnd = 0;
      /** @type {number} Previous starting repeat index (based on scroll offset) */
      this.startIndex = 0;
      /** @type {number} Previous ending repeat index (based on scroll offset) */
      this.endIndex = 0;
      // TODO: measure width/height of first element from dom if not provided.
      // getComputedStyle?
      /** @type {?number} Height/width of repeated elements. */
      this.itemSize = $scope.$eval($attrs.mdItemSize) || null;
      /** @type {boolean} Whether this is the first time that items are rendered. */
      this.isFirstRender = true;
      /**
       * @private {boolean} Whether the items in the list are already being updated. Used to prevent
       *     nested calls to virtualRepeatUpdate_.
       */
      this.isVirtualRepeatUpdating_ = false;
      /** @type {number} Most recently seen length of items. */
      this.itemsLength = 0;
      /**
       * @type {!Function} Unwatch callback for item size (when md-items-size is
       *     not specified), or angular.noop otherwise.
       */
      this.unwatchItemSize_ = angular.noop;
      /**
       * Presently rendered blocks by repeat index.
       * @type {Object<number, !VirtualRepeatController.Block}
       */
      this.blocks = {};
      /** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */
      this.pooledBlocks = [];
      $scope.$on('$destroy', angular.bind(this, this.cleanupBlocks_));
  }
  /**
   * An object representing a repeated item.
   * @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}}
   */
  VirtualRepeatController.Block;
  /**
   * Called at startup by the md-virtual-repeat postLink function.
   * @param {!VirtualRepeatContainerController} container The container's controller.
   * @param {!Function} transclude The repeated element's bound transclude function.
   * @param {string} repeatName The left hand side of the repeat expression, indicating
   *     the name for each item in the array.
   * @param {!Function} repeatListExpression A compiled expression based on the right hand side
   *     of the repeat expression. Points to the array to repeat over.
   * @param {string|undefined} extraName The optional extra repeatName.
   */
  VirtualRepeatController.prototype.link_ =
      function (container, transclude, repeatName, repeatListExpression, extraName) {
          this.container = container;
          this.transclude = transclude;
          this.repeatName = repeatName;
          this.rawRepeatListExpression = repeatListExpression;
          this.extraName = extraName;
          this.sized = false;
          this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
          this.container.register(this);
      };
  /** @private Cleans up unused blocks. */
  VirtualRepeatController.prototype.cleanupBlocks_ = function () {
      angular.forEach(this.pooledBlocks, function cleanupBlock(block) {
          block.element.remove();
      });
  };
  /** @private Attempts to set itemSize by measuring a repeated element in the dom */
  VirtualRepeatController.prototype.readItemSize_ = function () {
      if (this.itemSize) {
          // itemSize was successfully read in a different asynchronous call.
          return;
      }
      this.items = this.repeatListExpression(this.$scope);
      this.parentNode = this.$element[0].parentNode;
      var block = this.getBlock_(0);
      if (!block.element[0].parentNode) {
          this.parentNode.appendChild(block.element[0]);
      }
      this.itemSize = block.element[0][
          this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null;
      this.blocks[0] = block;
      this.poolBlock_(0);
      if (this.itemSize) {
          this.containerUpdated();
      }
  };
  /**
   * Returns the user-specified repeat list, transforming it into an array-like
   * object in the case of infinite scroll/dynamic load mode.
   * @param {!angular.Scope} The scope.
   * @return {!Array|!Object} An array or array-like object for iteration.
   */
  VirtualRepeatController.prototype.repeatListExpression_ = function (scope) {
      var repeatList = this.rawRepeatListExpression(scope);
      if (this.onDemand && repeatList) {
          var virtualList = new VirtualRepeatModelArrayLike(repeatList);
          virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
          return virtualList;
      } else {
          return repeatList;
      }
  };
  /**
   * Called by the container. Informs us that the containers scroll or size has
   * changed.
   */
  VirtualRepeatController.prototype.containerUpdated = function () {
      // If itemSize is unknown, attempt to measure it.
      if (!this.itemSize) {
          // Make sure to clean up watchers if we can (see #8178)
          if (this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop) {
              this.unwatchItemSize_();
          }
          this.unwatchItemSize_ = this.$scope.$watchCollection(
              this.repeatListExpression,
              angular.bind(this, function (items) {
                  if (items && items.length) {
                      this.$$rAF(angular.bind(this, this.readItemSize_));
                  }
              }));
          if (!this.$rootScope.$$phase) this.$scope.$digest();
          return;
      } else if (!this.sized) {
          this.items = this.repeatListExpression(this.$scope);
      }
      if (!this.sized) {
          this.unwatchItemSize_();
          this.sized = true;
          this.$scope.$watchCollection(this.repeatListExpression,
              angular.bind(this, function (items, oldItems) {
                  if (!this.isVirtualRepeatUpdating_) {
                      this.virtualRepeatUpdate_(items, oldItems);
                  }
              }));
      }
      this.updateIndexes_();
      if (this.newStartIndex !== this.startIndex ||
          this.newEndIndex !== this.endIndex ||
          this.container.getScrollOffset() > this.container.getScrollSize()) {
          if (this.items instanceof VirtualRepeatModelArrayLike) {
              this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
          }
          this.virtualRepeatUpdate_(this.items, this.items);
      }
  };
  /**
   * Called by the container. Returns the size of a single repeated item.
   * @return {?number} Size of a repeated item.
   */
  VirtualRepeatController.prototype.getItemSize = function () {
      return this.itemSize;
  };
  /**
   * Called by the container. Returns the size of a single repeated item.
   * @return {?number} Size of a repeated item.
   */
  VirtualRepeatController.prototype.getItemCount = function () {
      return this.itemsLength;
  };
  /**
   * Updates the order and visible offset of repeated blocks in response to scrolling
   * or items updates.
   * @private
   */
  VirtualRepeatController.prototype.virtualRepeatUpdate_ = function (items, oldItems) {
      this.isVirtualRepeatUpdating_ = true;
      var itemsLength = items && items.length || 0;
      var lengthChanged = false;
      // If the number of items shrank
      if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
          this.items = items;
          var previousScrollOffset = this.container.getScrollOffset();
          this.container.resetScroll();
          this.container.scrollTo(previousScrollOffset);
          return;
      }
      if (itemsLength !== this.itemsLength) {
          lengthChanged = true;
          this.itemsLength = itemsLength;
      }
      this.items = items;
      if (items !== oldItems || lengthChanged) {
          this.updateIndexes_();
      }
      this.parentNode = this.$element[0].parentNode;
      if (lengthChanged) {
          this.container.setScrollSize(itemsLength * this.itemSize);
      }
      if (this.isFirstRender) {
          this.isFirstRender = false;
          var startIndex = this.$attrs.mdStartIndex ?
              this.$scope.$eval(this.$attrs.mdStartIndex) :
              this.container.topIndex;
          this.container.scrollToIndex(startIndex);
      }
      // Detach and pool any blocks that are no longer in the viewport.
      Object.keys(this.blocks).forEach(function (blockIndex) {
          var index = parseInt(blockIndex, 10);
          if (index < this.newStartIndex || index >= this.newEndIndex) {
              this.poolBlock_(index);
          }
      }, this);
      // Add needed blocks.
      // For performance reasons, temporarily block browser url checks as we digest
      // the restored block scopes ($$checkUrlChange reads window.location to
      // check for changes and trigger route change, etc, which we don't need when
      // trying to scroll at 60fps).
      this.$browser.$$checkUrlChange = angular.noop;
      var i, block,
          newStartBlocks = [],
          newEndBlocks = [];
      // Collect blocks at the top.
      for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) {
          block = this.getBlock_(i);
          this.updateBlock_(block, i);
          newStartBlocks.push(block);
      }
      // Update blocks that are already rendered.
      for (; this.blocks[i] != null; i++) {
          this.updateBlock_(this.blocks[i], i);
      }
      var maxIndex = i - 1;
      // Collect blocks at the end.
      for (; i < this.newEndIndex; i++) {
          block = this.getBlock_(i);
          this.updateBlock_(block, i);
          newEndBlocks.push(block);
      }
      // Attach collected blocks to the document.
      if (newStartBlocks.length) {
          this.parentNode.insertBefore(
              this.domFragmentFromBlocks_(newStartBlocks),
              this.$element[0].nextSibling);
      }
      if (newEndBlocks.length) {
          this.parentNode.insertBefore(
              this.domFragmentFromBlocks_(newEndBlocks),
                  this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling);
      }
      // Restore $$checkUrlChange.
      this.$browser.$$checkUrlChange = this.browserCheckUrlChange;
      this.startIndex = this.newStartIndex;
      this.endIndex = this.newEndIndex;
      this.isVirtualRepeatUpdating_ = false;
  };
  /**
   * @param {number} index Where the block is to be in the repeated list.
   * @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index.
   * @private
   */
  VirtualRepeatController.prototype.getBlock_ = function (index) {
      if (this.pooledBlocks.length) {
          return this.pooledBlocks.pop();
      }
      var block;
      this.transclude(angular.bind(this, function (clone, scope) {
          block = {
              element: clone,
              new: true,
              scope: scope
          };
          this.updateScope_(scope, index);
          this.parentNode.appendChild(clone[0]);
      }));
      return block;
  };
  /**
   * Updates and if not in a digest cycle, digests the specified block's scope to the data
   * at the specified index.
   * @param {!VirtualRepeatController.Block} block The block whose scope should be updated.
   * @param {number} index The index to set.
   * @private
   */
  VirtualRepeatController.prototype.updateBlock_ = function (block, index) {
      this.blocks[index] = block;
      if (!block.new &&
          (block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) {
          return;
      }
      block.new = false;
      // Update and digest the block's scope.
      this.updateScope_(block.scope, index);
      // Perform digest before reattaching the block.
      // Any resulting synchronous dom mutations should be much faster as a result.
      // This might break some directives, but I'm going to try it for now.
      if (!this.$rootScope.$$phase) {
          block.scope.$digest();
      }
  };
  /**
   * Updates scope to the data at the specified index.
   * @param {!angular.Scope} scope The scope which should be updated.
   * @param {number} index The index to set.
   * @private
   */
  VirtualRepeatController.prototype.updateScope_ = function (scope, index) {
      scope.$index = index;
      scope[this.repeatName] = this.items && this.items[index];
      if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index];
  };
  /**
   * Pools the block at the specified index (Pulls its element out of the dom and stores it).
   * @param {number} index The index at which the block to pool is stored.
   * @private
   */
  VirtualRepeatController.prototype.poolBlock_ = function (index) {
      this.pooledBlocks.push(this.blocks[index]);
      this.parentNode.removeChild(this.blocks[index].element[0]);
      delete this.blocks[index];
  };
  /**
   * Produces a dom fragment containing the elements from the list of blocks.
   * @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements
   *     should be added to the document fragment.
   * @return {DocumentFragment}
   * @private
   */
  VirtualRepeatController.prototype.domFragmentFromBlocks_ = function (blocks) {
      var fragment = this.$document[0].createDocumentFragment();
      blocks.forEach(function (block) {
          fragment.appendChild(block.element[0]);
      });
      return fragment;
  };
  /**
   * Updates start and end indexes based on length of repeated items and container size.
   * @private
   */
  VirtualRepeatController.prototype.updateIndexes_ = function () {
      var itemsLength = this.items ? this.items.length : 0;
      var containerLength = Math.ceil(this.container.getSize() / this.itemSize);
      this.newStartIndex = Math.max(0, Math.min(
              itemsLength - containerLength,
          Math.floor(this.container.getScrollOffset() / this.itemSize)));
      this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
      this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
      this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
  };
  /**
   * This VirtualRepeatModelArrayLike class enforces the interface requirements
   * for infinite scrolling within a mdVirtualRepeatContainer. An object with this
   * interface must implement the following interface with two (2) methods:
   *
   * getItemAtIndex: function(index) -> item at that index or null if it is not yet
   *     loaded (It should start downloading the item in that case).
   *
   * getLength: function() -> number The data legnth to which the repeater container
   *     should be sized. Ideally, when the count is known, this method should return it.
   *     Otherwise, return a higher number than the currently loaded items to produce an
   *     infinite-scroll behavior.
   *
   * @usage
   * <hljs lang="html">
   *  <md-virtual-repeat-container md-orient-horizontal>
   *    <div md-virtual-repeat="i in items" md-on-demand>
   *      Hello {{i}}!
   *    </div>
   *  </md-virtual-repeat-container>
   * </hljs>
   *
   */
  function VirtualRepeatModelArrayLike(model) {
      if (!angular.isFunction(model.getItemAtIndex) || !angular.isFunction(model.getLength)) {
          throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' +
              'functions getItemAtIndex() and getLength() ');
      }
      this.model = model;
  }
  VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function (start, end) {
      for (var i = start; i < end; i++) {
          if (!this.hasOwnProperty(i)) {
              this[i] = this.model.getItemAtIndex(i);
          }
      }
      this.length = this.model.getLength();
  };
  function abstractMethod() {
      throw Error('Non-overridden abstract method called.');
  }
  