0

I have created an array of elements that I would like to continually loop through on window scroll checking if element is visible and if so setting it as active and then I would like to remove this element from the array eventually doing a check to see if the array is empty in order to unbind the scroll event.

At the moment I'm having some difficulty knowing what way I should remove this element? I'm currently using:

var index = innerItems.index($thisEl.index());
innerItems.splice(index, 1);

This however seems to screw up the function I'm using to check the elements in view and my array length never seems to change.

Can anyone recommend how I can achieve my target of being able to remove each element as it becomes active until my array is empty and unbind the scroll event? Also if anyone can offer any improvements that would be amazing.

Codepen http://codepen.io/styler/pen/zDJrx

JS

var $mainContainer = $('.main-container'),
    innerItems = $mainContainer.children();

function isElementInViewport (el) {

    //special bonus for those using jQuery
    if (typeof jQuery === "function" && el instanceof jQuery) {
        el = el[0];
    }

    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() */
    );
}

function init() {
  itemChecker();
}

init();

$(window).on('scroll.windowScroll', itemChecker);

function itemChecker() {
  innerItems.each(function(i, element) {
    console.log('Index', i);
    console.log('Element', element);

    var $thisEl = $(element);

    // if isElementInViewport then add class is-active and remove from innerItems array
    var inView = isElementInViewport(element);

    if( inView ) {
      $thisEl.addClass('is-active');

      // Remove each element as it becomes ready/in view
      var index = innerItems.index($thisEl.index());
      innerItems.splice(index, 1);
    }

    console.log('innerItems length', innerItems.length);

    if( innerItems.length === 0 ) {
      $(window).off('scroll.windowScroll');
    }
  });
}
styler
  • 13,569
  • 20
  • 73
  • 127
  • Are you sure *"turning the scroll off"* is going to give a pleasant user experience...? 0.o just asking... – T J Sep 29 '14 at 18:29
  • Just so you know, when scrolling down the top of the rectangles go negative, so its never > 0. And problem is using each and then removing an element, causes trouble. You should traverse in inverse order: `for(var index=innerItems.length-1;index>=0;index--){` – juvian Sep 29 '14 at 18:31
  • hi @juvian sorry I'm not completely sure I follow what you're saying? – styler Sep 29 '14 at 18:34
  • @TJ apologies I mean't turning off the scroll event in order to prevent the itemChecker checker from running when no longer needed – styler Sep 29 '14 at 18:35
  • `rect.top >= 0` thats almost always false, so your `isElementInViewport` function return false most of the time. Should change for `rect.top <= 0` – juvian Sep 29 '14 at 18:36
  • @juvian and how can I remove each active element from my array? the isElementInViewport function was written by John Resig http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport – styler Sep 29 '14 at 18:37
  • I think your way of removing works fine, but you should check if `element != undefined` before calling `isElementInViewport` – juvian Sep 29 '14 at 18:51

2 Answers2

2

You have a flaw in your loop. You can not loop trough array with .each and removing it's items in the loop. The loop will expect initial number of items and when it comes to the index that is removed it will spit undefined error.

In such cases you should use inverse loop basically going from the end of the array to the start. Or looping normally but updating the index variable in cases you are removing the item from the array.

Example of inverse loop:

for (var i=arr.length;i--;) {
    if (i%2==0) {
         arr.splice(i, 1);
         // We removed the item but this will not 
         // interfere with our counting as we are doing it in reverse
    }
}

Example of normal loop with index updating:

for (var i=0,len=arr.length;i<len;i++) {
    if (i%2==0) {
         arr.splice(i, 1);
         // We removed the item from the array and we need to decrease it's length by one
         len--;
    }
}

To get back to your example here is an updated version, forked codepen with fixed code here..

var $mainContainer = $('.main-container'),
    innerItemsCache = $mainContainer.children(),
    innerItemsVisible;

function isElementInViewport (el) {

    //special bonus for those using jQuery
    if (typeof jQuery === "function" && el instanceof jQuery) {
        el = el[0];
    }

    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() */
    );
}

function init() {
  itemChecker();
}

init();

$(window).on('scroll.windowScroll', itemChecker);

function itemChecker() {
  innerItemsVisible = [];
  for (var i=innerItemsCache.length; i--;) {
    console.log('Index', i);
    console.log('Element', element);
    var element = innerItemsCache[i];
    var $thisEl = $(element);

    // if isElementInViewport then add class is-active and remove from innerItems array
    var inView = isElementInViewport(element);

    if( inView ) {
      $thisEl.addClass('is-active');

      // Add elements that are visible
      innerItemsVisible.push(innerItemsCache[i]);
    } else {
      $thisEl.removeClass('is-active');
    }

    console.log('innerItems length', innerItemsVisible.length);
  }

   if( innerItemsVisible.length === 0 ) {
      $(window).off('scroll.windowScroll');
  }

}

