javascriptjquerysettimeoutviewportscrolltop

JavaScript – detect if element stays in viewport for n seconds


I’m need to push a data layer event whenever a content block of a certain css class is visible for 5 seconds (a sign that the user is reading the content.

Ive used something like this:

$(window).on(‘scroll resize’, function() {
  $(‘.myClass’).each(function(element) {
    If (isInViewport(element)) {
      setTimeout(function() {
        if (isInViewport(element)) {
          ... // Push the data layer event.
        }
      }, 5000);
    }
  });
});

function isInViewport(element) {
  ... // Returns true if element is visible.
};

Just wrote this from memory, so it may not be 100% correct, but the gist is I try to:

  1. Test visibility on every myClass element on scroll/resize
  2. If one is visible, wait 5 seconds and check the same element one more time.

Trouble is, element is undefined when setTimeout runs isInViewport. Maybe jQuery’s .each and setTimeout are a bad match?


Solution

  • I used the jquery-visible plugin to achieve a script that will output the time (in seconds) since a particular element is in view. The output uses an interval of X seconds... out of the scroll handler.

    On stop scrolling, we check all the monitored elements to know if they're in the viewport.

    If an element is, we check if it already was logged in the visible_begins array on a previous scroll stop. If it isn't, we push an object containing its id and the actual time in milliseconds.

    Still on scroll stop, if an element isn't in the viewport, we check if it was logged in the visible_begins and if it's the case, we remove it.

    Now on an interval of X seconds (your choice), we check all the monitored elements and each that is still in viewport is outputed with the time differential from now.

    console.clear();
    
    var scrolling = false;
    var scrolling_timeout;
    var reading_check_interval;
    var reading_check_delay = 5;    // seconds
    var completePartial = false;     // "true" to include partially in viewport
    var monitored_elements = $(".target");
    var visible_begins = [];
    
    
    // Scroll handler
    $(window).on("scroll",function(){
      if(!scrolling){
        console.log("User started scrolling.");
      }
      scrolling = true;
    
      clearTimeout(scrolling_timeout);
      scrolling_timeout = setTimeout(function(){
        scrolling = false;
        console.log("User stopped scrolling.");
    
        // User stopped scrolling, check all element for visibility
        monitored_elements.each(function(){
          if($(this).visible(completePartial)){
            console.log(this.id+" is in view.");
    
            // Check if it's already logged in the visible_begins array
            var found = false;
            for(i=0;i<visible_begins.length;i++){
              if(visible_begins[i].id == this.id){
                found = true;
              }
            }
            if(!found){
              // Push an object with the visible element id and the actual time
              visible_begins.push({id:this.id,time:new Date().getTime()});
            }
          }
        });
      },200);   // scrolling delay, 200ms is good.
    }); // End on scroll handler
    
    
    // visibility check interval
    reading_check_interval = setInterval(function(){
      monitored_elements.each(function(){
        if($(this).visible(completePartial)){
          // The element is visible
          // Check all object in the array to fing this.id
          for(i=0;i<visible_begins.length;i++){
            if(visible_begins[i].id == this.id){
              var now = new Date().getTime();
              var readTime = ((now-visible_begins[i].time)/1000).toFixed(1);
              console.log(visible_begins[i].id+" is in view since "+readTime+" seconds.")
            }
    
          }
        }else{
          // The element is not visible
          // Remove it from thevisible_begins array if it's there
          for(i=0;i<visible_begins.length;i++){
            if(visible_begins[i].id == this.id){
              visible_begins.splice(i,1);
              console.log(this.id+" was removed from the array.");
            } 
          }
        }
      });
    },reading_check_delay*1000);  // End interval
    .target{
      height:400px;
      border-bottom:2px solid black;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-visible/1.2.0/jquery.visible.min.js"></script>
    
    <div id="one" class="target">1</div>
    <div id="two" class="target">2</div>
    <div id="three" class="target">3</div>
    <div id="four" class="target">4</div>
    <div id="five" class="target">5</div>
    <div id="six" class="target">6</div>
    <div id="seven" class="target">7</div>
    <div id="eight" class="target">8</div>
    <div id="nine" class="target">9</div>
    <div id="ten" class="target">10</div>

    Please run the snippet in full page mode, since there is a couple console logs.

    CodePen