سؤال

I guess all of you are familiar with reloading elements on a website via ajax once the bottom of the page is reached.

I tried to this myself but it seems my script is getting triggered multiple times …

<script type="text/javascript">
        $(document).ready(function() {

            $(window).scroll(function() {
                if ( $(window).scrollTop() + $(window).height() == $(document).height() ) {
                      myfunctionCall();

Any idea how I can stop this from beeing called multiple times? I guess this only happens on a mac with the "smooth scrolling" or whatever you might call that.

Kind regards, Matt

هل كانت مفيدة؟

المحلول

One really simple option is to use a variable to store whether or not the function should be called.

<script type="text/javascript">
$(document).ready(function() {
  var callFunction = true;
  $(window).scroll(function() {
    if ( callFunction && $(window).scrollTop() + $(window).height() == $(document).height() ) {
      myfunctionCall();
      callFunction = false;
    }
  }
}
</script>

نصائح أخرى

Try this. When user scrolls to this div at page bottom, function runs.

<div id="loadMore"></div>

When working with the scroll function you need to be careful as the event is triggered once for every pixel scrolled. Therefore you need to use a timeout to only call the function when scrolling has stopped.

function isScrolledIntoView(elem) {
    var docViewTop = $(window).scrollTop();
    var docViewBottom = docViewTop + $(window).height();
    var elemTop = $(elem).offset().top;
    var elemBottom = elemTop + $(elem).height();

    console.log(docViewTop, docViewBottom, elemTop, elemBottom);
    return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) && (elemBottom <= docViewBottom) && (elemTop >= docViewTop));

}

var timer;
$(window).scroll(function () {
    clearTimeout(timer);
    timer = setTimeout(function() {
        if (isScrolledIntoView($('#loadMore'))) {
            alert('reached');
            myfunctionCall();
            return false;
        }
    }, 50);
});

Here is a fiddle that demonstrates the call. http://jsfiddle.net/2KhjJ/

You can set a variable to prevent the scroll callback to fire multiple times, but make sure it will work in conjuction with the probable async fashion of myfunctionCall, used to retrieve and append data.

An example,

http://jsbin.com/OfaKarek/1/edit

html

<div id="container">
  </div>
  
  <div id="popup">loading....</div>

css

#popup{
  display:none;
  position:fixed;
  left:25%;
  top:25%;
  background-color:grey;
}

js

var counter=1;
function generateData(){
  var text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu lectus viverra, convallis enim interdum, viverra nisl.";
  var limit = counter+50;
  for(counter;counter<limit;counter++){
    var data=("<p>"+counter+text+"</p>");
    $('#container').append(data);
  }
}

function myfunctionCall(callback){
  $("#popup").show();
  /*retrieve data, mimic an async ajax call*/
  setTimeout(function(){
/*this is the part where the data returned from the call are appended*/
    generateData();
    $("#popup").hide();
    callback();
  },4000);
}

$(document).ready(function() {
  
  generateData();

  var retrievingData = false;
  $(window).scroll(function() {
    
    if ( !retrievingData && $(window).scrollTop() + $(window).height() == $(document).height() ) {
      retrievingData = true;
      myfunctionCall(function(){retrievingData = false;});
    }
  });
                                      
});

So it has been assumed that myfunctionCall or another function will probably be related to retrieving the data asynchronously by making an ajax call. In order to make the function irrelevant to the scroll event, a callback has been accommodated. A callback with myFunctionCall is being used to set the value of the variable in an appropriate context as is within the scroll event function.

Also, there are plenty of solutions for achieving an infinite or lazy load scroll effect. One of which is http://www.infinite-scroll.com/ with many options created by Paul Irish, i have used in the past and it worked great.

If you know what elements are being added once you reach the bottom, you could use this to your advantage. Don't trigger a callback only on reaching the bottom of the page, but in combination with the last element being shown. What I have used to implement continuous scrolling is something like this:

$(window).on("scroll", function ()
{
    // Get last shown element
    var trigger = $(".elements:last-child()"); 

    // Trigger callback when the top of the last element comes in view
    if ($(window).scrollTop() + $(window).height() > trigger.offset().top)
        myfunctionCall();
});
jQuery Waypoints - v2.0.3
Copyright (c) 2011-2013 Caleb Troughton
Dual licensed under the MIT license and GPL license.
https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt
*/
(function () { var t = [].indexOf || function (t) { for (var e = 0, n = this.length; e < n; e++) { if (e in this && this[e] === t) return e } return -1 }, e = [].slice; (function (t, e) { if (typeof define === "function" && define.amd) { return define("waypoints", ["jquery"], function (n) { return e(n, t) }) } else { return e(t.jQuery, t) } })(this, function (n, r) { var i, o, l, s, f, u, a, c, h, d, p, y, v, w, g, m; i = n(r); c = t.call(r, "ontouchstart") >= 0; s = { horizontal: {}, vertical: {} }; f = 1; a = {}; u = "waypoints-context-id"; p = "resize.waypoints"; y = "scroll.waypoints"; v = 1; w = "waypoints-waypoint-ids"; g = "waypoint"; m = "waypoints"; o = function () { function t(t) { var e = this; this.$element = t; this.element = t[0]; this.didResize = false; this.didScroll = false; this.id = "context" + f++; this.oldScroll = { x: t.scrollLeft(), y: t.scrollTop() }; this.waypoints = { horizontal: {}, vertical: {} }; t.data(u, this.id); a[this.id] = this; t.bind(y, function () { var t; if (!(e.didScroll || c)) { e.didScroll = true; t = function () { e.doScroll(); return e.didScroll = false }; return r.setTimeout(t, n[m].settings.scrollThrottle) } }); t.bind(p, function () { var t; if (!e.didResize) { e.didResize = true; t = function () { n[m]("refresh"); return e.didResize = false }; return r.setTimeout(t, n[m].settings.resizeThrottle) } }) } t.prototype.doScroll = function () { var t, e = this; t = { horizontal: { newScroll: this.$element.scrollLeft(), oldScroll: this.oldScroll.x, forward: "right", backward: "left" }, vertical: { newScroll: this.$element.scrollTop(), oldScroll: this.oldScroll.y, forward: "down", backward: "up" } }; if (c && (!t.vertical.oldScroll || !t.vertical.newScroll)) { n[m]("refresh") } n.each(t, function (t, r) { var i, o, l; l = []; o = r.newScroll > r.oldScroll; i = o ? r.forward : r.backward; n.each(e.waypoints[t], function (t, e) { var n, i; if (r.oldScroll < (n = e.offset) && n <= r.newScroll) { return l.push(e) } else if (r.newScroll < (i = e.offset) && i <= r.oldScroll) { return l.push(e) } }); l.sort(function (t, e) { return t.offset - e.offset }); if (!o) { l.reverse() } return n.each(l, function (t, e) { if (e.options.continuous || t === l.length - 1) { return e.trigger([i]) } }) }); return this.oldScroll = { x: t.horizontal.newScroll, y: t.vertical.newScroll } }; t.prototype.refresh = function () { var t, e, r, i = this; r = n.isWindow(this.element); e = this.$element.offset(); this.doScroll(); t = { horizontal: { contextOffset: r ? 0 : e.left, contextScroll: r ? 0 : this.oldScroll.x, contextDimension: this.$element.width(), oldScroll: this.oldScroll.x, forward: "right", backward: "left", offsetProp: "left" }, vertical: { contextOffset: r ? 0 : e.top, contextScroll: r ? 0 : this.oldScroll.y, contextDimension: r ? n[m]("viewportHeight") : this.$element.height(), oldScroll: this.oldScroll.y, forward: "down", backward: "up", offsetProp: "top" } }; return n.each(t, function (t, e) { return n.each(i.waypoints[t], function (t, r) { var i, o, l, s, f; i = r.options.offset; l = r.offset; o = n.isWindow(r.element) ? 0 : r.$element.offset()[e.offsetProp]; if (n.isFunction(i)) { i = i.apply(r.element) } else if (typeof i === "string") { i = parseFloat(i); if (r.options.offset.indexOf("%") > -1) { i = Math.ceil(e.contextDimension * i / 100) } } r.offset = o - e.contextOffset + e.contextScroll - i; if (r.options.onlyOnScroll && l != null || !r.enabled) { return } if (l !== null && l < (s = e.oldScroll) && s <= r.offset) { return r.trigger([e.backward]) } else if (l !== null && l > (f = e.oldScroll) && f >= r.offset) { return r.trigger([e.forward]) } else if (l === null && e.oldScroll >= r.offset) { return r.trigger([e.forward]) } }) }) }; t.prototype.checkEmpty = function () { if (n.isEmptyObject(this.waypoints.horizontal) && n.isEmptyObject(this.waypoints.vertical)) { this.$element.unbind([p, y].join(" ")); return delete a[this.id] } }; return t }(); l = function () { function t(t, e, r) { var i, o; r = n.extend({}, n.fn[g].defaults, r); if (r.offset === "bottom-in-view") { r.offset = function () { var t; t = n[m]("viewportHeight"); if (!n.isWindow(e.element)) { t = e.$element.height() } return t - n(this).outerHeight() } } this.$element = t; this.element = t[0]; this.axis = r.horizontal ? "horizontal" : "vertical"; this.callback = r.handler; this.context = e; this.enabled = r.enabled; this.id = "waypoints" + v++; this.offset = null; this.options = r; e.waypoints[this.axis][this.id] = this; s[this.axis][this.id] = this; i = (o = t.data(w)) != null ? o : []; i.push(this.id); t.data(w, i) } t.prototype.trigger = function (t) { if (!this.enabled) { return } if (this.callback != null) { this.callback.apply(this.element, t) } if (this.options.triggerOnce) { return this.destroy() } }; t.prototype.disable = function () { return this.enabled = false }; t.prototype.enable = function () { this.context.refresh(); return this.enabled = true }; t.prototype.destroy = function () { delete s[this.axis][this.id]; delete this.context.waypoints[this.axis][this.id]; return this.context.checkEmpty() }; t.getWaypointsByElement = function (t) { var e, r; r = n(t).data(w); if (!r) { return [] } e = n.extend({}, s.horizontal, s.vertical); return n.map(r, function (t) { return e[t] }) }; return t }(); d = { init: function (t, e) { var r; if (e == null) { e = {} } if ((r = e.handler) == null) { e.handler = t } this.each(function () { var t, r, i, s; t = n(this); i = (s = e.context) != null ? s : n.fn[g].defaults.context; if (!n.isWindow(i)) { i = t.closest(i) } i = n(i); r = a[i.data(u)]; if (!r) { r = new o(i) } return new l(t, r, e) }); n[m]("refresh"); return this }, disable: function () { return d._invoke(this, "disable") }, enable: function () { return d._invoke(this, "enable") }, destroy: function () { return d._invoke(this, "destroy") }, prev: function (t, e) { return d._traverse.call(this, t, e, function (t, e, n) { if (e > 0) { return t.push(n[e - 1]) } }) }, next: function (t, e) { return d._traverse.call(this, t, e, function (t, e, n) { if (e < n.length - 1) { return t.push(n[e + 1]) } }) }, _traverse: function (t, e, i) { var o, l; if (t == null) { t = "vertical" } if (e == null) { e = r } l = h.aggregate(e); o = []; this.each(function () { var e; e = n.inArray(this, l[t]); return i(o, e, l[t]) }); return this.pushStack(o) }, _invoke: function (t, e) { t.each(function () { var t; t = l.getWaypointsByElement(this); return n.each(t, function (t, n) { n[e](); return true }) }); return this } }; n.fn[g] = function () { var t, r; r = arguments[0], t = 2 <= arguments.length ? e.call(arguments, 1) : []; if (d[r]) { return d[r].apply(this, t) } else if (n.isFunction(r)) { return d.init.apply(this, arguments) } else if (n.isPlainObject(r)) { return d.init.apply(this, [null, r]) } else if (!r) { return n.error("jQuery Waypoints needs a callback function or handler option.") } else { return n.error("The " + r + " method does not exist in jQuery Waypoints.") } }; n.fn[g].defaults = { context: r, continuous: true, enabled: true, horizontal: false, offset: 0, triggerOnce: false }; h = { refresh: function () { return n.each(a, function (t, e) { return e.refresh() }) }, viewportHeight: function () { var t; return (t = r.innerHeight) != null ? t : i.height() }, aggregate: function (t) { var e, r, i; e = s; if (t) { e = (i = a[n(t).data(u)]) != null ? i.waypoints : void 0 } if (!e) { return [] } r = { horizontal: [], vertical: [] }; n.each(r, function (t, i) { n.each(e[t], function (t, e) { return i.push(e) }); i.sort(function (t, e) { return t.offset - e.offset }); r[t] = n.map(i, function (t) { return t.element }); return r[t] = n.unique(r[t]) }); return r }, above: function (t) { if (t == null) { t = r } return h._filter(t, "vertical", function (t, e) { return e.offset <= t.oldScroll.y }) }, below: function (t) { if (t == null) { t = r } return h._filter(t, "vertical", function (t, e) { return e.offset > t.oldScroll.y }) }, left: function (t) { if (t == null) { t = r } return h._filter(t, "horizontal", function (t, e) { return e.offset <= t.oldScroll.x }) }, right: function (t) { if (t == null) { t = r } return h._filter(t, "horizontal", function (t, e) { return e.offset > t.oldScroll.x }) }, enable: function () { return h._invoke("enable") }, disable: function () { return h._invoke("disable") }, destroy: function () { return h._invoke("destroy") }, extendFn: function (t, e) { return d[t] = e }, _invoke: function (t) { var e; e = n.extend({}, s.vertical, s.horizontal); return n.each(e, function (e, n) { n[t](); return true }) }, _filter: function (t, e, r) { var i, o; i = a[n(t).data(u)]; if (!i) { return [] } o = []; n.each(i.waypoints[e], function (t, e) { if (r(i, e)) { return o.push(e) } }); o.sort(function (t, e) { return t.offset - e.offset }); return n.map(o, function (t) { return t.element }) } }; n[m] = function () { var t, n; n = arguments[0], t = 2 <= arguments.length ? e.call(arguments, 1) : []; if (h[n]) { return h[n].apply(null, t) } else { return h.aggregate.call(null, n) } }; n[m].settings = { resizeThrottle: 100, scrollThrottle: 30 }; return i.load(function () { return n[m]("refresh") }) }) }).call(this);

Include the above code. It is waypoint plugin.

Use it like this:

$(window).waypoint(function() {
  myFunctionCall() //This function will be called after the page is scrolled to the bottom 
}, { offset: '100%' });

For more information: Waypoint

Mark it as answer, if it helps :)

There's nothing in your logic that would prevent the function from being called multiple times, so the code's doing exactly what you ask it. Browsers act differently when triggering the "scroll" event. Some do it once per scroll action, while others fire the event continuously as long as the user is scrolling.

One way to fix your problem is to "debounce" the event so that it only fires once every so many milliseconds. The exact way to handle debouncing will depend on your specific requirements. If you are using (or can use) the Underscore.JS utility library, there's a built-in debounce() function. If you don't want to use the whole library, you could copy its code. (It's only a few lines.) If you're targeting modern browsers, you can use requestAnimationFrame events to debounce.

Dr. Google has lots of info.

You can use solution by @rtcherry, but I just want to add some thoughts:

  1. There are many plugins, that will help with it. Just google "infinite scroll".
  2. Using such construction:

    if ( $(window).scrollTop() + $(window).height() == $(document).height() ) {
        // code
    }
    

    means, that "code" will run in very bottom of page... I recommend make such condition:

    if ( $(window).scrollTop() + $(window).height() + 500 == $(document).height() ) {
    

    in this case code will be runned little earlier and user wouldn't wait for result in very bottom. You can use any other number instead of "500", it was just for demo.

  3. As far as I know, "scroll" is firing multiple times in all browsers and OS, not only on mac.

You could unbind the scroll event in your myfunctionCall method and rebind it later when you need it again. Use jqueries on() and off() methods. I think this approach is more clean then adding a global boolean variable.

   var myfunctionCall= function(){
       win=$(this);//we use the function on the window, so $(this) is the window
       if ( win.scrollTop() + win.height() == $(document).height() ){
          //first unbind the scroll event
           win.off('scroll',myfunctionCall);
          //then do your (assync) stuff
           alert('this can be anything');
      } 
    }

    $(document).ready(function() {
       //bind the scroll handler on document ready. You can do this whenever you want 
       //to rebind the handler.
       $(window).on('scroll', myfunctionCall);
    });
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top