3

I'm a beginner in javascript and i'm trying to build some stuff with es6 spec.

I would like to recreate the pin effects from ScrollMagic and pin different section while i scroll down my page.

So I have this simple html markup with an header a footer and 3 section:

<header class="forewords">
 <h1>Some text</h1>
</header>

<div class="wrapper">
 <section class="project" id="item1">this is section 1</section>
 <section class="project" id="item2">this is section 2</section>
 <section class="project" id="item3">this is section 3</section>
</div>

<footer class="endings">
 <h1>some text</h1>
</footer>

And I've attached some styles to simulate a realistic situation.

Here comes the javascript logic:

Get all the projects:

const projects = Array.from(document.querySelectorAll('.project'));

Get all the projects offset from top and all the projects height:

let projectsOffsetTop = projects.map(project => project.offsetTop);
let projectsHeight = projects.map(project => project.offsetHeight);

Create a function to update the value if somebody resize the window:

function updateProjectsOffsetTop() {
  projectsOffsetTop = projects.map(project => project.offsetTop);
  projectsHeight = projects.map(project => project.offsetHeight);
};

window.addEventListener('resize', updateProjectsOffsetTop);

finaly pin the element if the scroll is greater than its offset.

function pinElement() {

  if (window.scrollY >= projectsOffsetTop[1]) {
    document.body.style.paddingTop = projectsHeight[1] +'px';
    projects[1].classList.add('fixed');
  } else {
    document.body.style.paddingTop = 0;
    projects[1].classList.remove('fixed');
  }

};

window.addEventListener('scroll', pinElement);

But i cant make it work with all the projects element. Even with for loop. What is the best practice? I want to solve this in Vanilla ES6 if it's possible.

Find attached the complete js fiddle.

Thanks

const projects = Array.from(document.querySelectorAll('.project'));
    let projectsOffsetTop = projects.map(project => project.offsetTop);
    let projectsHeight = projects.map(project => project.offsetHeight);

    function updateProjectsOffsetTop() {
      projectsOffsetTop = projects.map(project => project.offsetTop);
      projectsHeight = projects.map(project => project.offsetHeight);
    };

    function pinElement() {

      if (window.scrollY >= projectsOffsetTop[1]) {
        document.body.style.paddingTop = projectsHeight[1] +'px';
        projects[1].classList.add('fixed');
      } else {
        document.body.style.paddingTop = 0;
        projects[1].classList.remove('fixed');
      }

    };

    window.addEventListener('resize', updateProjectsOffsetTop);
    window.addEventListener('scroll', pinElement);
html {
      box-sizing: border-box;
      
    }

    *, *::before, *::after {
      box-sizing: inherit;
      margin: 0;
      padding: 0;
    }

    header, footer {
      width: 100%;
      padding: 10%;
      background-color: grey;
      position: relative;
    }

    .project {
      width: 100%;
      height: 100vh;
      position: relative;
      display: flex;
      justify-content: center;
      align-items: center;
      top: 0;
    }

    #item1 {background-color: yellow;}
    #item2 {background-color: blue;}
    #item3 {background-color: red;}


    .fixed {
      position: fixed;
    }
<header class="forewords"><h1>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Harum soluta ipsam quaerat cupiditate neque, necessitatibus amet nihil perferendis sunt minus! Exercitationem nulla inventore, aut beatae magnam, totam et minus hic.</h1>
  </header>

  <div class="wrapper">
    <section class="project" id="item1">this is section 1</section>
    <section class="project" id="item2">this is section 2</section>
    <section class="project" id="item3">this is section 3</section>
  </div>

  <footer class="endings"><h1>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae vel, perferendis ullam totam recusandae sed repellendus cum! Molestiae, aut ut sequi eos quidem nam quo est, ad tempora inventore odit.</h1>
  </footer>
mdash
  • 153
  • 2
  • 12

1 Answers1

3

You've provided an amazing MCVE to work on, so thank you so much for taking so much effort and time to ask a great question. The good news is that you are almost there! Your logic is sound, and everything makes sense. What you are really missing is this:

  • The correct placement of the logic to reset the styles (body top padding and removing the fixed class)
  • Getting the index of the .project element that is closest, but more than, the scroll height

What you want to do in your pinElement() method is the following:

  1. Reset/unfix everything first
  2. Get the projectsOffsetTop value that is more than scrollY, but the closest to it (so that it will be element that we want to pin)
  3. From that, get the index of the .project element that this value belongs to
  4. If the index is -1 (i.e. we do not have an element that fits the criteria in point 2), return and stop execution.
  5. Otherwise, we perform the logic you have in your original method, but substitute 1 with the index we have identified in step 3.

With that in mind, here is your slightly refactored pinElement() method:

