Need help reading regular expression

This is the script I’m using:

There’s a working demo at https://codepen.io/paulobrien/full/EjwdeG/

There’s still the whole jQuery thing to resolve.

This might be a good opportunity for me to demonstrate the conversion process of going from jQuery to vanilla JS.

1 Like

Replace document ready

Starting from the top, the document ready part can be removed. Scripts should be run from the end of the body, which codePen does for us by default.

// $(document).ready(function() {
    ...
// });

The updated code still works after that too.

Replace window height

Next, the $(window).height() can be replaced with window.innerHeight.

// var winHeight = $(window).height(),
var winHeight = window.innerHeight,

Here is the update with that innerHeight part: https://codepen.io/pmw57/pen/mdOLMpj

Replace on event

This replacement is an easy one, we just replace the on keyword with `addEventListener instead.

$(window).on('scroll', function() {
window.addEventListener('scroll', function() {

The updated code that uses addEventListener is at https://codepen.io/pmw57/pen/RwoyZBY

Replace $(…) with document.querySelector or querySelectorAll

When the first matching element is desired, we use querySelector. When multiple elements are wanted we use querySelectorAll instead.

  // $('.scroll li').each(function() {
  $(document.querySelectorAll('.scroll li')).each(function() {

Currently we are still using $(…) because each requires that, but we’ll work on that next.

The updated code using querySelectorAll is found at https://codepen.io/pmw57/pen/MWbGvqy

Replacing the each command

The next part to get replaced is the each command. Here we can use the array forEach method instead.

The each command is tricksy because the this keyword is different, and it uses index and item function parameters in reverse to the forEach item and index parameters.

There are faster ways to perform this conversion, but the way that I’m doing it here allows the code to keep running in the same way that it was before, while performing the change.

To make the each command easier to convert, we update the code to use the index and item parameters:

  // $(document.querySelectorAll('.scroll li')).each(function() {
  $(document.querySelectorAll('.scroll li')).each(function(i, el) {

We can now replace the this keywords with el instead.

    // var thisTop = $(this).offset().top - $(window).scrollTop();
    var thisTop = $(el).offset().top - $(window).scrollTop();
    // if (thisTop >= topLimit && (thisTop + $(this).height()) <= bottomLimit) {
    if (thisTop >= topLimit && (thisTop + $(el).height()) <= bottomLimit) {
      // $(this).addClass('highlight')
      $(el).addClass('highlight')
    } else {
      // $(this).removeClass('highlight')
      $(el).removeClass('highlight')
    }

We can now replace the each command with forEach instead, being sure to switch the function parameters, and as the i parameter isn’t needed we can remove that one too.

  // $(document.querySelectorAll('.scroll li')).each(function(i, el) {
  document.querySelectorAll('.scroll li').forEach(function(el) {

The code with the updated forEach method is found at https://codepen.io/pmw57/pen/GRNdvPP

Replace offset top

The offset top is represented in vanilla JS with the offsetTop value:

    var thisTop = $(el).offset().top - $(window).scrollTop();

This is also a good time to rename thisTop to a better name, such as viewportOffset.

    var viewportOffset = el.offsetTop - $(window).scrollTop();
    if (viewportOffset >= topLimit && (viewportOffset + $(el).height()) <= bottomLimit) {

The updated code using offsetTop and the renamed viewportOffset is at https://codepen.io/pmw57/pen/OJbZxOV

Replace scrollTop

The scrollTop parameter is switched over to vanilla JavaScript by using scrollY

    // var viewportOffset = el.offsetTop - $(window).scrollTop();
    var viewportOffset = el.offsetTop - window.scrollY;

The updated code using scrollY is found at https://codepen.io/pmw57/pen/LYbmzeM

Replace height

The height of an element is kept in the clientHeight property:

    // if (viewportOffset >= topLimit && (viewportOffset + $(el).height()) <= bottomLimit) {
    if (viewportOffset >= topLimit && (viewportOffset + el.clientHeight) <= bottomLimit) {

The updated code using clientHeight is found at https://codepen.io/pmw57/pen/MWbGEVE

Replace class code

Adding and removing classnames from elements is easily achieved using the classList API.

      // $(el).addClass('highlight')
      el.classList.add('highlight');
    } else {
      // $(el).removeClass('highlight')
      el.classList.remove('highlight');
    }

As semicolons are used throughout most of the code, I’ve added in some missing ones there too.

The updated code using the classList API is found at https://codepen.io/pmw57/pen/YzpLrjm

Remove the jQuery library

Now that nothing of jQuery us being used by the code, we can remove the library completely, freeing up vast amounts of that huge library from needing to be downloaded when someone loads the page.

With codePen, we go to settings, and in the JS section we can remove jQuery from the list of external scripts.

The code with the jQuery library fully removed is found at https://codepen.io/pmw57/pen/NWbMaOd

Next steps

There’s more to be done with that code, but it is completely free of the jQuery library now.

I’ll take a quick scan through the code and recommend other improvements to be made to it from here.

2 Likes

Edit: I deleted my previous post, as I don’t think it was particularly helpful

Worthy of consideration would be Intersection Observer

A few more links

A Few Functional Uses for Intersection Observer

Intersection Observer vs EventListener Scroll

1 Like

One variable per line

Comma-separated variables become more difficult to refactor. We tend to start programming with one per line, think we’re clever with comma allowing multiple declarations in a single statement, then get pain when trying to improve that code, resulting in us realizing that one variable statement per line.

// var winHeight = window.innerHeight,
//     topLimit = winHeight * .2,
//     bottomLimit = winHeight * .8;
var winHeight = window.innerHeight;
var topLimit = winHeight * .2;
var bottomLimit = winHeight * .8;

Use leading zero’s

Without the leading zero it’s very easy to mistake the decimal point and ignore it, or confuse it for screen grime.

Putting in the leading zero helps to provide a more rapid understanding of the code.

// var topLimit = winHeight * .2;
// var bottomLimit = winHeight * .8;
var topLimit = winHeight * 0.2;
var bottomLimit = winHeight * 0.8;

Make percentages even clearer

It can be even clearer too that you intend it to be as a percentage, by making the 20/100 more explicit.

var topLimit = winHeight * 20 / 100;
var bottomLimit = winHeight * 80 / 100;

Extract functions

When the addEventListener and forEach functions are named, it’s much easier to extract those functions, making it easier to understand the forEach and addEventHandler lines of code.

function highlightElement(el) {
  //...
}
function scrollHighlightHandler() {
  document.querySelectorAll('.scroll li').forEach(highlightElement);
}
window.addEventListener('scroll', scrollHighlightHandler);

Move the if-statement condition to a function

The condition of the if statement is easier to understand when the function name helps to summarize exactly what is going on there.

function isWithinLimits(el, topLimit, bottomLimit) {
  var viewportOffset = el.offsetTop - window.scrollY;
  return viewportOffset >= topLimit && (viewportOffset + el.clientHeight) <= bottomLimit;
}
//...
  // var viewportOffset = el.offsetTop - window.scrollY;
  // if (viewportOffset >= topLimit && (viewportOffset + el.clientHeight) <= bottomLimit) {
  if (isWithinLimits(el, topLimit, bottomLimit)) {

Group together the limits

As topLimit and bottomLimit are directly related to each other, and are being separately passed to a function, they should be grouped together so that they can be passed as a group instead.

var limits = {
  top: winHeight * 20 / 100,
  bottom: bottomLimit = winHeight * 80 / 100
};
//...
// function isWithinLimits(el, topLimit, bottomLimit) {
function isWithinLimits(el, limits) {
  var viewportOffset = el.offsetTop - window.scrollY;
  // return viewportOffset >= topLimit && (viewportOffset + el.clientHeight) <= bottomLimit;
  return viewportOffset >= limits.top && (viewportOffset + el.clientHeight) <= limits.bottom;
}
//...
  // if (isWithinLimits(el, topLimit, bottomLimit)) {
  if (isWithinLimits(el, limits)) {

Summary

That all leaves us with a final set of code as follows:

var winHeight = window.innerHeight;
var limits = {
  top: winHeight * 20 / 100,
  bottom: bottomLimit = winHeight * 80 / 100
};

function isWithinLimits(el, limits) {
  var viewportOffset = el.offsetTop - window.scrollY;
  return viewportOffset >= limits.top && (viewportOffset + el.clientHeight) <= limits.bottom;
}
function highlightElement(el) {
  if (isWithinLimits(el, limits)) {
    el.classList.add('highlight');
  } else{
    el.classList.remove('highlight');
  }
}
function scrollHighlightHandler() {
  document.querySelectorAll('.scroll li').forEach(highlightElement);
}
window.addEventListener('scroll', scrollHighlightHandler);

The above code can be explored at https://codepen.io/pmw57/pen/PobeJXN

2 Likes

@Paul_Wilkins Not constants? Or do you think that confuses matters?

Constants are even better, but I prefer to restrict confusions to only one at a time.

The var statements can be updated to be const ones instead, for example:

// var winHeight = window.innerHeight;
const winHeight = window.innerHeight;

It makes no difference to the code as it is.

I guess though that a benefit of using const is that it helps to inform us that the programmer is desirous of using improved techniques wherever practical.

The updated code using const is found at https://codepen.io/pmw57/pen/LYbmeWL

1 Like

Speaking of improvements, we can add that throttle to the scroll code.

We can add lodash to the codePen list of external scripts, giving us access to a throttle method.

It just means updating the addEventListener part of the code.

// window.addEventListener('scroll', scrollHighlightHandler);
window.addEventListener('scroll', _.throttle(scrollHighlightHandler, 200));

That limits the scroll event to happening no more than 5 times a second, which is fast enough to be visually useful but not so fast that it strangles the web browser.

Instead of just the 200 though, I prefer to use an explanatory variable for it instead.

const throttleMilliseconds = 200;
window.addEventListener('scroll', _.throttle(scrollHighlightHandler, throttleMilliseconds));

The updated code using the throttle is found at https://codepen.io/pmw57/pen/ZEBovJb

1 Like

I tried it out, and it can’t get the feeling of Java code off my mind.

Ack! Out damn’d spot. scrubs with sanitiser - Will these hands never be clean?

2 Likes

Thanks, Paul_Wilkins for the in-depth answer

I tried your script with my own HTML so I had to make changes to document.querySelectorAll. It removed the highlight class from each paragraph and didn’t add it.

That’s a shame. There must be something that you are not quite understanding yet.

When I disable one of my stylesheets, it works. So I’m debugging my style sheet now.

1 Like

This was the culprit:

section {
    position: relative;
    min-height: 80vh;
}

I commented it out and it’s working now.

1 Like

That’s great to hear. I’m not entirely sure though about how things will work with your other assignments.

Yes people will be here to give help and assistance, but teachers tend to frown when other people do the work for you.

1 Like

The next part of my assignment is to highlight the nav bar according to which section I’m in. So if I scroll to section 2, the nav bar TAB 2 will respond. How do I detect which section I’m in?

You will want to search through a reverse-ordered list of the nav sections. The first one of those that is not below the visible viewport of the page, is the current tab.

Knowing what the active tab is, you can remove the active status from all tabs, and then set the active status on what should be the current tab.

How would I write the jQuery expression var $sections = $($(".section").get().reverse()); in plain javascript?

Possibly?

const sections = Array.from(document.querySelectAll('.section')).reverse()

With a helper function

const select = (selector, root = document) => root.querySelector(selector)
const selectAll = (selector, root = document) => root.querySelectorAll(selector)

const sections = Array.from(selectAll('.section')).reverse()