From 0d44313a4bc338ef82fc5411834fd4c58ff0a6e0 Mon Sep 17 00:00:00 2001 From: Neil Lalonde 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 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}} - - - - - - - - - - {{#each model}} - - - - - - - - {{/each}} - -
{{i18n admin.logs.action}}{{i18n admin.logs.blocked_emails.match_count}}{{i18n admin.logs.blocked_emails.last_match_at}}{{i18n admin.logs.created_at}}
{{actionName}}{{match_count}}{{unboundAgeWithTooltip last_match_at}}{{unboundAgeWithTooltip created_at}}
+
+
+ +
{{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() { + +})();