Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Optional more callback #11

Open
wants to merge 8 commits into from

3 participants

@DeaconDesperado

I previously had confused the lazy callback for the functionality this pull request now adds.

Scrolling to the bottom of the ListView will trigger a check to the last of its pages to check if the last of that pages items is onscreen. If the item is onscreen, and a callback function for more was supplied to the ListView constructor, that view will call its more callback.

Since the context of this in the more callback refers to the ListView itself, this.append can be called from within the more callback to add more items, whether they come from an ajax call or some other static data source.

I left preloader logic out pending feedback from the original devs.

@DeaconDesperado

Some quick demos:

http://snips.deacondesperado.com/infinity_demo/ajax_demo.html
http://snips.deacondesperado.com/infinity_demo/demo.html

The ajax demo responds indefinitely on scroll. In a real app, I would check the data length, and if a length 0 is returned set ListView.more = false to disable further superfluous requests.

@reissbaker
Collaborator

Nice!

The one thing I'm not sure about is having to manually set ListView.more. I wonder if there's a way to do this automatically: prevent all future requests until a callback is called, maybe? I.e.

new ListView({
  // this will be prevented from running more than once until `done` is called
  more: function(done) {
    $.get(..., function() {
      // do things
      ...

      // signal that the callback is done
      done();
    });
  }
});

and an implementation something like:

if(isBottomList(lastItem.$el) && currentView.more) {
  currentView.more = false;
  currentView.moreFn(function() {
    currentView.more = true;
  });
}

It could get more advanced (auto-append contents passed in, or stop all future requests if some value -- false? null? -- is passed), but I think that's probably making too many decisions for the end user. Just auto-blocking requests until old ones finish would be enough for now, I think.

Good work!

@DeaconDesperado

I agree... I'll definitely amend what I have to insert a done callback for the data source for more.

However, the use case I was referring to earlier was long lists of heavily paginated content that the server would stream back. At least where I'm using infinity would be a scenario where more would be used to fetch the next page, page=2&per_page=50 or something like that in the query string. What I meant was that the first req that retrieved empty data should unhook the functionality for more, otherwise every scroll to the bottom of the viewport would try to go back and effectively fetch data we know doesn't exist.

Since the more callback would necessarily ALWAYS be sourced to ajax, I would think if/when to unhook the functionality for more would be best left up to the person implementing... that way, if they know they will not retrieve any new data on the next scroll for some reason, they can save the resources of attempting to fetch it. Sorry if I wasn't clear with how I worded it.

@reissbaker
Collaborator

Ah for sure, hence the done callback. Maybe I'm misunderstanding?

My thought was that the done callback would be the trigger to control whether the more functionality keeps firing -- just without needing to explicitly set variables. You could wait to call it until after the AJAX callback completes (which seems like the sensible idea, since otherwise you could trigger the callback multiple times for the same data), or, if you wanted to, you could call it immediately and let the more callback fire as often as it wants. For functionality like yours in the demo, you'd do:

new ListView({
  more: function(done) {
    // finish immediately to let the `more` callback keep firing without waiting for new data
    done();

    // do things. an ajax call? randomly generate data clientside? who knows?
  }
});

And, of course, if the server returns an empty dataset you could always just not call done at all, in which case the more callback would never fire again.

The thing I like about a callback is it unhooks more by default -- which seems right, if you're waiting on asynchronous network requests (and most applications will be doing that). And since callbacks are a pretty standard way to do things on async request completion, it seems like it'd make sense here.

Admittedly, I could be missing something.

@redterror

I think this needs work, it assumes that window is the scroll container. You can sidestep the whole container issue if you call more() once the last page is onscreen. It may be slightly early in some cases, but if your user has scrolled to the last page, they're pretty close to the end!

In that case, you'd just remove isBottomList entirely.

@DeaconDesperado

@redterror Agreed, it's been awhile since I last took a crack at this but I'll go into the patch today and apply the suggested changes.

@DeaconDesperado

Sorry for the multiple commits. I took the advice shared here and made the requested functionality for the done callback, which can accept a boolean indicating whether further calls to the moreFn are to be expected (to suit the ajax empty response example). I also removed the window-based scrolling logic in favor of checking lastPage on screen status per @redterror 's comments.

