Permalink
Browse files

Implement infinite scroll for events

This introduces a new view LoadMoreView which triggers a call to `loadMore` on the controller when the view is visible in the current viewport. The callback then can trigger a loading of more items...
  • Loading branch information...
1 parent b263a56 commit 68d1728ec26dae5062eae5be43d61083cfc34f14 @pangratz committed Aug 12, 2012
View
@@ -1,17 +1,51 @@
require('dashboard/core');
+Dashboard.LoadMoreMixin = Ember.Mixin.create(Ember.Evented, {
+ canLoadMore: true,
+ autoFetch: true,
+ currentPage: 1,
+ resetLoadMore: function() {
+ this.set('currentPage', 1);
+ },
+ loadMore: Ember.K
+});
+
Dashboard.ApplicationController = Ember.Controller.extend();
Dashboard.UserController = Ember.ObjectController.extend();
Dashboard.RepositoryController = Ember.ObjectController.extend();
-Dashboard.EventsController = Ember.ArrayController.extend();
+Dashboard.EventsController = Ember.ArrayController.extend(Dashboard.LoadMoreMixin, {
+ canLoadMore: function() {
+ return this.get('currentPage') < 10;
+ }.property('currentPage'),
+
+ loadMore: function() {
+ if (this.get('canLoadMore')) {
+ var page = this.incrementProperty('currentPage');
+ this.get('target').send('loadMoreEvents', page);
+ }
+ }
+});
-Dashboard.RepositoriesController = Ember.ArrayController.extend({
+Dashboard.RepositoriesController = Ember.ArrayController.extend(Dashboard.LoadMoreMixin, {
sortProperties: 'updated_at'.w(),
sortAscending: false,
loadWatchedRepositories: function(username) {
this.get('dataSource').watchedRepositories(username, this, 'addObjects');
+ },
+
+ canLoadMore: function() {
+ var reposCount = this.get('owner.publicRepos');
+ var length = this.get('length');
+ return true || (length < reposCount);
+ }.property('owner.publicRepos', 'length'),
+
+ loadMore: function() {
+ if (this.get('canLoadMore')) {
+ var page = this.incrementProperty('currentPage');
+ this.get('target').send('loadMoreRepos', page);
+ }
}
});
@@ -28,16 +28,17 @@ Dashboard.GitHubAdpater = DS.Adapter.extend({
}
},
- _invoke: function(target, callback) {
+ _invoke: function(target, callback, query) {
return function(data) {
Ember.tryInvoke(target, callback, [data]);
- }
+ Ember.tryInvoke(query, 'isLoadedCallback');
+ };
},
_storeLoad: function(store, type, id) {
return function(data) {
store.load(type, id, data);
- }
+ };
},
find: function(store, type, id) {
@@ -58,25 +59,28 @@ Dashboard.GitHubAdpater = DS.Adapter.extend({
this.ajax('/users/%@/repos'.fmt(query.username), this._invoke(modelArray, 'load'));
// events for a repository
- } else if (Dashboard.Event.detect(type) && query.username && query.repository) {
- this.ajax('/repos/%@/%@/events'.fmt(query.username, query.repository), this._invoke(modelArray, 'load'));
+ } else if (Dashboard.Event.detect(type) && query.repoName) {
+ this.ajax('/repos/%@/events?page=%@'.fmt(query.repoName, query.page || 1), this._invoke(modelArray, 'load', query));
// events for a user
- } else if (Dashboard.Event.detect(type) && query.username && !query.repository) {
- this.ajax('/users/%@/events'.fmt(query.username), this._invoke(modelArray, 'load'));
+ } else if (Dashboard.Event.detect(type) && query.username) {
+ this.ajax('/users/%@/events?page=%@'.fmt(query.username, query.page || 1), this._invoke(modelArray, 'load', query));
}
}
});
Dashboard.GitHubFixtureAdpater = Dashboard.GitHubAdpater.extend({
PREFIX: 'http://localhost:9292/app/tests/mock_response_data',
_ajax: function(url, callback) {
- Ember.$.ajax({
- url: this.PREFIX + url + '.json',
- context: this,
- success: function(data) {
- callback.call(this, {meta: {}, data: data});
- }
- });
+ var encodedUrl = url.replace(/\?/, '%3F').replace(/\=/, '%3D');
+ Ember.run.later(this, function() {
+ Ember.$.ajax({
+ url: this.PREFIX + encodedUrl + '.json',
+ context: this,
+ success: function(data) {
+ callback.call(this, {meta: {}, data: data});
+ }
+ });
+ }, 500);
}
});
View
@@ -28,18 +28,30 @@ Dashboard.Router = Ember.Router.extend({
var username = router.get('userController.id');
var store = router.get('store');
+ router.get('eventsController').resetLoadMore();
+ router.get('repositoriesController').resetLoadMore();
+
+ // set current query
+ var query = { username: username, isLoadedCallback: function() {
+ router.set('eventsController.isLoading', false);
+ }};
+ router.set('eventsController.query', query);
+ router.set('eventsController.isLoading', true);
+
// get repositories for user
- var repos = store.findQuery(Dashboard.Repository, { username: username });
- router.set('repositoriesController.content', repos);
+ var userRepositories = store.findQuery(Dashboard.Repository, { username: username });
// get events performed by user
- var userEvents = store.findQuery(Dashboard.Event, { username: username });
- router.set('eventsController.content', userEvents);
+ var filter = function(data) {
+ if (Ember.get(data, 'savedData.org.login') === username) { return true; }
+ return Ember.get(data, 'savedData.actor.login') === username;
+ };
+ var userEvents = store.filter(Dashboard.Event, query, filter);
// connect user with events and watched repositories
router.get('applicationController').connectOutlet('user');
- router.get('userController').connectOutlet('repositories', 'repositories');
- router.get('userController').connectOutlet('events', 'events');
+ router.get('userController').connectOutlet('repositories', 'repositories', userRepositories);
+ router.get('userController').connectOutlet('events', 'events', userEvents);
}
}),
@@ -48,22 +60,48 @@ Dashboard.Router = Ember.Router.extend({
connectOutlets: function(router, context) {
var username = router.get('userController.id');
var repoName = context.repository;
+ var name = '%@/%@'.fmt(username, repoName);
+
+ router.get('eventsController').resetLoadMore();
+
+ // set current query
+ var query = {
+ repoName: name,
+ isLoadedCallback: function() {
+ router.set('eventsController.isLoading', false);
+ }
+ };
+ router.set('eventsController.query', query);
+ router.set('eventsController.isLoading', true);
// fetch repo for current user
- var repo = router.get('store').find(Dashboard.Repository, '%@/%@'.fmt(username, repoName));
- router.set('repositoryController.content', repo);
+ var repository = router.get('store').find(Dashboard.Repository, name);
- var events = router.get('store').findQuery(Dashboard.Event, {
- username: username,
- repository: repoName
- });
+ // get all events for this repository
+ var filter = function(data) {
+ return Ember.get(data, 'savedData.repo.name') === name;
+ };
+ var events = router.get('store').filter(Dashboard.Event, query, filter);
// connect repository and events
+ router.set('repositoryController.content', repository);
router.get('applicationController').connectOutlet('repository');
router.get('repositoryController').connectOutlet('events', 'events', events);
}
}),
+ loadMoreEvents: function(router, page) {
+ var query = router.get('eventsController.query');
+ query.page = page;
+ router.get('store').findQuery(Dashboard.Event, query);
+ router.set('eventsController.isLoading', true);
+ },
+ loadMoreRepos: function(router, page) {
+ var username = router.get('userController.id');
+ var query = { username: username };
+ var store = router.get('store').findQuery(Dashboard.Repository, query);
+ },
+
showUserOfEvent: function(router, evt) {
var e = evt.context;
var username = e.get('actor.login');
View
@@ -1,10 +1,23 @@
require('dashboard/core');
require('dashboard/handlebars_helpers');
+require('jquery.inview');
Dashboard.ApplicationView = Ember.View.extend({
templateName: 'application'
});
+Dashboard.LoadMoreView = Ember.View.extend({
+ templateName: 'loadMore',
+ didInsertElement: function() {
+ if (this.get('controller.autoFetch')) {
+ var view = this;
+ this.$().bind('inview', function(event, isInView, visiblePartX, visiblePartY) {
+ if (isInView) Ember.tryInvoke(view.get('controller'), 'loadMore');
+ });
+ }
+ }
+});
+
Dashboard.EventsView = Ember.View.extend({
templateName: 'events',
EventView: Ember.View.extend({
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -2,7 +2,6 @@
<ul class="unstyled">
{{#each event in controller}}
{{view view.EventView eventBinding="event"}}
- {{else}}
- loading ...
{{/each}}
+ {{view Dashboard.LoadMoreView controllerBinding="view.controller" }}
</ul>
@@ -0,0 +1,9 @@
+{{#if isLoading}}
+ fetching some more stuff <img width="10" src="img/ajax-loader.gif" >
+{{else}}
+ {{#if canLoadMore}}
+ <a {{action "loadMore" target="controller" }} >click to load more</a>
+ {{else}}
+ <strong><em>no more items</em></strong>
+ {{/if}}
+{{/if}}
@@ -8,7 +8,5 @@
<div>{{repository.description}}</div>
<dl class="dl-horizontal">
</li>
- {{else}}
- loading ...
{{/each}}
</ul>
@@ -0,0 +1,118 @@
+/**
+ * author Christopher Blum
+ * - based on the idea of Remy Sharp, http://remysharp.com/2009/01/26/element-in-view-event-plugin/
+ * - forked from http://github.com/zuk/jquery.inview/
+ */
+(function ($) {
+ var inviewObjects = {}, viewportSize, viewportOffset,
+ d = document, w = window, documentElement = d.documentElement, expando = $.expando;
+
+ $.event.special.inview = {
+ add: function(data) {
+ inviewObjects[data.guid + "-" + this[expando]] = { data: data, $element: $(this) };
+ },
+
+ remove: function(data) {
+ try { delete inviewObjects[data.guid + "-" + this[expando]]; } catch(e) {}
+ }
+ };
+
+ function getViewportSize() {
+ var mode, domObject, size = { height: w.innerHeight, width: w.innerWidth };
+
+ // if this is correct then return it. iPad has compat Mode, so will
+ // go into check clientHeight/clientWidth (which has the wrong value).
+ if (!size.height) {
+ mode = d.compatMode;
+ if (mode || !$.support.boxModel) { // IE, Gecko
+ domObject = mode === 'CSS1Compat' ?
+ documentElement : // Standards
+ d.body; // Quirks
+ size = {
+ height: domObject.clientHeight,
+ width: domObject.clientWidth
+ };
+ }
+ }
+
+ return size;
+ }
+
+ function getViewportOffset() {
+ return {
+ top: w.pageYOffset || documentElement.scrollTop || d.body.scrollTop,
+ left: w.pageXOffset || documentElement.scrollLeft || d.body.scrollLeft
+ };
+ }
+
+ function checkInView() {
+ var $elements = $(), elementsLength, i = 0;
+
+ $.each(inviewObjects, function(i, inviewObject) {
+ var selector = inviewObject.data.selector,
+ $element = inviewObject.$element;
+ $elements = $elements.add(selector ? $element.find(selector) : $element);
+ });
+
+ elementsLength = $elements.length;
+ if (elementsLength) {
+ viewportSize = viewportSize || getViewportSize();
+ viewportOffset = viewportOffset || getViewportOffset();
+
+ for (; i<elementsLength; i++) {
+ // Ignore elements that are not in the DOM tree
+ if (!$.contains(documentElement, $elements[i])) {
+ continue;
+ }
+
+ var $element = $($elements[i]),
+ elementSize = { height: $element.height(), width: $element.width() },
+ elementOffset = $element.offset(),
+ inView = $element.data('inview'),
+ visiblePartX,
+ visiblePartY,
+ visiblePartsMerged;
+
+ // Don't ask me why because I haven't figured out yet:
+ // viewportOffset and viewportSize are sometimes suddenly null in Firefox 5.
+ // Even though it sounds weird:
+ // It seems that the execution of this function is interferred by the onresize/onscroll event
+ // where viewportOffset and viewportSize are unset
+ if (!viewportOffset || !viewportSize) {
+ return;
+ }
+
+ if (elementOffset.top + elementSize.height > viewportOffset.top &&
+ elementOffset.top < viewportOffset.top + viewportSize.height &&
+ elementOffset.left + elementSize.width > viewportOffset.left &&
+ elementOffset.left < viewportOffset.left + viewportSize.width) {
+ visiblePartX = (viewportOffset.left > elementOffset.left ?
+ 'right' : (viewportOffset.left + viewportSize.width) < (elementOffset.left + elementSize.width) ?
+ 'left' : 'both');
+ visiblePartY = (viewportOffset.top > elementOffset.top ?
+ 'bottom' : (viewportOffset.top + viewportSize.height) < (elementOffset.top + elementSize.height) ?
+ 'top' : 'both');
+ visiblePartsMerged = visiblePartX + "-" + visiblePartY;
+ if (!inView || inView !== visiblePartsMerged) {
+ $element.data('inview', visiblePartsMerged).trigger('inview', [true, visiblePartX, visiblePartY]);
+ }
+ } else if (inView) {
+ $element.data('inview', false).trigger('inview', [false]);
+ }
+ }
+ }
+ }
+
+ $(w).bind("scroll resize", function() {
+ viewportSize = viewportOffset = null;
+ });
+
+ // Use setInterval in order to also make sure this captures elements within
+ // "overflow:scroll" elements or elements that appeared in the dom tree due to
+ // dom manipulation and reflow
+ // old: $(window).scroll(checkInView);
+ //
+ // By the way, iOS (iPad, iPhone, ...) seems to not execute, or at least delays
+ // intervals while the user scrolls. Therefore the inview event might fire a bit late there
+ setInterval(checkInView, 250);
+})(jQuery);

0 comments on commit 68d1728

Please sign in to comment.