diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 06ada5e0976..ddcc17a8141 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 d667a2ad11b..2ff1b165db8 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 b7373efb16e..f5e48aaf7c8 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 @@
{{i18n loading}}
{{else}}
{{#if model.length}}
-
-
- {{i18n admin.logs.blocked_emails.email}} |
- {{i18n admin.logs.action}} |
- {{i18n admin.logs.blocked_emails.match_count}} |
- {{i18n admin.logs.blocked_emails.last_match_at}} |
- {{i18n admin.logs.created_at}} |
-
-
- {{#each model}}
-
- {{email}} |
- {{actionName}} |
- {{match_count}} |
- {{unboundAgeWithTooltip last_match_at}} |
- {{unboundAgeWithTooltip created_at}} |
-
- {{/each}}
-
-
+
+
+
{{i18n admin.logs.blocked_emails.email}}
+
{{i18n admin.logs.action}}
+
{{i18n admin.logs.blocked_emails.match_count}}
+
{{i18n admin.logs.blocked_emails.last_match_at}}
+
{{i18n admin.logs.created_at}}
+
+
+
+ {{view Discourse.BlockedEmailsListView contentBinding="controller"}}
+
+
{{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 00000000000..4e364079647
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/logs/blocked_emails_list_item.js.handlebars
@@ -0,0 +1,6 @@
+{{email}}
+{{actionName}}
+{{match_count}}
+{{unboundAgeWithTooltip last_match_at}}
+{{unboundAgeWithTooltip created_at}}
+
\ 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 00000000000..102547afe2d
--- /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 87a88021ed0..d6b8ee41380 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 2ef690709a9..866672f3f64 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 00000000000..af923982d9a
--- /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
+
+ ```
+
+ ```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('');
+ this._super(buffer);
+ buffer.push('
');
+ },
+
+ 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
+
+
+
+ Stefan Penner
+
+
+ Alex Navasardyan
+
+
+ Rey Cohen
+
+
+
+
+ ```
+
+ 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"}}
+
+
+ ```
+
+ ```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() {
+
+})();