As you see I've added a caching array so you don't have to search for all the items in each iteration. In the same time since you already are looping trough all of the items it's easier to just create empty array and fill it with visible items as in this working example..

Goran.it
  • 5,001
  • 2
  • 19
  • 25
  • I'm not sure this code achieves what I need, the innerItemsVisible array length is instantly set to 0 so as each item come in to view nothing gets set? So when I scroll I instantly go inside the off conditional even though I have yet to scroll the second element in to view – styler Sep 29 '14 at 19:45
  • Did you try link of updated codepen ? innerItemsVisible contains visible items on each update. Basically when the function is triggered it empties that array and fills it with visible items. So after the function has finished its processing the array is updated and it will always contain visible items. http://codepen.io/anon/pen/zflLn – Goran.it Sep 29 '14 at 19:50
  • Hey yeah I tried the pen but I'm a little confused by it, basically as each item becomes visible it should get the class .is-active but in your demo the first element gets is-active then as soon as i scroll this class is removed and no other elements ever get is-active? – styler Sep 29 '14 at 19:52
  • Well I have a large monitor and while scrolling the 3 displayed div's are having is-active class while other div's are without it (that's because I've added removeClass in the code). So If I scroll down the page, next 3 rows that are visible get the active class and in the same time innerItemsVisible contains references to those nodes. – Goran.it Sep 29 '14 at 19:55
  • I think the issue is with this if( innerItemsVisible.length === 0 ) { $(window).off('scroll.windowScroll'); } Im not sure that's the correct check? – styler Sep 29 '14 at 19:57
  • That's correct check, if you use your console and remove non visible nodes by hand .. scroll event handler will be removed and checking of visible nodes will not continue – Goran.it Sep 29 '14 at 20:01
  • if I log out innerItemsVisible inside the inView conditional it only ever gets logged out once? – styler Sep 29 '14 at 20:05
  • because the if( innerItemsVisible.length === 0 ) { $(window).off('scroll.windowScroll'); } is outside the for loop it gets run instantly as there would be 0 itemsVisible going in reverse order, would I be right? – styler Sep 29 '14 at 20:08
  • Sorry I'm just trying to understand this but I'm really unsure how this solves my issue – styler Sep 29 '14 at 20:10
  • Noup .. when the loop ends innerItemsVisible contains visible items as they are added inside the loop. First we empty the array, then during the loop we populate it with visible items. After that the loop ends and we check if there are any visible items inside it and if no we delete the event handler – Goran.it Sep 29 '14 at 20:13
  • So ideally I would like the array reduce in length when an element becomes active so on init my array length is 10, if 1 element is visible this reduces the array length to 9. when I scroll and another element becomes visible my array which is now 9 gets reduced to 8. at the moment your example continually loops through 10 elements every time – styler Sep 29 '14 at 20:14
  • My code is just a example of the loop, what it does is taking care that during the scroll it updates innerItemsVisible array with visible items and adds/removes their classes. If you would like to remove the item from initial array here is an updated fork : http://codepen.io/anon/pen/KwyBb – Goran.it Sep 29 '14 at 20:32
  • Thanks so much for your patience! Sorry I got a little confused during this as I wasn't sure what you where totally getting at with the array but I totally get it now!! – styler Sep 29 '14 at 20:38
0

If it is ok to use plugin, you can achieve the same functionality as simple as:

$('.inner-container').waypoint(function () {
    $(this).addClass('is-active');
    if($(this).is(".inner-container:last-child")){
       alert("last item in view, destroying the functionality as you mentioned in comments");
       $(this).waypoint('destroy');
    }
}, {
    offset: 'bottom-in-view'
});
* {
    box-sizing:border-box
}
body {
    padding:0;
    margin:0
}
.main-container {
}
.inner-container {
    background: rgba(255, 0, 0, .4);
    width: 100%;
    height: 200px;
    border: 10px solid white;
}
.inner-container.is-active {
    background: rgba(255, 0, 0, .7);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/waypoints/2.0.3/waypoints.min.js"></script>
<div class="main-container">
    <div class="inner-container"></div>
    <div class="inner-container"></div>
    <div class="inner-container"></div>
    <div class="inner-container"></div>
    <div class="inner-container"></div>
    <div class="inner-container"></div>
    <div class="inner-container"></div>
    <div class="inner-container"></div>
    <div class="inner-container"></div>
    <div class="inner-container"></div>
</div>
T J
  • 40,740
  • 11
  • 73
  • 131