4

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?

  • That effetively is a bad match. `scroll` fires like a machine gun at each wheel spin... To each of these event, an `.each()` loop starts going throught X number of elements. Then... 5 seconds after, even is `element` was in scope, would it be the right one? Sure not! --- So you have to pass the element in a way that it will not be defined on scroll or in the each loop. I think that using an array out of the scroll handler is a clue... But I can't tell more for now. – Louys Patrice Bessette Aug 01 '18 at 23:00
  • Second taught that might help: I would store all visible elements in an array, **when the user has stopped scrolling**... And then, set a timeout for 5 seconds to compare the new visible elements with the array. Any other scrolling in between should clear the timeout... and wait for the scrolling to stop again. ;) – Louys Patrice Bessette Aug 01 '18 at 23:08
  • You can try [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). Its in experimental state but supported by all browsers except IE. – bigless Aug 01 '18 at 23:47

3 Answers3

1

You can use this function to check if an element is in the viewport (from this answer):

function isElementInViewport (el) {

    var rect = el.getBoundingClientRect();

    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
    );
}

<input id="inViewport"/>
<span style="margin-left: 9999px;" id="notInViewport">s</span>
<script>
function isElementInViewport (el) {

    var rect = el.getBoundingClientRect();

    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
    );
}
console.log("#inViewport in viewport: "+isElementInViewport(document.getElementById("inViewport")));
console.log("#notInViewport in viewport: "+isElementInViewport(document.getElementById("notInViewport")));
</script>
iota
  • 34,586
  • 7
  • 32
  • 51
1

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

Louys Patrice Bessette
  • 27,837
  • 5
  • 32
  • 57
0

You can try using Waypoints, its a library that allows you to determine when a element enters or leaves that viewport. You pass it an event handler that accepts a direction parameter. The direction tells you whether the tracked element entered or exited the screen. Once you detect the element has entered the screen then start a timer. If you don't see and event for when the element exited the viewport then you know it has been on screen for that period of time.

BeWarned
  • 2,212
  • 1
  • 15
  • 22