From 0d44313a4bc338ef82fc5411834fd4c58ff0a6e0 Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Thu, 8 Aug 2013 17:16:07 -0400
Subject: [PATCH] Use Ember.ListView for blocked emails list

---
 app/assets/javascripts/admin.js               |    1 +
 .../admin_logs_blocked_emails_controller.js   |    1 +
 .../logs/blocked_emails.js.handlebars         |   33 +-
 .../blocked_emails_list_item.js.handlebars    |    6 +
 .../views/logs/blocked_emails_list_view.js    |    5 +
 app/assets/stylesheets/admin/admin_base.scss  |   38 +-
 .../admin/blocked_emails_controller.rb        |    2 +-
 vendor/assets/javascripts/list_view.js        | 1189 +++++++++++++++++
 8 files changed, 1252 insertions(+), 23 deletions(-)
 create mode 100644 app/assets/javascripts/admin/templates/logs/blocked_emails_list_item.js.handlebars
 create mode 100644 app/assets/javascripts/admin/views/logs/blocked_emails_list_view.js
 create mode 100755 vendor/assets/javascripts/list_view.js

diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 06ada5e09..ddcc17a81 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1 +1,2 @@
+//= require list_view.js
 //= require_tree ./admin
diff --git a/app/assets/javascripts/admin/controllers/admin_logs_blocked_emails_controller.js b/app/assets/javascripts/admin/controllers/admin_logs_blocked_emails_controller.js
index d667a2ad1..2ff1b165d 100644
--- a/app/assets/javascripts/admin/controllers/admin_logs_blocked_emails_controller.js
+++ b/app/assets/javascripts/admin/controllers/admin_logs_blocked_emails_controller.js
@@ -8,6 +8,7 @@
 **/
 Discourse.AdminLogsBlockedEmailsController = Ember.ArrayController.extend(Discourse.Presence, {
   loading: false,
+  content: [],
 
   show: function() {
     var self = this;
diff --git a/app/assets/javascripts/admin/templates/logs/blocked_emails.js.handlebars b/app/assets/javascripts/admin/templates/logs/blocked_emails.js.handlebars
index b7373efb1..f5e48aaf7 100644
--- a/app/assets/javascripts/admin/templates/logs/blocked_emails.js.handlebars
+++ b/app/assets/javascripts/admin/templates/logs/blocked_emails.js.handlebars
@@ -2,27 +2,20 @@
   <div class='admin-loading'>{{i18n loading}}</div>
 {{else}}
   {{#if model.length}}
-    <table class='table blocked-emails'>
-      <thead>
-        <th class="email">{{i18n admin.logs.blocked_emails.email}}</th>
-        <th class="action">{{i18n admin.logs.action}}</th>
-        <th class="match_count">{{i18n admin.logs.blocked_emails.match_count}}</th>
-        <th class="last_match_at">{{i18n admin.logs.blocked_emails.last_match_at}}</th>
-        <th class="created_at">{{i18n admin.logs.created_at}}</th>
-      </thead>
 
-      <tbody>
-        {{#each model}}
-          <tr>
-            <td class="email">{{email}}</td>
-            <td class="action">{{actionName}}</td>
-            <td class="match_count">{{match_count}}</td>
-            <td class="last_match_at">{{unboundAgeWithTooltip last_match_at}}</td>
-            <td class="created_at">{{unboundAgeWithTooltip created_at}}</td>
-          </tr>
-        {{/each}}
-      </tbody>
-    </table>
+    <div class='table blocked-emails'>
+      <div class="heading-container">
+        <div class="col heading email">{{i18n admin.logs.blocked_emails.email}}</div>
+        <div class="col heading action">{{i18n admin.logs.action}}</div>
+        <div class="col heading match_count">{{i18n admin.logs.blocked_emails.match_count}}</div>
+        <div class="col heading last_match_at">{{i18n admin.logs.blocked_emails.last_match_at}}</div>
+        <div class="col heading created_at">{{i18n admin.logs.created_at}}</div>
+        <div class="clearfix"></div>
+      </div>
+
+      {{view Discourse.BlockedEmailsListView contentBinding="controller"}}
+    </div>
+
   {{else}}
     {{i18n search.no_results}}
   {{/if}}
diff --git a/app/assets/javascripts/admin/templates/logs/blocked_emails_list_item.js.handlebars b/app/assets/javascripts/admin/templates/logs/blocked_emails_list_item.js.handlebars
new file mode 100644
index 000000000..4e3640796
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/logs/blocked_emails_list_item.js.handlebars
@@ -0,0 +1,6 @@
+<div class="col email">{{email}}</div>
+<div class="col action">{{actionName}}</div>
+<div class="col match_count">{{match_count}}</div>
+<div class="col last_match_at">{{unboundAgeWithTooltip last_match_at}}</div>
+<div class="col created_at">{{unboundAgeWithTooltip created_at}}</div>
+<div class="clearfix"></div>
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/views/logs/blocked_emails_list_view.js b/app/assets/javascripts/admin/views/logs/blocked_emails_list_view.js
new file mode 100644
index 000000000..102547afe
--- /dev/null
+++ b/app/assets/javascripts/admin/views/logs/blocked_emails_list_view.js
@@ -0,0 +1,5 @@
+Discourse.BlockedEmailsListView = Ember.ListView.extend({
+  height: 500,
+  rowHeight: 32,
+  itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/blocked_emails_list_item"})
+});
diff --git a/app/assets/stylesheets/admin/admin_base.scss b/app/assets/stylesheets/admin/admin_base.scss
index 87a88021e..d6b8ee413 100644
--- a/app/assets/stylesheets/admin/admin_base.scss
+++ b/app/assets/stylesheets/admin/admin_base.scss
@@ -694,12 +694,35 @@ table {
   }
 }
 
-/* Logs */
+// Logs
 
 .blocked-emails {
-  .match_count, .last_match_at, .created_at {
+  width: 900px;
+  margin-left: 5px;
+  border-bottom: dotted 1px #ddd;
+  .heading-container {
+    width: 100%;
+    background-color: #e4e4e4;
+  }
+  .heading {
+    font-weight: bold;
+  }
+  .col {
+    display: inline-block;
+    padding-top: 6px;
+  }
+  .email {
+    width: 400px;
+    margin-left: 5px;
+  }
+  .action, .match_count, .last_match_at, .created_at {
+    width: 110px;
     text-align: center;
   }
+  .ember-list-item-view {
+    width: 100%;
+    border-top: solid 1px #ddd;
+  }
 }
 
 .staff-actions {
@@ -719,3 +742,14 @@ table {
     }
   }
 }
+
+// Ember.ListView
+
+.ember-list-view {
+  overflow-y: auto;
+  overflow-x: hidden;
+  position: relative;
+}
+.ember-list-item-view {
+  position: absolute;
+}
diff --git a/app/controllers/admin/blocked_emails_controller.rb b/app/controllers/admin/blocked_emails_controller.rb
index 2ef690709..866672f3f 100644
--- a/app/controllers/admin/blocked_emails_controller.rb
+++ b/app/controllers/admin/blocked_emails_controller.rb
@@ -1,7 +1,7 @@
 class Admin::BlockedEmailsController < Admin::AdminController
 
   def index
-    blocked_emails = BlockedEmail.limit(50).order('last_match_at desc').to_a
+    blocked_emails = BlockedEmail.limit(200).order('last_match_at desc').to_a
     render_serialized(blocked_emails, BlockedEmailSerializer)
   end
 
diff --git a/vendor/assets/javascripts/list_view.js b/vendor/assets/javascripts/list_view.js
new file mode 100755
index 000000000..af923982d
--- /dev/null
+++ b/vendor/assets/javascripts/list_view.js
@@ -0,0 +1,1189 @@
+(function() {
+var get = Ember.get, set = Ember.set;
+
+function samePosition(a, b) {
+  return a && b && a.x === b.x && a.y === b.y;
+}
+
+function positionElement() {
+  var element, position, _position;
+
+  Ember.instrument('view.updateContext.positionElement', this, function() {
+    element = get(this, 'element');
+    position = get(this, 'position');
+    _position = this._position;
+
+    if (!position || !element) { return; }
+
+    // TODO: avoid needing this by avoiding unnecessary
+    // calls to this method in the first place
+    if (samePosition(position, _position)) { return; }
+    this._parentView.applyTransform(element, position);
+
+    this._position = position;
+  }, this);
+}
+
+Ember.ListItemViewMixin = Ember.Mixin.create({
+  init: function(){
+    this._super();
+    this.one('didInsertElement', positionElement);
+  },
+  classNames: ['ember-list-item-view'],
+  _position: null,
+  _positionDidChange: Ember.observer(positionElement, 'position'),
+  _positionElement: positionElement
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+var backportedInnerString = function(buffer) {
+  var content = [], childBuffers = buffer.childBuffers;
+
+  Ember.ArrayPolyfills.forEach.call(childBuffers, function(buffer) {
+    var stringy = typeof buffer === 'string';
+    if (stringy) {
+      content.push(buffer);
+    } else {
+      buffer.array(content);
+    }
+  });
+
+  return content.join('');
+};
+
+function willInsertElementIfNeeded(view) {
+  if (view.willInsertElement) {
+    view.willInsertElement();
+  }
+}
+
+function didInsertElementIfNeeded(view) {
+  if (view.didInsertElement) {
+    view.didInsertElement();
+  }
+}
+
+function rerender() {
+  var element, buffer, context, hasChildViews;
+  element = get(this, 'element');
+
+  if (!element) { return; }
+
+  context = get(this, 'context');
+
+  // releases action helpers in contents
+  // this means though that the ListViewItem itself can't use classBindings or attributeBindings
+  // need support for rerender contents in ember
+  this.triggerRecursively('willClearRender');
+
+  if (this.lengthAfterRender > this.lengthBeforeRender) {
+    this.clearRenderedChildren();
+    this._childViews.length = this.lengthBeforeRender; // triage bug in ember
+  }
+
+  if (context) {
+    buffer = Ember.RenderBuffer();
+    buffer = this.renderToBuffer(buffer);
+
+    // check again for childViews, since rendering may have added some
+    hasChildViews = this._childViews.length > 0;
+
+    if (hasChildViews) {
+      this.invokeRecursively(willInsertElementIfNeeded, false);
+    }
+
+    element.innerHTML = buffer.innerString ? buffer.innerString() : backportedInnerString(buffer);
+
+    set(this, 'element', element);
+
+    this.transitionTo('inDOM');
+
+    if (hasChildViews) {
+      this.invokeRecursively(didInsertElementIfNeeded, false);
+    }
+  } else {
+    element.innerHTML = ''; // when there is no context, this view should be completely empty
+  }
+}
+
+/**
+  The `Ember.ListViewItem` view class renders a
+  [div](https://developer.mozilla.org/en/HTML/Element/div) HTML element
+  with `ember-list-item-view` class. It allows you to specify a custom item
+  handlebars template for `Ember.ListView`.
+
+  Example:
+
+  ```handlebars
+  <script type="text/x-handlebars" data-template-name="row_item">
+    {{name}}
+  </script>
+  ```
+
+  ```javascript
+  App.ListView = Ember.ListView.extend({
+    height: 500,
+    rowHeight: 20,
+    itemViewClass: Ember.ListItemView.extend({templateName: "row_item"})
+  });
+  ```
+
+  @extends Ember.View
+  @class ListItemView
+  @namespace Ember
+*/
+Ember.ListItemView = Ember.View.extend(Ember.ListItemViewMixin, {
+  updateContext: function(newContext){
+    var context = get(this, 'context');
+    Ember.instrument('view.updateContext.render', this, function() {
+      if (context !== newContext) {
+        this.set('context', newContext);
+        if (newContext instanceof Ember.ObjectController) {
+          this.set('controller', newContext);
+        }
+      }
+    }, this);
+  },
+  rerender: function () { Ember.run.scheduleOnce('render', this, rerender); },
+  _contextDidChange: Ember.observer(rerender, 'context', 'controller')
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+Ember.ReusableListItemView = Ember.View.extend(Ember.ListItemViewMixin, {
+  init: function(){
+    this._super();
+    this.set('context', Ember.ObjectProxy.create());
+  },
+  isVisible: Ember.computed('context.content', function(){
+    return !!this.get('context.content');
+  }),
+  updateContext: function(newContext){
+    var context = get(this, 'context.content');
+    if (context !== newContext) {
+      if (this.state === 'inDOM') {
+        this.prepareForReuse(newContext);
+      }
+      set(this, 'context.content', newContext);
+    }
+  },
+  prepareForReuse: Ember.K
+});
+
+})();
+
+
+
+(function() {
+Ember.ListViewHelper = {
+  applyTransform: (function(){
+    var element = document.createElement('div');
+
+    if ('webkitTransform' in element.style){
+      return function(element, position){
+        var x = position.x,
+            y = position.y;
+
+        element.style.webkitTransform = 'translate3d(' + x + 'px, ' + y + 'px, 0)';
+      };
+    }else{
+      return function(element, position){
+        var x = position.x,
+            y = position.y;
+
+        element.style.top =  y + 'px';
+        element.style.left = x + 'px';
+      };
+    }
+  })()
+};
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set,
+min = Math.min, max = Math.max, floor = Math.floor,
+ceil = Math.ceil,
+forEach = Ember.ArrayPolyfills.forEach;
+
+function addContentArrayObserver() {
+  var content = get(this, 'content');
+  if (content) {
+    content.addArrayObserver(this);
+  }
+}
+
+function removeAndDestroy(object){
+  this.removeObject(object);
+  object.destroy();
+}
+
+function syncChildViews(){
+  Ember.run.once(this, '_syncChildViews');
+}
+
+function sortByContentIndex (viewOne, viewTwo){
+  return get(viewOne, 'contentIndex') - get(viewTwo, 'contentIndex');
+}
+
+function detectListItemViews(childView) {
+  return Ember.ListItemViewMixin.detect(childView);
+}
+
+function notifyMutationListeners() {
+  if (Ember.View.notifyMutationListeners) {
+    Ember.run.once(Ember.View, 'notifyMutationListeners');
+  }
+}
+
+var domManager = Ember.create(Ember.ContainerView.proto().domManager);
+
+domManager.prepend = function(view, html) {
+  view.$('.ember-list-container').prepend(html);
+  notifyMutationListeners();
+};
+
+function syncListContainerWidth(){
+  var elementWidth, columnCount, containerWidth, element;
+
+  elementWidth = get(this, 'elementWidth');
+  columnCount = get(this, 'columnCount');
+  containerWidth = elementWidth * columnCount;
+  element = this.$('.ember-list-container');
+
+  if (containerWidth && element) {
+    element.css('width', containerWidth);
+  }
+}
+
+function enableProfilingOutput() {
+  function before(name, time, payload) {
+    console.time(name);
+  }
+
+  function after (name, time, payload) {
+    console.timeEnd(name);
+  }
+
+  if (Ember.ENABLE_PROFILING) {
+    Ember.subscribe('view._scrollContentTo', {
+      before: before,
+      after: after
+    });
+    Ember.subscribe('view.updateContext', {
+      before: before,
+      after: after
+    });
+  }
+}
+
+/**
+  @class Ember.ListViewMixin
+  @namespace Ember
+*/
+Ember.ListViewMixin = Ember.Mixin.create({
+  itemViewClass: Ember.ListItemView,
+  classNames: ['ember-list-view'],
+  attributeBindings: ['style'],
+  domManager: domManager,
+  scrollTop: 0,
+  bottomPadding: 0,
+  _lastEndingIndex: 0,
+  paddingCount: 1,
+
+  /**
+    @private
+
+    Setup a mixin.
+    - adding observer to content array
+    - creating child views based on height and length of the content array
+
+    @method init
+  */
+  init: function() {
+    this._super();
+    enableProfilingOutput();
+    addContentArrayObserver.call(this);
+    this._syncChildViews();
+    this.columnCountDidChange();
+    this.on('didInsertElement', syncListContainerWidth);
+  },
+
+  /**
+    Called on your view when it should push strings of HTML into a
+    `Ember.RenderBuffer`.
+
+    Adds a [div](https://developer.mozilla.org/en-US/docs/HTML/Element/div)
+    with a required `ember-list-container` class.
+
+    @method render
+    @param {Ember.RenderBuffer} buffer The render buffer
+  */
+  render: function(buffer) {
+    buffer.push('<div class="ember-list-container">');
+    this._super(buffer);
+    buffer.push('</div>');
+  },
+
+  willInsertElement: function() {
+    if (!this.get("height") || !this.get("rowHeight")) {
+      throw "A ListView must be created with a height and a rowHeight.";
+    }
+    this._super();
+  },
+
+  /**
+    @private
+
+    Sets inline styles of the view:
+    - height
+    - width
+    - position
+    - overflow
+    - -webkit-overflow
+    - overflow-scrolling
+
+    Called while attributes binding.
+
+    @property {Ember.ComputedProperty} style
+  */
+  style: Ember.computed('height', 'width', function() {
+    var height, width, style, css;
+
+    height = get(this, 'height');
+    width = get(this, 'width');
+    css = get(this, 'css');
+
+    style = '';
+
+    if (height) { style += 'height:' + height + 'px;'; }
+    if (width)  { style += 'width:'  + width  + 'px;'; }
+
+    for ( var rule in css ){
+      if (css.hasOwnProperty(rule)) {
+        style += rule + ':' + css[rule] + ';';
+      }
+    }
+
+    return style;
+  }),
+
+  /**
+    @private
+
+    Performs visual scrolling. Is overridden in Ember.ListView.
+
+    @method scrollTo
+  */
+  scrollTo: function(y) {
+    throw 'must override to perform the visual scroll and effectively delegate to _scrollContentTo';
+  },
+
+  /**
+    @private
+
+    Internal method used to force scroll position
+
+    @method scrollTo
+  */
+  _scrollTo: Ember.K,
+
+  /**
+    @private
+    @method _scrollContentTo
+  */
+  _scrollContentTo: function(y) {
+    var startingIndex, endingIndex,
+        contentIndex, visibleEndingIndex, maxContentIndex,
+        contentIndexEnd, contentLength, scrollTop;
+
+    scrollTop = max(0, y);
+
+    Ember.instrument('view._scrollContentTo', {
+      scrollTop: scrollTop,
+      content: get(this, 'content'),
+      startingIndex: this._startingIndex(),
+      endingIndex: min(max(get(this, 'content.length') - 1, 0), this._startingIndex() + this._numChildViewsForViewport())
+    }, function () {
+      contentLength = get(this, 'content.length');
+      set(this, 'scrollTop', scrollTop);
+
+      maxContentIndex = max(contentLength - 1, 0);
+
+      startingIndex = this._startingIndex();
+      visibleEndingIndex = startingIndex + this._numChildViewsForViewport();
+
+      endingIndex = min(maxContentIndex, visibleEndingIndex);
+
+      this.trigger('scrollYChanged', y);
+
+      if (startingIndex === this._lastStartingIndex &&
+          endingIndex === this._lastEndingIndex) {
+        return;
+      }
+
+      this._reuseChildren();
+
+      this._lastStartingIndex = startingIndex;
+      this._lastEndingIndex = endingIndex;
+    }, this);
+  },
+
+  /**
+    @private
+
+    Computes the height for a `Ember.ListView` scrollable container div.
+    You must specify `rowHeight` parameter for the height to be computed properly.
+
+    @property {Ember.ComputedProperty} totalHeight
+  */
+  totalHeight: Ember.computed('content.length', 'rowHeight', 'columnCount', 'bottomPadding', function() {
+    var contentLength, rowHeight, columnCount, bottomPadding;
+
+    contentLength = get(this, 'content.length');
+    rowHeight = get(this, 'rowHeight');
+    columnCount = get(this, 'columnCount');
+    bottomPadding = get(this, 'bottomPadding');
+
+    return ((ceil(contentLength / columnCount)) * rowHeight) + bottomPadding;
+  }),
+
+  /**
+    @private
+    @method _prepareChildForReuse
+  */
+  _prepareChildForReuse: function(childView) {
+    childView.prepareForReuse();
+  },
+
+  /**
+    @private
+    @method _reuseChildForContentIndex
+  */
+  _reuseChildForContentIndex: function(childView, contentIndex) {
+    var content, context, newContext, childsCurrentContentIndex, position, enableProfiling;
+
+    content = get(this, 'content');
+    enableProfiling = get(this, 'enableProfiling');
+    position = this.positionForIndex(contentIndex);
+    set(childView, 'position', position);
+
+    set(childView, 'contentIndex', contentIndex);
+
+    if (enableProfiling) {
+      Ember.instrument('view._reuseChildForContentIndex', position, function(){}, this);
+    }
+
+    newContext = content.objectAt(contentIndex);
+    childView.updateContext(newContext);
+  },
+
+  /**
+    @private
+    @method positionForIndex
+  */
+  positionForIndex: function(index){
+    var elementWidth, width, columnCount, rowHeight, y, x;
+
+    elementWidth = get(this, 'elementWidth') || 1;
+    width = get(this, 'width') || 1;
+    columnCount = get(this, 'columnCount');
+    rowHeight = get(this, 'rowHeight');
+
+    y = (rowHeight * floor(index/columnCount));
+    x = (index % columnCount) * elementWidth;
+
+    return {
+      y: y,
+      x: x
+    };
+  },
+
+  /**
+    @private
+    @method _childViewCount
+  */
+  _childViewCount: function() {
+    var contentLength, childViewCountForHeight;
+
+    contentLength = get(this, 'content.length');
+    childViewCountForHeight = this._numChildViewsForViewport();
+
+    return min(contentLength, childViewCountForHeight);
+  },
+
+  /**
+    @private
+
+    Returns a number of columns in the Ember.ListView (for grid layout).
+
+    If you want to have a multi column layout, you need to specify both
+    `width` and `elementWidth`.
+
+    If no `elementWidth` is specified, it returns `1`. Otherwise, it will
+    try to fit as many columns as possible for a given `width`.
+
+    @property {Ember.ComputedProperty} columnCount
+  */
+  columnCount: Ember.computed('width', 'elementWidth', function() {
+    var elementWidth, width, count;
+
+    elementWidth = get(this, 'elementWidth');
+    width = get(this, 'width');
+
+    if (elementWidth) {
+      count = floor(width / elementWidth);
+    } else {
+      count = 1;
+    }
+
+    return count;
+  }),
+
+  /**
+    @private
+
+    Fires every time column count is changed.
+
+    @event columnCountDidChange
+  */
+  columnCountDidChange: Ember.observer(function(){
+    var ratio, currentScrollTop, proposedScrollTop, maxScrollTop,
+        scrollTop, lastColumnCount, newColumnCount, element;
+
+    lastColumnCount = this._lastColumnCount;
+
+    currentScrollTop = get(this, 'scrollTop');
+    newColumnCount = get(this, 'columnCount');
+    maxScrollTop = get(this, 'maxScrollTop');
+    element = get(this, 'element');
+
+    this._lastColumnCount = newColumnCount;
+
+    if (lastColumnCount) {
+      ratio = (lastColumnCount / newColumnCount);
+      proposedScrollTop = currentScrollTop * ratio;
+      scrollTop = min(maxScrollTop, proposedScrollTop);
+
+      this._scrollTo(scrollTop);
+      set(this, 'scrollTop', scrollTop);
+    }
+
+    if (arguments.length > 0) {
+      // invoked by observer
+      Ember.run.schedule('afterRender', this, syncListContainerWidth);
+    }
+  }, 'columnCount'),
+
+  /**
+    @private
+
+    Computes max possible scrollTop value given the visible viewport
+    and scrollable container div height.
+
+    @property {Ember.ComputedProperty} maxScrollTop
+  */
+  maxScrollTop: Ember.computed('height', 'totalHeight', function(){
+    var totalHeight, viewportHeight;
+
+    totalHeight = get(this, 'totalHeight'),
+    viewportHeight = get(this, 'height');
+
+    return max(0, totalHeight - viewportHeight);
+  }),
+
+  /**
+    @private
+
+    Computes the number of views that would fit in the viewport area.
+    You must specify `height` and `rowHeight` parameters for the number of
+    views to be computed properly.
+
+    @method _numChildViewsForViewport
+  */
+  _numChildViewsForViewport: function() {
+    var height, rowHeight, paddingCount, columnCount;
+
+    height = get(this, 'height');
+    rowHeight = get(this, 'rowHeight');
+    paddingCount = get(this, 'paddingCount');
+    columnCount = get(this, 'columnCount');
+
+    return (ceil(height / rowHeight) * columnCount) + (paddingCount * columnCount);
+  },
+
+  /**
+    @private
+
+    Computes the starting index of the item views array.
+    Takes `scrollTop` property of the element into account.
+
+    Is used in `_syncChildViews`.
+
+    @method _startingIndex
+  */
+  _startingIndex: function() {
+    var scrollTop, rowHeight, columnCount, calculatedStartingIndex,
+        contentLength, largestStartingIndex;
+
+    contentLength = get(this, 'content.length');
+    scrollTop = get(this, 'scrollTop');
+    rowHeight = get(this, 'rowHeight');
+    columnCount = get(this, 'columnCount');
+
+    calculatedStartingIndex = floor(scrollTop / rowHeight) * columnCount;
+
+    largestStartingIndex = max(contentLength - 1, 0);
+
+    return min(calculatedStartingIndex, largestStartingIndex);
+  },
+
+  /**
+    @private
+    @event contentWillChange
+  */
+  contentWillChange: Ember.beforeObserver(function() {
+    var content;
+
+    content = get(this, 'content');
+
+    if (content) {
+      content.removeArrayObserver(this);
+    }
+  }, 'content'),
+
+  /**
+    @private
+    @event contentDidChange
+  */
+  contentDidChange: Ember.observer(function() {
+    addContentArrayObserver.call(this);
+    syncChildViews.call(this);
+  }, 'content'),
+
+  /**
+    @private
+    @property {Function} needsSyncChildViews
+  */
+  needsSyncChildViews: Ember.observer(syncChildViews, 'height', 'width', 'columnCount'),
+
+  /**
+    @private
+
+    Returns a new item view. Takes `contentIndex` to set the context
+    of the returned view properly.
+
+    @param {Number} contentIndex item index in the content array
+    @method _addItemView
+  */
+  _addItemView: function(contentIndex){
+    var itemViewClass, childView;
+
+    itemViewClass = get(this, 'itemViewClass');
+    childView = this.createChildView(itemViewClass);
+
+    this.pushObject(childView);
+   },
+
+  /**
+    @private
+
+    Intelligently manages the number of childviews.
+
+    @method _syncChildViews
+   **/
+  _syncChildViews: function(){
+    var itemViewClass, startingIndex, childViewCount,
+        endingIndex, numberOfChildViews, numberOfChildViewsNeeded,
+        childViews, count, delta, index, childViewsLength, contentIndex;
+
+    if (get(this, 'isDestroyed') || get(this, 'isDestroying')) {
+      return;
+    }
+
+    childViewCount = this._childViewCount();
+    childViews = this.positionOrderedChildViews();
+
+    startingIndex = this._startingIndex();
+    endingIndex = startingIndex + childViewCount;
+
+    numberOfChildViewsNeeded = childViewCount;
+    numberOfChildViews = childViews.length;
+
+    delta = numberOfChildViewsNeeded - numberOfChildViews;
+
+    if (delta === 0) {
+      // no change
+    } else if (delta > 0) {
+      // more views are needed
+      contentIndex = this._lastEndingIndex;
+
+      for (count = 0; count < delta; count++, contentIndex++) {
+        this._addItemView(contentIndex);
+      }
+
+    } else {
+      // less views are needed
+      forEach.call(
+        childViews.splice(numberOfChildViewsNeeded, numberOfChildViews),
+        removeAndDestroy,
+        this
+      );
+    }
+
+    this._scrollContentTo(get(this, 'scrollTop'));
+
+    // if _scrollContentTo short-circuits, we still need
+    // to call _reuseChildren to get new views positioned
+    // and rendered correctly
+    this._reuseChildren();
+
+    this._lastStartingIndex = startingIndex;
+    this._lastEndingIndex   = this._lastEndingIndex + delta;
+  },
+
+  /**
+    @private
+    @method _reuseChildren
+  */
+  _reuseChildren: function(){
+    var contentLength, childViews, childViewsLength,
+        startingIndex, endingIndex, childView, attrs,
+        contentIndex, visibleEndingIndex, maxContentIndex,
+        contentIndexEnd, scrollTop;
+
+    scrollTop = get(this, 'scrollTop');
+    contentLength = get(this, 'content.length');
+    maxContentIndex = max(contentLength - 1, 0);
+    childViews = get(this, 'listItemViews');
+    childViewsLength =  childViews.length;
+
+    startingIndex = this._startingIndex();
+    visibleEndingIndex = startingIndex + this._numChildViewsForViewport();
+
+    endingIndex = min(maxContentIndex, visibleEndingIndex);
+
+    this.trigger('scrollContentTo', scrollTop);
+
+    contentIndexEnd = min(visibleEndingIndex, startingIndex + childViewsLength);
+
+    for (contentIndex = startingIndex; contentIndex < contentIndexEnd; contentIndex++) {
+      childView = childViews[contentIndex % childViewsLength];
+      this._reuseChildForContentIndex(childView, contentIndex);
+    }
+  },
+
+  /**
+    @private
+
+    Returns an array of current ListItemView views in the visible area
+    when you start to scroll.
+
+    @property {Ember.ComputedProperty} listItemViews
+  */
+  listItemViews: Ember.computed('[]', function(){
+    return this.filter(detectListItemViews);
+  }),
+
+  /**
+    @private
+    @method positionOrderedChildViews
+  */
+  positionOrderedChildViews: function() {
+    return get(this, 'listItemViews').sort(sortByContentIndex);
+  },
+
+  arrayWillChange: Ember.K,
+
+  /**
+    @private
+    @event arrayDidChange
+  */
+  // TODO: refactor
+  arrayDidChange: function(content, start, removedCount, addedCount) {
+    var index, contentIndex;
+
+    if (this.state === 'inDOM') {
+      // ignore if all changes are out of the visible change
+      if( start >= this._lastStartingIndex || start < this._lastEndingIndex) {
+        index = 0;
+        // ignore all changes not in the visible range
+        // this can re-position many, rather then causing a cascade of re-renders
+        forEach.call(
+          this.positionOrderedChildViews(),
+          function(childView) {
+            contentIndex = this._lastStartingIndex + index;
+            this._reuseChildForContentIndex(childView, contentIndex);
+            index++;
+          },
+          this
+        );
+      }
+
+      syncChildViews.call(this);
+    }
+  }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+/**
+  The `Ember.ListView` view class renders a
+  [div](https://developer.mozilla.org/en/HTML/Element/div) HTML element,
+  with `ember-list-view` class.
+
+  The context of each item element within the `Ember.ListView` are populated
+  from the objects in the `Element.ListView`'s `content` property.
+
+  ### `content` as an Array of Objects
+
+  The simplest version of an `Ember.ListView` takes an array of object as its
+  `content` property. The object will be used as the `context` each item element
+  inside the rendered `div`.
+
+  Example:
+
+  ```javascript
+  App.contributors = [{ name: 'Stefan Penner' }, { name: 'Alex Navasardyan' }, { name: 'Rey Cohen'}];
+  ```
+
+  ```handlebars
+  {{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50}}
+    {{name}}
+  {{/collection}}
+  ```
+
+  Would result in the following HTML:
+
+  ```html
+   <div id="ember181" class="ember-view ember-list-view" style="height:500px;width:500px;position:relative;overflow:scroll;-webkit-overflow-scrolling:touch;overflow-scrolling:touch;">
+    <div class="ember-list-container">
+      <div id="ember186" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 0px, 0);">
+        <script id="metamorph-0-start" type="text/x-placeholder"></script>Stefan Penner<script id="metamorph-0-end" type="text/x-placeholder"></script>
+      </div>
+      <div id="ember187" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 50px, 0);">
+        <script id="metamorph-1-start" type="text/x-placeholder"></script>Alex Navasardyan<script id="metamorph-1-end" type="text/x-placeholder"></script>
+      </div>
+      <div id="ember188" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 100px, 0);">
+        <script id="metamorph-2-start" type="text/x-placeholder"></script>Rey Cohen<script id="metamorph-2-end" type="text/x-placeholder"></script>
+      </div>
+      <div id="ember189" class="ember-view ember-list-scrolling-view" style="height: 150px"></div>
+    </div>
+  </div>
+  ```
+
+  By default `Ember.ListView` provides support for `height`,
+  `rowHeight`, `width`, `elementWidth`, `scrollTop` parameters.
+
+  Note, that `height` and `rowHeight` are required parameters.
+
+  ```handlebars
+  {{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50}}
+    {{name}}
+  {{/collection}}
+  ```
+
+  If you would like to have multiple columns in your view layout, you can
+  set `width` and `elementWidth` parameters respectively.
+
+  ```handlebars
+  {{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50 width=500 elementWidth=80}}
+    {{name}}
+  {{/collection}}
+  ```
+
+  ### extending `Ember.ListView`
+
+  Example:
+
+  ```handlebars
+  {{view App.ListView contentBinding="content"}}
+
+  <script type="text/x-handlebars" data-template-name="row_item">
+    {{name}}
+  </script>
+  ```
+
+  ```javascript
+  App.ListView = Ember.ListView.extend({
+    height: 500,
+    width: 500,
+    elementWidth: 80,
+    rowHeight: 20,
+    itemViewClass: Ember.ListItemView.extend({templateName: "row_item"})
+  });
+  ```
+
+  @extends Ember.ContainerView
+  @class ListView
+  @namespace Ember
+*/
+Ember.ListView = Ember.ContainerView.extend(Ember.ListViewMixin, {
+  css: {
+    position: 'relative',
+    overflow: 'scroll',
+    '-webkit-overflow-scrolling': 'touch',
+    'overflow-scrolling': 'touch'
+  },
+
+  applyTransform: function(element, position){
+    var x = position.x,
+        y = position.y;
+
+    element.style.top =  y + 'px';
+    element.style.left = x + 'px';
+  },
+
+  _scrollTo: function(scrollTop) {
+    var element = get(this, 'element');
+
+    if (element) { element.scrollTop = scrollTop; }
+  },
+
+  didInsertElement: function() {
+    var that, element;
+
+    that = this,
+    element = get(this, 'element');
+
+    this._updateScrollableHeight();
+
+    this._scroll = function(e) { that.scroll(e); };
+
+    Ember.$(element).on('scroll', this._scroll);
+  },
+
+  willDestroyElement: function() {
+    var element;
+
+    element = get(this, 'element');
+
+    Ember.$(element).off('scroll', this._scroll);
+  },
+
+  scroll: function(e) {
+    Ember.run(this, this.scrollTo, e.target.scrollTop);
+  },
+
+  scrollTo: function(y){
+    var element = get(this, 'element');
+    this._scrollTo(y);
+    this._scrollContentTo(y);
+  },
+
+  totalHeightDidChange: Ember.observer(function () {
+    Ember.run.scheduleOnce('afterRender', this, this._updateScrollableHeight);
+  }, 'totalHeight'),
+
+  _updateScrollableHeight: function () {
+    if (this.state === 'inDOM') {
+      this.$('.ember-list-container').css({
+        height: get(this, 'totalHeight')
+      });
+    }
+  }
+});
+
+})();
+
+
+
+(function() {
+/*global Scroller*/
+var max = Math.max, get = Ember.get, set = Ember.set;
+
+function updateScrollerDimensions(target) {
+  var width, height, totalHeight;
+
+  target = target || this;
+
+  width = get(target, 'width');
+  height = get(target, 'height');
+  totalHeight = get(target, 'totalHeight');
+
+  target.scroller.setDimensions(width, height, width, totalHeight);
+  target.trigger('scrollerDimensionsDidChange');
+}
+
+/**
+  VirtualListView
+
+  @class VirtualListView
+  @namespace Ember
+*/
+Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, {
+  _isScrolling: false,
+  css: {
+    position: 'relative',
+    overflow: 'hidden'
+  },
+
+  init: function(){
+    this._super();
+    this.setupScroller();
+  },
+  _scrollerTop: 0,
+  applyTransform: Ember.ListViewHelper.applyTransform,
+
+  setupScroller: function(){
+    var view, y;
+
+    view = this;
+
+    view.scroller = new Scroller(function(left, top, zoom) {
+      if (view.state !== 'inDOM') { return; }
+
+      if (view.listContainerElement) {
+        view.applyTransform(view.listContainerElement, {x: 0, y: -top});
+        view._scrollerTop = top;
+        view._scrollContentTo(top);
+      }
+    }, {
+      scrollingX: false,
+      scrollingComplete: function(){
+        view.trigger('scrollingDidComplete');
+      }
+    });
+
+    view.trigger('didInitializeScroller');
+    updateScrollerDimensions(view);
+  },
+
+  scrollerDimensionsNeedToChange: Ember.observer(function() {
+    Ember.run.once(this, updateScrollerDimensions);
+  }, 'width', 'height', 'totalHeight'),
+
+  didInsertElement: function() {
+    var that, listContainerElement;
+
+    that = this;
+    this.listContainerElement = this.$('> .ember-list-container')[0];
+
+    this._mouseWheel = function(e) { that.mouseWheel(e); };
+    this.$().on('mousewheel', this._mouseWheel);
+  },
+
+  willDestroyElement: function() {
+    this.$().off('mousewheel', this._mouseWheel);
+  },
+
+  willBeginScroll: function(touches, timeStamp) {
+    this._isScrolling = false;
+    this.trigger('scrollingDidStart');
+
+    this.scroller.doTouchStart(touches, timeStamp);
+  },
+
+  continueScroll: function(touches, timeStamp) {
+    var startingScrollTop, endingScrollTop, event;
+
+    if (this._isScrolling) {
+      this.scroller.doTouchMove(touches, timeStamp);
+    } else {
+      startingScrollTop = this._scrollerTop;
+
+      this.scroller.doTouchMove(touches, timeStamp);
+
+      endingScrollTop = this._scrollerTop;
+
+      if (startingScrollTop !== endingScrollTop) {
+        event = Ember.$.Event("scrollerstart");
+        Ember.$(touches[0].target).trigger(event);
+
+        this._isScrolling = true;
+      }
+    }
+  },
+
+  // api
+  scrollTo: function(y, animate) {
+    if (animate === undefined) {
+      animate = true;
+    }
+
+    this.scroller.scrollTo(0, y, animate, 1);
+  },
+
+  // events
+  mouseWheel: function(e){
+    var inverted, delta, candidatePosition;
+
+    inverted = e.webkitDirectionInvertedFromDevice;
+    delta = e.wheelDeltaY * (inverted ? 0.8 : -0.8);
+    candidatePosition = this.scroller.__scrollTop + delta;
+
+    if ((candidatePosition >= 0) && (candidatePosition <= this.scroller.__maxScrollTop)) {
+      this.scroller.scrollBy(0, delta, true);
+    }
+
+    return false;
+  },
+
+  endScroll: function(timeStamp) {
+    this.scroller.doTouchEnd(timeStamp);
+  },
+
+  touchStart: function(e){
+    e = e.originalEvent || e;
+    this.willBeginScroll(e.touches, e.timeStamp);
+    return false;
+  },
+
+  touchMove: function(e){
+    e = e.originalEvent || e;
+    this.continueScroll(e.touches, e.timeStamp);
+    return false;
+  },
+
+  touchEnd: function(e){
+    e = e.originalEvent || e;
+    this.endScroll(e.timeStamp);
+    return false;
+  },
+
+  mouseDown: function(e){
+    this.willBeginScroll([e], e.timeStamp);
+    return false;
+  },
+
+  mouseMove: function(e){
+    this.continueScroll([e], e.timeStamp);
+    return false;
+  },
+
+  mouseUp: function(e){
+    this.endScroll(e.timeStamp);
+    return false;
+  },
+
+  mouseLeave: function(e){
+    this.endScroll(e.timeStamp);
+    return false;
+  }
+});
+
+})();
+
+
+
+(function() {
+
+})();