text: /*!
   * Angular Material Design
   * https://github.com/angular/material
   * @license MIT
   * v0.11.0-rc1-master-d74f93a
   */
  (function( window, angular, undefined ){
  "use strict";
  
  /**
   * @ngdoc module
   * @name material.components.virtualRepeat
   */
  angular.module('material.components.virtualRepeat', [
    'material.core'
  ])
  .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>
   *   <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
   * </md-virtual-repeat-container>
   * </hljs>
   *
   * @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, $scope, $element, $attrs) {
    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;
  
  
    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];
  
    $$rAF(angular.bind(this, this.updateSize));
  
    // TODO: Come up with a more robust (But hopefully also quick!) way of
    // detecting that we're not visible.
    if ($attrs.ngHide) {
      $scope.$watch($attrs.ngHide, angular.bind(this, function(hidden) {
        if (!hidden) {
          $$rAF(angular.bind(this, this.updateSize));
        }
      }));
    }
  }
  VirtualRepeatContainerController.$inject = ["$$rAF", "$scope", "$element", "$attrs"];
  
  
  /** 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} The new size to set.
   */
  VirtualRepeatContainerController.prototype.setSize_ = function(size) {
    this.size = size;
    this.$element[0].style[this.isHorizontal() ? 'width' : 'height'] = size + 'px';
  };
  
  
  /** Instructs the container to re-measure its size. */
  VirtualRepeatContainerController.prototype.updateSize = function() {
    if (this.originalSize) return;
  
    this.size = this.isHorizontal()
        ? this.$element[0].clientWidth
        : this.$element[0].clientHeight;
    this.repeater && this.repeater.containerUpdated();
  };
  
  
  /** @return {number} The container's scrollHeight or scrollWidth. */
  VirtualRepeatContainerController.prototype.getScrollSize = function() {
    return this.scrollSize;
  };
  
  
  /**
   * Sets the scroller element to the specified size.
   * @private
   * @param {number} size The new size.
   */
  VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) {
    var dimension =  this.isHorizontal() ? 'width' : 'height';
    var crossDimension = this.isHorizontal() ? 'height' : 'width';
  
    // 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 {
      // Clear any existing dimensions.
      this.sizer.innerHTML = '';
      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 (shrinkSize < (this.originalSize || this.size)) {
        if (!this.originalSize) {
          this.originalSize = this.size;
        }
  
        this.setSize_(shrinkSize);
      } else if (this.originalSize) {
        this.setSize_(this.originalSize);
        this.originalSize = null;
      }
    }
  };
  
  
  /**
   * 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_();
  };
  
  VirtualRepeatContainerController.prototype.resetScroll = function() {
    this.scrollTo(0);
  };
  
  
  VirtualRepeatContainerController.prototype.handleScroll_ = function() {
    var offset = this.isHorizontal() ? this.scroller.scrollLeft : this.scroller.scrollTop;
    if (offset === this.scrollOffset) 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(';
        transform +=  (numItems * itemSize) + 'px)';
  
    this.scrollOffset = offset;
    this.offsetter.style.webkitTransform = transform;
    this.offsetter.style.transform = transform;
  
    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) -> 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.
   */
  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);
        };
      }
    };
  }
  VirtualRepeatDirective.$inject = ["$parse"];
  
  
  /** ngInject */
  function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $$rAF) {
    this.$scope = $scope;
    this.$element = $element;
    this.$attrs = $attrs;
    this.$browser = $browser;
    this.$document = $document;
    this.$$rAF = $$rAF;
  
    /** @type {boolean} Whether we are in on-demand mode. */
    this.onDemand = $attrs.hasOwnProperty('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;
  
    /** @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 = [];
  }
  VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$$rAF"];
  
  
  /**
   * 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 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) {
      this.unwatchItemSize_ = this.$scope.$watchCollection(
          this.repeatListExpression,
          angular.bind(this, function(items) {
            if (items && items.length) {
              this.$$rAF(angular.bind(this, this.readItemSize_));
            }
          }));
      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, this.virtualRepeatUpdate_));
    }
  
    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;
  };
  
  
  /**
   * Updates the order and visible offset of repeated blocks in response to scrolling
   * or items updates.
   * @private
   */
  VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) {
    var itemsLength = items ? items.length : 0;
    var lengthChanged = false;
  
    if (itemsLength !== this.itemsLength) {
      lengthChanged = true;
      this.itemsLength = itemsLength;
    }
  
    // If the number of items shrank, scroll up to the top.
    if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
      this.items = items;
      this.container.resetScroll();
      return;
    }
  
    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) : 0;
      this.container.scrollTo(startIndex * this.itemSize);
    }
  
    // 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;
  };
  
  
  /**
   * @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.$scope.$root.$$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.');
  }
  
  })(window, window.angular);