The demos from the older comment are updated as well.

@DeaconDesperado DeaconDesperado referenced this pull request
Open

infinity js #25

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 44 additions and 9 deletions.
  1. +24 −3 build/infinity.js
  2. +1 −1  build/infinity.min.js
  3. +19 −5 infinity.js
View
27 build/infinity.js
@@ -79,6 +79,9 @@
this.lazy = !!options.lazy;
this.lazyFn = options.lazy || null;
+ // new callback for loading more content
+ this.more = !!options.more;
+ this.moreFn = options.more || null;
initBuffer(this);
@@ -501,11 +504,21 @@
// and disallows future scheduling.
function scrollHandler() {
- if(!scrollScheduled) {
+ if(!scrollScheduled) {
setTimeout(scrollAll, config.SCROLL_THROTTLE);
- scrollScheduled = true;
+ scrollScheduled = true;
}
}
+
+ // ### isBottomList
+ //
+ // On scroll, check if the given element has appeared onscreen. Used to trigger
+ // the optional more callback
+
+ function isBottomList($el){
+ var viewportBot = $(window).scrollTop()+$(window).height();
+ return $el.offset().top <= viewportBot;
+ }
// ### scrollAll
@@ -517,7 +530,15 @@
function scrollAll() {
var index, length;
for(index = 0, length = boundViews.length; index < length; index++) {
- updateStartIndex(boundViews[index]);
+ var currentView = boundViews[index];
+ updateStartIndex(currentView);
+ var lastPage = currentView.pages[currentView.pages.length-1]
+ var lastItem = lastPage.items[lastPage.items.length-1]
+ if (isBottomList(lastItem.$el) && currentView.more){
+ // If we have reached the bottom of the list and the last element of the
+ // last page is onscreen, call the optional more callback and get more elements
+ currentView.moreFn()
+ }
}
scrollScheduled = false;
}
View
2  build/infinity.min.js
@@ -3,4 +3,4 @@
// infinity.js may be freely distributed under the terms of the BSD
// license. For all licensing information, details, and documention:
// http://airbnb.github.com/infinity
-!function(e,t,n){"use strict";function l(e,t){t=t||{},this.$el=k(),this.$shadow=k(),e.append(this.$el),this.lazy=!!t.lazy,this.lazyFn=t.lazy||null,c(this),this.top=this.$el.offset().top,this.width=0,this.height=0,this.pages=[],this.startIndex=0,E.attach(this)}function c(e){e._$buffer=k().prependTo(e.$el)}function h(e){var t,n=e.pages,r=e._$buffer;n.length>0?(t=n[e.startIndex],r.height(t.top)):r.height(0)}function p(e,t){t.$el.remove(),e.$el.append(t.$el),C(t,e.height),t.$el.remove()}function d(e){var n,r,i,s=e.pages,o=!1,u=!0;n=e.startIndex,r=t.min(n+f,s.length);for(n;n<r;n++)i=s[n],e.lazy&&i.lazyload(e.lazyFn),o&&i.onscreen&&(u=!1),u?i.onscreen||(o=!0,i.appendTo(e.$el)):(i.stash(e.$shadow),i.appendTo(e.$el))}function v(e){var n,i,s,o,u,a=e.startIndex,l=r.scrollTop()-e.top,c=r.height(),p=l+c,v=b(e,l,p);if(v<0||v===a)return a;s=e.pages,a=e.startIndex,o=t.min(a+f,s.length),u=t.min(v+f,s.length);for(n=a,i=o;n<i;n++)(n<v||n>=u)&&s[n].stash(e.$shadow);return e.startIndex=v,d(e),h(e),v}function m(e,t){var r;return t instanceof N?t:(typeof t=="string"&&(t=n(t)),r=new N(t),p(e,r),r)}function g(e,t){y(e)}function y(e){var t,n,r,i,s,o,u,a,f,l=e.pages,c=[];n=new S(e),c.push(n);for(r=0,i=l.length;r<i;r++){t=l[r],u=t.items;for(s=0,o=u.length;s<o;s++)a=u[s],f=a.clone(),n.hasVacancy()?n.append(f):(n=new S(e),c.push(n),n.append(f));t.remove()}e.pages=c,d(e)}function b(e,n,r){var i=w(e,n,r);return i=t.max(i-a,0),i=t.min(i,e.pages.length),i}function w(e,n,r){var i,s,o,u,f,l,c,h=e.pages,p=n+(r-n)/2;u=t.min(e.startIndex+a,h.length-1);if(h.length<=0)return-1;o=h[u],f=o.top+o.height/2,c=p-f;if(c<0){for(i=u-1;i>=0;i--){o=h[i],f=o.top+o.height/2,l=p-f;if(l>0)return l<-c?i:i+1;c=l}return 0}if(c>0){for(i=u+1,s=h.length;i<s;i++){o=h[i],f=o.top+o.height/2,l=p-f;if(l<0)return-l<c?i:i-1;c=l}return h.length-1}return u}function S(e){this.parent=e,this.items=[],this.$el=k(),this.id=x.generatePageId(this),this.$el.attr(u,this.id),this.top=0,this.bottom=0,this.width=0,this.height=0,this.lazyloaded=!1,this.onscreen=!1}function T(e,t){var n,r,i,s=t.items;for(n=0,r=s.length;n<r;n++)if(s[n]===e){i=n;break}return i==null?!1:(s.splice(i,1),t.bottom-=e.height,t.height=t.bottom-t.top,t.hasVacancy()&&g(t.parent,t),!0)}function N(e){this.$el=e,this.parent=null,this.top=0,this.bottom=0,this.width=0,this.height=0}function C(e,t){var n=e.$el;e.top=t,e.height=n.outerHeight(!0),e.bottom=e.top+e.height,e.width=n.width()}function k(){return n("<div>").css({margin:0,padding:0,border:"none"})}function L(e){var t;e?(t=e.ListView,n.fn.listView=function(e){return new t(this,e)}):delete n.fn.listView}var r=n(e),i=e.infinity,s=e.infinity={},o=s.config={},u="data-infinity-pageid",a=1,f=a*2+1;o.PAGE_TO_SCREEN_RATIO=3,o.SCROLL_THROTTLE=350,l.prototype.append=function(e){if(!e||!e.length)return null;var t,n=m(this,e),r=this.pages;this.height+=n.height,this.$el.height(this.height),t=r[r.length-1];if(!t||!t.hasVacancy())t=new S(this),r.push(t);return t.append(n),d(this),n},l.prototype.remove=function(){this.$el.remove(),this.cleanup()},l.prototype.find=function(e){var t,r,i;return typeof e=="string"?(r=this.$el.find(e),i=this.$shadow.find(e),this.find(r).concat(this.find(i))):e instanceof N?[e]:(t=[],e.each(function(){var e,r,i,s,o,a,f=n(this).parentsUntil("["+u+"]").andSelf().first(),l=f.parent();e=l.attr(u),r=x.lookup(e);if(r){i=r.items;for(s=0,o=i.length;s<o;s++){a=i[s];if(a.$el.is(f)){t.push(a);break}}}}),t)},l.prototype.cleanup=function(){var e=this.pages,t;E.detach(this);while(t=e.pop())t.cleanup()};var E=function(){function s(){t||(setTimeout(u,o.SCROLL_THROTTLE),t=!0)}function u(){var e,n;for(e=0,n=i.length;e<n;e++)v(i[e]);t=!1}function a(){n&&clearTimeout(n),n=setTimeout(f,200)}function f(){var e,t;for(e=0;t=i[e];e++)y(t)}var e=!1,t=!1,n=null,i=[];return{attach:function(t){e||(r.on("scroll",s),r.on("resize",a),e=!0),i.push(t)},detach:function(t){var n,o;for(n=0,o=i.length;n<o;n++)if(i[n]===t)return i.splice(n,1),i.length===0&&(r.off("scroll",s),r.off("resize",a),e=!1),!0;return!1}}}();S.prototype.append=function(e){var t=this.items;t.length===0&&(this.top=e.top),this.bottom=e.bottom,this.width=this.width>e.width?this.width:e.width,this.height=this.bottom-this.top,t.push(e),e.parent=this,this.$el.append(e.$el),this.lazyloaded=!1},S.prototype.prepend=function(e){var t=this.items;this.bottom+=e.height,this.width=this.width>e.width?this.width:e.width,this.height=this.bottom-this.top,t.push(e),e.parent=this,this.$el.prepend(e.$el),this.lazyloaded=!1},S.prototype.hasVacancy=function(){return this.height<r.height()*o.PAGE_TO_SCREEN_RATIO},S.prototype.appendTo=function(e){this.onscreen||(this.$el.appendTo(e),this.onscreen=!0)},S.prototype.prependTo=function(e){this.onscreen||(this.$el.prependTo(e),this.onscreen=!0)},S.prototype.stash=function(e){this.onscreen&&(this.$el.appendTo(e),this.onscreen=!1)},S.prototype.remove=function(){this.onscreen&&(this.$el.remove(),this.onscreen=!1),this.cleanup()},S.prototype.cleanup=function(){var e=this.items,t;this.parent=null,x.remove(this);while(t=e.pop())t.cleanup()},S.prototype.lazyload=function(e){var t=this.$el,n,r;if(!this.lazyloaded){for(n=0,r=t.length;n<r;n++)e.call(t[n],t[n]);this.lazyloaded=!0}};var x=function(){var e=[];return{generatePageId:function(t){return e.push(t)-1},lookup:function(t){return e[t]||null},remove:function(t){var n=t.id;return e[n]?(e[n]=null,!0):!1}}}();N.prototype.clone=function(){var e=new N(this.$el);return e.top=this.top,e.bottom=this.bottom,e.width=this.width,e.height=this.height,e},N.prototype.remove=function(){this.$el.remove(),T(this,this.parent),this.cleanup()},N.prototype.cleanup=function(){this.parent=null},s.ListView=l,s.Page=S,s.ListItem=N,L(s),s.noConflict=function(){return e.infinity=i,L(i),s}}(window,Math,jQuery);
+!function(e,t,n){"use strict";function f(e,t){t=t||{},this.$el=k(),this.$shadow=k(),e.append(this.$el),this.lazy=!!t.lazy,this.lazyFn=t.lazy||null,this.more=!!t.more,this.moreFn=t.more||null,l(this),this.top=this.$el.offset().top,this.width=0,this.height=0,this.pages=[],this.startIndex=0,w.attach(this)}function l(e){e._$buffer=k().prependTo(e.$el)}function c(e){var t,n=e.pages,r=e._$buffer;n.length>0?(t=n[e.startIndex],r.height(t.top)):r.height(0)}function h(e,t){t.$el.remove(),e.$el.append(t.$el),N(t,e.height),t.$el.remove()}function p(e){var n,r,i,s=e.pages,o=!1,u=!0;n=e.startIndex,r=t.min(n+a,s.length);for(n;n<r;n++)i=s[n],e.lazy&&i.lazyload(e.lazyFn),o&&i.onscreen&&(u=!1),u?i.onscreen||(o=!0,i.appendTo(e.$el)):(i.stash(e.$shadow),i.appendTo(e.$el))}function d(r){var i,s,o,u,f,l,h,d=r.startIndex,v=n(e).scrollTop()-r.top,m=n(e).height(),g=v+m,b=y(r,v,g);if(b<0||b===d)return d;u=r.pages,d=r.startIndex,f=new Array(u.length),l=t.min(d+a,u.length),h=t.min(b+a,u.length);for(i=b,s=h;i<s;i++)f[i]=!0;for(i=d,s=l;i<s;i++)f[i]||u[i].stash(r.$shadow);return r.startIndex=b,p(r),c(r),b}function v(e,t){var r;return t instanceof T?t:(typeof t=="string"&&(t=n(t)),r=new T(t),h(e,r),r)}function m(e,t){}function g(e){var t,n,r,i,s,o,u,a,f=e.pages,l=[];n=new E(e),l.push(n);for(r=0,i=f.length;r<i;r++){t=f[r];for(s=0,o=t.items.length;s<o;s++)u=t.items[s],a=u.clone(),n.hasVacancy()?n.append(a):(n=new E(e),l.push(n),n.append(a));t.remove()}e.pages=l,p(e)}function y(e,n,r){var i=b(e,n,r);return i=t.max(i-u,0),i=t.min(i,e.pages.length),i}function b(e,n,r){var i,s,o,a,f,l,c,h=e.pages,p=n+(r-n)/2;a=t.min(e.startIndex+u,h.length-1);if(h.length<=0)return-1;o=h[a],f=o.top+o.height/2,c=p-f;if(c<0){for(i=a-1;i>=0;i--){o=h[i],f=o.top+o.height/2,l=p-f;if(l>0)return l<-c?i:i+1;c=l}return 0}if(c>0){for(i=a+1,s=h.length;i<s;i++){o=h[i],f=o.top+o.height/2,l=p-f;if(l<0)return-l<c?i:i-1;c=l}return h.length-1}return a}function E(e){this.parent=e,this.items=[],this.$el=k(),this.id=S.generatePageId(this),this.$el.attr(o,this.id),this.top=0,this.bottom=0,this.width=0,this.height=0,this.lazyloaded=!1,this.onscreen=!1}function x(e,t){var n,r,i,s=t.items;for(n=0,r=s.length;n<r;n++)if(s[n]===e){i=n;break}return i==null?!1:(s.splice(i,1),t.bottom-=e.height,t.height=t.bottom-t.top,t.hasVacancy()||m(t.parent,t),!0)}function T(e){this.$el=e,this.parent=null,this.top=0,this.bottom=0,this.width=0,this.height=0}function N(e,t){var n=e.$el,r=n.offset();e.top=t,e.height=n.outerHeight(!0),e.bottom=e.top+e.height,e.width=n.width()}function C(){return n("<div></div>")}function k(){return C().css({margin:0,padding:0,border:"none"})}function L(e){return parseInt(e.replace("px",""),10)}var r=e.infinity,i=e.infinity={},s=i.config={},o="data-infinity-pageid",u=1,a=u*2+1;s.PAGE_TO_SCREEN_RATIO=3,s.SCROLL_THROTTLE=350,f.prototype.append=function(e){if(!e||e.length&&e.length===0)return null;var t,n=v(this,e),r=this.pages,i=!1;this.height+=n.height,this.$el.height(this.height),r.length>0&&(t=r[r.length-1]);if(!t||!t.hasVacancy())t=new E(this),r.push(t),i=!0;return t.append(n),p(this),n},f.prototype.remove=function(){this.$el.remove(),this.cleanup()},f.prototype.find=function(e){var t,r,i;return typeof e=="string"?(r=this.$el.find(e),i=this.$shadow.find(e),this.find(r).concat(this.find(i))):e instanceof T?[e]:(t=[],e.each(function(){var e,r,i,s,u,a,f=n(this),l=f.parent();while(!l.attr(o)&&l.length>0)f=l,l=l.parent();e=parseInt(l.attr(o),10),r=S.lookup(e);if(r){i=r.items;for(s=0,u=i.length;s<u;s++){a=i[s];if(a.$el.is(f)){t.push(a);break}}}}),t)},f.prototype.cleanup=function(){var e=this.pages;w.detach(this);while(e.length>0)e.pop().cleanup()};var w=function(e,t){function u(){r||(setTimeout(f,s.SCROLL_THROTTLE),r=!0)}function a(n){var r=t(e).scrollTop()+t(e).height();return n.offset().top<=r}function f(){var e,t;for(e=0,t=o.length;e<t;e++){var n=o[e];d(n);var i=n.pages[n.pages.length-1],s=i.items[i.items.length-1];a(s.$el)&&n.more&&n.moreFn()}r=!1}function l(){i&&clearTimeout(i),i=setTimeout(c,200)}function c(){var e,t;for(e=0;t=o[e];e++)g(t)}var n=!1,r=!1,i=null,o=[];return{attach:function(r){n||(t(e).on("scroll",u),t(e).on("resize",l),n=!0),o.push(r)},detach:function(r){var i,s;for(i=0,s=o.length;i<s;i++)if(o[i]===r)return o.splice(i,1),o.length===0&&(t(e).off("scroll",u),t(e).off("resize",l),n=!1),!0;return!1}}}(e,n);E.prototype.append=function(e){var t=this.items;t.length===0&&(this.top=e.top),this.bottom=e.bottom,this.width=this.width>e.width?this.width:e.width,this.height=this.bottom-this.top,t.push(e),e.parent=this,this.$el.append(e.$el),this.lazyloaded=!1},E.prototype.prepend=function(e){var t=this.items;this.bottom+=e.height,this.width=this.width>e.width?this.width:e.width,this.height=this.bottom-this.top,t.push(e),e.parent=this,this.$el.prepend(e.$el),this.lazyloaded=!1},E.prototype.hasVacancy=function(){return this.height<n(e).height()*s.PAGE_TO_SCREEN_RATIO},E.prototype.appendTo=function(e){this.onscreen||(this.$el.remove(),this.$el.appendTo(e),this.onscreen=!0)},E.prototype.prependTo=function(e){this.onscreen||(this.$el.prependTo(e),this.onscreen=!0)},E.prototype.stash=function(e){this.onscreen&&(this.$el.remove(),this.onscreen=!1,e.append(this.$el))},E.prototype.remove=function(){this.onscreen&&(this.$el.remove(),this.onscreen=!1),this.cleanup()},E.prototype.cleanup=function(){var e=this.items;this.parent=null,S.remove(this);while(e.length>0)e.pop().cleanup()},E.prototype.lazyload=function(e){var t,n;if(!this.lazyloaded){for(t=0,n=this.$el.length;t<n;t++)e.call(this.$el[t],this.$el[t]);this.lazyloaded=!0}};var S=function(){var e=[];return{generatePageId:function(t){return e.push(t)-1},lookup:function(t){return t>=e.length?null:e[t]},remove:function(t){var n=t.id;return n>=e.length?!1:e[n]?(e[n]=null,!0):!1}}}();T.prototype.clone=function(){var e=new T(this.$el);return e.top=this.top,e.bottom=this.bottom,e.width=this.width,e.height=this.height,e},T.prototype.remove=function(){this.$el.remove(),x(this,this.parent),this.cleanup()},T.prototype.cleanup=function(){this.parent=null},i.ListView=f,i.Page=E,i.ListItem=T,i.noConflict=function(){return e.infinity=r,i}}(window,Math,jQuery);
View
24 infinity.js
@@ -79,6 +79,9 @@
this.lazy = !!options.lazy;
this.lazyFn = options.lazy || null;
+ // new callback for loading more content
+ this.more = !!options.more;
+ this.moreFn = options.more || null;
this.useElementScroll = options.useElementScroll === true;
@@ -591,13 +594,12 @@
// and disallows future scheduling.
function scrollHandler() {
- if(!scrollScheduled) {
+ if(!scrollScheduled) {
setTimeout(scrollAll, config.SCROLL_THROTTLE);
- scrollScheduled = true;
+ scrollScheduled = true;
}
}
-
-
+
// ### scrollAll
//
// Callback passed to the setTimeout throttle. Calls `scrollListView` on
@@ -607,7 +609,19 @@
function scrollAll() {
var index, length;
for(index = 0, length = boundViews.length; index < length; index++) {
- updateStartIndex(boundViews[index]);
+ var currentView = boundViews[index];
+ updateStartIndex(currentView);
+ var lastPage = currentView.pages[currentView.pages.length-1]
+ var lastItem = lastPage.items[lastPage.items.length-1]
+ if (lastPage.onscreen && currentView.more){
+ currentView.more = false;
+ // If we have reached the bottom of the list and the last element of the
+ // last page is onscreen, call the optional more callback and get more elements
+ currentView.moreFn(function(hasMore){
+ hasMore = typeof hasMore == 'boolean' ? hasMore : true;
+ currentView.more=hasMore;
+ })
+ }
}
scrollScheduled = false;
}
Something went wrong with that request. Please try again.