Search code examples
javascripthtmlscrollevent-handling

Scrolling multiple divs in js


I have more than two divs which i would like to scroll, it does not matter which one i scroll, they all should move for same amount at the same time. The problem i have is that el.scrollLeft fires the listener again. I also tried to disable function on wheel and enable it on end of scrolling, but the problem is that on one wheel event multiple scroll events happen (as i researched you cant disable this smooth scrolling on chrome?) This is my current code which does not work.

document.querySelectorAll('.rowDataDiv').forEach(el =>
    el.addEventListener('scroll', e =>
        document.querySelectorAll('.rowDataDiv').forEach(el =>
            el.scrollLeft = e.target.scrollLeft
        );
    );
);

EDIT: I want to avoid the loop caused by scrollLeft if I wasnt clear enough


Solution

  • So, new answer, now that I understand better the question.

    And quite a convoluted one.

    Note that it is almost a duplicate question: Make "scrollLeft" / "scrollTop" changes not trigger scroll event listener

    And I took my inspiration from the best answer there. The difference, and it is bigger than it seems at first glance, is that in that question there are only 2 divs, plus, the initial event is fired explicitly from code. So they know when to start ignoring future events. And they know when they can stop ignoring them.

    But the base problem is the same: events are fired afterward. And the scroll event most often doesn't come alone. So you have a burst of scroll events coming from (say) first div. And then some other scroll events coming from the update of scrollLeft. And you can't assume anything about the order in which those events will occur.

    Note that the browser would not fire a scroll event if setting scrollLeft doesn't change scrollLeft value. Which is both

    • the reason why your code is not so bad as is (it works, and it doesn't get caught in an infinite loop. Just, looking closely, sometimes, when subsequent event from previous updating of scrollLeft fires after another mouse event, the scrolling movement can't temporarily go backward (which I can't really see with my naked eye. But is visible when logging positions).
    • another reason why the answer is not that simple (one dealt in the question I quoted). General idea is to ignore events while updating scrollLeft on other div. But to know when to stop ignoring event, you have to do this in the event handler. Because doing so after updating all scrollLeft is too soon: event from those update haven't been fired yet. But you have to take into account that event handler is not always called after a scrollLeft update. Precisely because of the reason why your version does not work so bad: event handler is called only if a real change has been made.

    So, all together, my solution

    // orgTarget is the div which fire the first (the real, coming from user interaction) event
    // Initialy there is none
    let orgTarget=false;
    
    // Same callback for all div
    function onScroll(e){
        // If we don't have a orgTarget, that means we weren't in the middle of updating scrollLeft. THe event come from the user interaction.
        // So  e.target is our originator div. 
        if(orgTarget===false) orgTarget=e.target;
        // pos is the scroll position we are trying to propagate
        // (Note that pos come from orgTarget, not from e.target)
        let pos=orgTarget.scrollLeft;
        // If current callback is called because of orgTarget, that is because of user interaction, then update all scrollLeft
        if(orgTarget==e.target){
            for(let d of divList){
                if(d.scrollLeft!=pos){
                    d.scrollLeft=pos; // This could have been outside the if: it would have no consequence (no future event) if it were useless anyway. But just to be sure, update only when needed
                    // important line is this one: when we change scrollLeft of d, we can expect a scroll event from d. Use an attribute of d to remind that we are expecting an event here
                    d.scrollEventToCome=true;
                }
            }
        }
        // We got an event for e.target (that is why we are running this callback)
        // So, if we were expecting one, we can stop expecting it: we got it
        e.target.scrollEventToCome=false;
        // Just check if there is still an event to come in any div
        let stillWaiting=false;
        for(let d of divList){
            if(d.scrollEventToCome) stillWaiting=true;
        }
        // if no event are expected from any div, then we can reset orgTarget
        // which means that the next event will be considered a "real" event
        if(!stillWaiting) orgTarget=false;
    }
    
    for(let d of divList){
        d.addEventListener('scroll', onScroll);
    }
    

    So, the idea is quite similar to the one in the post I referenced:

    • Before updating scrollLeft, note in some way that coming events should not trigger further modification of scrollLeft
    • To know when to stop ignoring future even, we do it in the event listener (before, it would be too soon), when we got all the "fake" event we were expecting.

    In the referenced message, this was done just by a global ignoreEvent variable. Meaning "we expect ONE fake event". Which was reset in the event handler, when we got the expected fake event (I call "fake event" the one coming from an explicit update of scrollLeft, and "real event" the one coming from user interaction). Being careful of a possible exception: if scrollLeft is not really updated, then we should let ignoreEvent to false immediatly, and not wait for the listener of a coming fake event, since none is to come.

    Here we have to take note of several fake events: each time a real event occurs, we update plenty of scrollLeft. Which will generate plenty of fake event. But there also, taking into account that fake event are to be expected only if scrollLeft is really updated.

    We also have to take into account that, while we are in the middle of receiving a burst of fake event because of a first real event, another event can occur, from the same div.

    Hence my solution:

    • keep tracks of which div triggered the real event (orgTarget)
    • process events from that div (the first real one, but also some other real events from that div that may occur in the middle of the process)
    • update scrollLeft on all other divs. And keep track of which future fake event we are expecting
    • when we receive a fake event (that is an event that is not coming from the orgTarget div), note that we are not expecting it anymore
    • when we are not expecting any fake event, we can't stop the ignoring process, and expect a new series of real event. Which is done by resetting orgTarget to false.

    I tested it. It works as expected. I never experienced any "backward" scrolling. It does only the strictly needed scrollLeft update (each time scrollLeft of one div is changed by a user action, it update once, and only once, scrollLeft of all other divs. It does so only if needed, just to not depend on that "event is not fired if scrollLeft is not really updated" behavior).