0

I am trying to track how much time certain sections on a page had a certain class.

The page structure is of form:

Topic 1
   Subtopic 1.1
   Subtopic 1.2
Topic 2
etc

All topics and subtopics have anchors, where the anchor id is the topic name. I use the variable scrollItems to refer to these main topic elements.

I retrieve all anchors and scrollItems via

     scrollItems = $('div.topicrow-header'),
     anchors = $('.navanchor');

So, scrollItems is a subset of anchors.

What I am doing is using a sticky class so that for a given topic on a page, it has a sticky header. A topic might have subtopics, and if any one of the subtopics is 'in view', as I determine in view, then the topic's header is stuck on top of the page as a section heading. So using above, if someone is viewing Topic 1, Subtopic 1.1, or Subtopic 1.2, then the topic whose anchor id is Topic 1 will have class sticky.

If someone scrolls quickly, then the detected topics and subtopics are changing quickly, and I don't consider that time spent on the material.

I sort of came up with a hard-coded way to track time spent on a topic, but I suspect it has many pitfalls. Here is what I am currently doing.

Note: some of the variables used below are retrieved elsewhere, but are available.

    var prevTopicId=0,
        curTopicId=0,
        prev_idx=null,
        scrollItems = $('div.topicrow-header'),
        anchors = $('.navanchor'),
        topMenu = curles.find('#lessontopics-'+curlesid),
        topMenuHeight = topMenu.outerHeight()+15,
        cur_idx=null;

    $(window).on('scroll', _.throttle(function() {
        // Get container scroll position
        var fromTop = $(this).scrollTop()+topMenuHeight;
        var cur = anchors.map(function(){
            if (($(this).offset().top < fromTop) && (this.getBoundingClientRect().top < .25*topMenuHeight )) {
                return this;
            }
        });

        // Get the id of the current element

        if (cur.length !== 0) {
            cur = cur[cur.length-1];
            var id = cur.id;
           
            // only set sticky class for topic row headers
            if ((cur.classList.contains('topicrow-header'))) {
                curTopicId = id;
                // first time scrolling, prevTopicId = 0, so sticky class will be added to first topic that comes in view
                // then sticky class will only be added to a newly seen topic
                if (curTopicId !== prevTopicId) {
                    console.log('setting active sticky item first time for '+curTopicId);
                    $.each(scrollItems, function( index, value ) {
                        if (value.id === curTopicId) {
                            $(value).addClass("stick");
                            topic_times[index].startTime = Date.now() / 1000 | 0;
                            cur_idx = index;
                            return false;
                        }
                    });
                }

                // first time around, prevTopicId =0, so no sticky class to remove
                // then we remove a previous sticky class when we have newly seen topic
                if ( prev_idx == null && cur_idx != null) {prev_idx = cur_idx;}
                if (prevTopicId !== 0 && curTopicId !== prevTopicId) {
                    console.log('removing previous active sticky item for ' + prevTopicId);
                    scrollItems.filter('#' + prevTopicId).removeClass('stick');
                    nowtime = Date.now() / 1000 | 0;
                    difftime = nowtime - topic_times[prev_idx].startTime;
                    if ( difftime > 10 ) {
                        topic_times[prev_idx].totalTime = topic_times[cur_idx].totalTime + difftime;
                        topic_times[prev_idx].endTime = nowtime;
                    } else {
                        topic_times[prev_idx].startTime = 0;
                    }
                }
                prev_idx = cur_idx;
                prevTopicId = curTopicId;
            }
         }
    });

Now, the above seems to work, but there is always the question of what to do when a sticky class is added for a topic, and there is no change to another topic. How to count time in that case, when I have no event to trigger me being able to make a time difference calculation?

So seems to me, in the more broad sense, I need an overall method to calculate time an element had a class name.

Anyway, I figure there has to be a tried and true way to do this, one that is sufficiently reliable.

What could I do differently? How would you solve this problem?

halfer
  • 19,824
  • 17
  • 99
  • 186
Brian
  • 561
  • 5
  • 16

1 Answers1

1

So seems to me, in the more broad sense, I need an overall method to calculate time an element had a class name.

I'm having difficulty following your scenario, but from what I understand, I think this is a very simplified approach to what you're ultimately asking for:

var addedTime = null
var className = 'stick'

function addClass( element ) {
    addedTime = Date.now()
    element.classList.add(className)
}

function removeClass( element ) {
    element.classList.remove(className)
    const duration = Date.now() - addedTime
    // You could return the duration or set it to some
    // variable, etc. then work with it afterwards.
}

I.e. do two actions and measure the duration between them. (Note: you could also use performance.now() instead of Date.now(), though they are different.)

What could I do differently? How would you solve this problem?

I know my answer above isn't very robust, but I don't want to go down that road too far just yet because frankly I think your problems are more deeply rooted than that. Since you asked, I hope you'll permit some advice.

Break things down

First off, you're doing way too much in a single block. I see if statements nested four levels deep with anonymous callbacks scattered therein. It's very difficult to know if you're even asking the right questions if your code is hard to follow. Here are my suggestions.

Make more pointed functions

Start with small purposeful functions. For example, make a function that can "stick" an element to the page. Don't worry about scrolling, timing, or any of that. Just make one function that can do that one specific task and nothing else. Then make a function that "unsticks" an element—and it does nothing but that. Give these functions appropriate names so that it's easy to read what's going on when these functions are called. For example, the ternary isStickied ? unsticky() : sticky() is very easy to read no matter how much complexity is behind either of those functions. Do this until you have all the basic pieces you need, then assemble bigger pieces by using the smaller ones, and so on.

The scroll event listeners and such shouldn't even be a thought until you have all your underlying mechanisms defined and laid out. All you're doing at that point is little more than assembling them.

Name reusable callbacks (Avoid "callback hell")

Putting callbacks inside of callbacks inside of callbacks is typically referred to as "callback hell" because it's often hell trying to read or maintain such code in the long run. It gets very messy very quickly. To avoid this, break off these functions and pass them by name when you need them.

So turn this:

window.on('scroll', function() {
    [].forEach(function( item ) {
        // ...
    })
})

Into this:

function myLoop( item ) {
    // ...
}

window.on('scroll', function() {
    [].forEach(myLoop)
})

Avoid excessive nesting

Nesting conditionals isn't inherently bad, but it's harder to read the deeper you nest. I find that if you're tempted to nest 3 or more if statements, that's usually a good time to ask yourself if you'd be more suited to breaking something off into its own function or rearranging the conditional statements. One way you can avoid nesting conditionals is by taking advantage of else if like so:

With nesting:

if ( person.isHome ) {
    if ( person.isAsleep ) {
        person.wake()
    }
    else {
        person.sleep()
    }
} else {
    person.goHome()
}

Without nesting:

if ( !person.isHome ) {
    person.goHome()
}
else if ( person.isAsleep ) {
    person.wake()
}
// We now know the person must be home and must be awake.
else {
    person.sleep()
}

Use JavaScript classes/constructors

I won't get too deep into this as you can do your own research on them if need be, but a JS class is a great way to share values between methods/functions that serve the same ultimate goal (like creating a control board for "sticky" elements) without polluting a larger scope. It keeps things nice and clean.

Reevaluate

Often by fixing smaller or deeper problems, your bigger problems naturally evaporate or a better question becomes more clear to you. Once you've fixed the deeper issues, reevaluate where you're at and see if you still need what you're asking for.

For instance, you're asking for time-related solutions so that's what I tried to tailor my answer to, but if you're talking about sticky stuff, I suspect what you might actually want are things like window.pageXOffset, window.pageYOffset, Element.prototype.getBoundingClientRect(), Element.prototype.offsetHeight, etc. rather anything to do with measuring time necessarily.


I hope that helps. I saw your comment on my post and figured I'd pitch in.

Leon Williams
  • 674
  • 1
  • 7
  • 15