function pinElement() {

  // Reset all styles
  projects.forEach((project) => {
    document.body.style.paddingTop = 0;
    project.classList.remove('fixed');
  });

  // Get the index of the project that is closest to top
  const valueClosestToScrollY = Math.max.apply(Math, projectsOffsetTop.filter((offsetTop) => offsetTop <= window.scrollY));
  const idx = projectsOffsetTop.indexOf(valueClosestToScrollY);

  // If index is not found, we don't do anything
  if (idx === -1)
    return;

  // Otherwise, we set the appropriate styles and classes
  if (window.scrollY >= projectsOffsetTop[idx]) {
    document.body.style.paddingTop = `${projectsHeight[idx]}px`;
    projects[idx].classList.add('fixed');
  }

};

Fun tip: you can use template literals to do this:

document.body.style.paddingTop = `${projectsHeight[idx]}px`;

…instead of this:

document.body.style.paddingTop = ${projectsHeight[idx] + 'px';

Here is a proof-of-concept example:

const projects = Array.from(document.querySelectorAll('.project'));
let projectsOffsetTop = projects.map(project => project.offsetTop);
let projectsHeight = projects.map(project => project.offsetHeight);

function updateProjectsOffsetTop() {
  projectsOffsetTop = projects.map(project => project.offsetTop);
  projectsHeight = projects.map(project => project.offsetHeight);
};

function pinElement() {

  // Reset all styles
  projects.forEach((project) => {
    document.body.style.paddingTop = 0;
    project.classList.remove('fixed');
  });

  // Get the index of the project that is closest to top
  const valueClosestToScrollY = Math.max.apply(Math, projectsOffsetTop.filter((offsetTop) => offsetTop <= window.scrollY));
  const idx = projectsOffsetTop.indexOf(valueClosestToScrollY);
  
  // If index is not found, we don't do anything
  if (idx === -1)
    return;

  // Otherwise, we set the appropriate styles and classes
  if (window.scrollY >= projectsOffsetTop[idx]) {
    document.body.style.paddingTop = `${projectsHeight[idx]}px`;
    projects[idx].classList.add('fixed');
  }

};

window.addEventListener('resize', updateProjectsOffsetTop);
window.addEventListener('scroll', pinElement);
html {
  box-sizing: border-box;
}

*,
*::before,
*::after {
  box-sizing: inherit;
  margin: 0;
  padding: 0;
}

header,
footer {
  width: 100%;
  padding: 10%;
  background-color: grey;
  position: relative;
}

.project {
  width: 100%;
  height: 100vh;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  top: 0;
}

#item1 {
  background-color: yellow;
}

#item2 {
  background-color: blue;
}

#item3 {
  background-color: red;
}

.fixed {
  position: fixed;
}
<header class="forewords">
  <h1>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Harum soluta ipsam quaerat cupiditate neque, necessitatibus amet nihil perferendis sunt minus! Exercitationem nulla inventore, aut beatae magnam, totam et minus hic.</h1>
</header>

<div class="wrapper">
  <section class="project" id="item1">this is section 1</section>
  <section class="project" id="item2">this is section 2</section>
  <section class="project" id="item3">this is section 3</section>
</div>

<footer class="endings">
  <h1>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae vel, perferendis ullam totam recusandae sed repellendus cum! Molestiae, aut ut sequi eos quidem nam quo est, ad tempora inventore odit.</h1>
</footer>

On a side note, for performance reasons, you might want to look into throttling/debouncing your scroll event, so that pinElement() is not called excessively.

Terry
  • 63,248
  • 15
  • 96
  • 118
  • Thank you so much for the solution! I need to study that `Math.max` thing but everything else is perfectly clear. I've tryed to add debounce but the amount of seconds needed to perform well is not that different from the normal refresh rate i think (with 10 milliseconds still getting some jumping :D ) I din't know about throttling. Maybe it's a better solution. – mdash May 02 '18 at 15:56
  • @mdash Ah, the `Math.max.apply` part is simply getting a max value out of an array of numbers :) https://stackoverflow.com/questions/1669190/find-the-min-max-element-of-an-array-in-javascript You can also use the object spread in ES6 if you want to. – Terry May 02 '18 at 16:36
  • I've made some test and some issue still remain. For example if I resize the windows while I've already some element fixed, I lose the header and everything broke. Furthermore, maybe the thing that i remove the class as the first operation of the function and than put it back everytime I scroll is the main issue in performance matter. Maybe I could flag the fixed item and remove the flag after the second one got the fixed class? – mdash May 04 '18 at 12:18
  • @mdash You probably have to re-run the `pinElement()` method on resize, too. That should fix it. – Terry May 04 '18 at 20:48