2

January 5, 2023: Update and recommended solution at the bottom.

Using plain Javascript or Typescript and no other dependencies, how would the following be implemented?

  • Two or more DIVs with different content lengths
  • Scroll at the same rate
  • Mouse pointer can be over any of the DIVs
  • When the shorter content DIV (SCD) runs out of content, it stops scrolling but the other DIVs continue to scroll. However, if scrolling reverses direction, the SCD starts reversing its direction until it runs out of content while the other DIVs continue to scroll.

The dozens of implementations of simultaneous multiple DIVs scrolls aren't able to do the above because:

  • Some assume the content to be the same length.
  • Only works if the mouse is scrolling over the DIV with the longer content. When the mouse is scrolling over the DIV with shorter content, scrolling stops for both after the SCD runs out of content.
  • Have DIVs move at different speeds to match the top and bottom of the content which is close to the requirements.

One previous discussion on a similar topic had some solutions that came close, especially a comment by 'Gregor y'. Unfortunately, that solution had both DIVs move at different speeds. The simplified Fiddle for their implementation is available.

function syncOnScroll(selector,SyncFn) {
  if(!SyncFn)return;
  let active = null;
  document.querySelectorAll(selector).forEach((div) => {
    div.addEventListener("mouseenter", (e) => {active = e.target});

    div.addEventListener("scroll", (e) => {
      //ignore inactive-scroll events
      if (e.target !== active) return;

      //push the active-scroll event to synced elements
      document.querySelectorAll(selector).forEach((target) => {
        if (active === target) return;

        SyncFn(target,active);
      });
    });
  });
}

function scrollSync(selector,scrollType) {
    let type = scrollType || 'both';
  function calcPosFn(position,fromRange,fromWidth,toRange,toWidth){ 
    if(fromRange == fromWidth) return 0;
    return Math.floor(
        position   *   ((toRange-toWidth)/
                    (fromRange-fromWidth))
    );
  }
  
  switch(type.toLowerCase()){
    case 'vertical':
        syncOnScroll(selector,function(t,a){
        t.scrollTop=calcPosFn(
            a.scrollTop,
          a.scrollHeight,
          a.clientHeight,
          t.scrollHeight,
          t.clientHeight)});break;
    case 'horizontal':
        syncOnScroll(selector,function(t,a){
        t.scrollLeft=calcPosFn(
            a.scrollLeft,
          a.scrollWidth,
          a.clientWidth,
          t.scrollWidth,
          t.clientWidth)});break;
    case 'both':default:
        syncOnScroll(selector,function(t,a){
        t.scrollTop  = calcPosFn(
            a.scrollTop,
          a.scrollHeight,
          a.clientHeight,
          t.scrollHeight,
          t.clientHeight);
        t.scrollLeft = calcPosFn(
            a.scrollLeft,
          a.scrollWidth,
          a.clientWidth,
          t.scrollWidth,
          t.clientWidth);
      });break;
    }
}

//RWM: Call the function on the elements you need synced.
scrollSync(".scrollSyncV",'vertical');
/* use whatever method you want to put the tables side-by-side */
:root {
  padding: 1rem;
}
.scrollSyncV {
    float: left;
    width: 50%;
    height: 10rem;
    overflow: auto;
    border: 1px solid black;
    box-sizing: border-box;
}

table {
    width: 100%;
}

tr:nth-child(even) td {
    background-color: #eef;
}
<div class="scrollSyncV" id="1">
   <table>
   <th>Longer</th>
       <tr><td>Left Line 1</td></tr>
       <tr><td>Left Line 2</td></tr>
       <tr><td>Left Line 3</td></tr>
       <tr><td>Left Line 4</td></tr>
       <tr><td>Left Line 5</td></tr>
       <tr><td>Left Line 6</td></tr>
       <tr><td>Left Line 7</td></tr>
       <tr><td>Left Line 8</td></tr>
       <tr><td>Left Line 9</td></tr>
       <tr><td>Left Line 10</td></tr>
       <tr><td>Left Line 11</td></tr>
       <tr><td>Left Line 12</td></tr>
       <tr><td>Left Line 13</td></tr>
       <tr><td>Left Line 14</td></tr>
       <tr><td>Left Line 15</td></tr>
       <tr><td>Left Line 16</td></tr>
       <tr><td>Left Line 17</td></tr>
       <tr><td>Left Line 18</td></tr>
       <tr><td>Left Line 19</td></tr>
       <tr><td>Left Line 20</td></tr>
       <tr><td>Left Line 21</td></tr>
       <tr><td>Left Line 22</td></tr>
       <tr><td>Left Line 23</td></tr>
       <tr><td>Left Line 24</td></tr>
       <tr><td>Left Line 25</td></tr>
   </table>
</div>
<div class="scrollSyncV" id="2">
    <table>
    <th>Shorter</th>
       <tr><td>Right Line 1</td></tr>
       <tr><td>Right Line 2</td></tr>
       <tr><td>Right Line 3</td></tr>
       <tr><td>Right Line 4</td></tr>
       <tr><td>Right Line 5</td></tr>
       <tr><td>Right Line 6</td></tr>
       <tr><td>Right Line 7</td></tr>
       <tr><td>Right Line 8</td></tr>
       <tr><td>Right Line 9</td></tr>
       <tr><td>Right Line 10</td></tr>
   </table>
</div>

<div id="fiddleBottomPadding" style="height:100px"></div>

Update:

@Roko's fiddle pretty much answered my question as was stated. He did make a note about fast scrolling. Thanks, Roko!

Based on further discussion with @Roko about Twitter's scroll implementation and his mention of using CSS sticky, a solution by @rootShiv that uses CSS sticky was found that met the concept I was after.

Thomas
  • 349
  • 3
  • 11
  • AS far as I understood, you want both DIVs to scroll in sync (if one scrolls 100px the other does too). What I guess would be difficult is that once one DIV reaches its scroll delta end - the scroll would not trigger any change to pass to the other DIV scrollTop. This one I think should be done by simply tapping into the `"wheel"` delta and simply apply a scroll amount to both the DIVs regardless. And do the same for their "scroll" Event. – Roko C. Buljan Jan 04 '23 at 22:46
  • @RokoC.Buljan, yes @ both scroll at 100px at the same time. Would you mind sharing the code to implement "wheel" delta? – Thomas Jan 04 '23 at 22:47

1 Answers1

1

Edit: The closest I got to a solution is this jsFiddle example.


To give you a starting point (no manual scrollbars scroll implemented yet)

by using the "wheel" event, preventing the default scroll behavior by using Event.preventDefault() and by applying a delta and by using Element.scrollBy()

const elsDIV = document.querySelectorAll(".scrollSyncV");

elsDIV.forEach(el => {

  const elOther = [...elsDIV].filter(e => e !== el)[0];

  el.addEventListener('wheel', evt => {
    evt.preventDefault();
    const delta = Math.sign(evt.deltaY);
    elsDIV.forEach(el => el.scrollBy({top: 90.909 * delta}))
  });
});
body {
  display: flex;
}

.scrollSyncV {
  width: 50%;
  height: 10rem;
  overflow: auto;
  border: 1px solid black;
  box-sizing: border-box;
  font-size: 2.5rem;
}

.scrollSyncV div {
  border: 10px dashed #aaa;
}
<div class="scrollSyncV">
  Lorem ipsum dolor sit amet, consectetur adipisicing elit.
  Consequuntur rerum rem quae iste earum aspernatur.
  Laudantium eum, animi maiores unde itaque,
  repellat sed magni dicta earum alias, ipsa ad labore!
  Deserunt recusandae modi. Earum autem provident eum in officia.
  Lorem ipsum dolor sit amet, consectetur adipisicing elit.
  Iure possimus doloribus veritatis dolores voluptate quae
  fuga eaque mollitia magni nam exercitationem ratione itaque
  est debitis dolor, nemo repudiandae voluptas sequi!
<br>
End.
</div>
<div class="scrollSyncV">
  Lorem ipsum dolor sit amet, consectetur adipisicing elit.
  Consequuntur rerum rem quae iste earum aspernatur.
  Laudantium eum, animi maiores unde itaque,
  repellat sed magni dicta earum alias, ipsa ad labore!
<br>End.
</div>
Roko C. Buljan
  • 196,159
  • 39
  • 305
  • 313
  • thanks for the starting point. How did you arrive at the 90.909 number? – Thomas Jan 05 '23 at 00:17
  • @Thomas I calculated the scroll difference in Chrome. Ended up being around ~90.909px :) – Roko C. Buljan Jan 05 '23 at 00:22
  • @Thomas just out of curiosity... what are you building? This seems quite like an [XY-Problem](https://en.wikipedia.org/wiki/XY_problem) – Roko C. Buljan Jan 05 '23 at 00:33
  • seems like the 90.909 would change if the content in each DIV would change, right? I'm building a web page where on a large screen the content area has those two DIVs. The left DIV contains a lot of content and uses infinite scroll. The right DIV contains sub-content that tend to be shorter. – Thomas Jan 05 '23 at 01:57
  • @Thomas interesting... I'm trying to imagine such an UI and I still see no purpose... why would I, by scrolling the right DIV would want to see scrolling the left one as well? (I'm not sure. I changed the DIV content length and still Chrome seems to report those ~90.1px. Quite an odd value...) – Roko C. Buljan Jan 05 '23 at 09:31
  • The design style isn't used often but can be quite effective on larger screens. Think of the two DIVs as one content panel without the scrollbars. ~90.1px is such an odd number. – Thomas Jan 05 '23 at 17:44
  • @Thomas the closes I got was: https://jsfiddle.net/RokoCB/92pvum8d/1/ - but as you can see, when you scroll the smaller right DIV - it will not work if you scroll really quickly. one solution (I have no time to implement) would be perhaps implementing requestAnimationFrame inside the `if` and lerp the elOther scrollTo() top value with easing - Anyways... **the UI makes no sense** from a User Perspective. A user would never expect to scroll an element that visually has the scrollbar stuck to the top - and specially not by doing so - see another element scroll. The idea is broken if you ask me. – Roko C. Buljan Jan 05 '23 at 20:25
  • Any I still have no idea what exactly are you building exactly and for what purpose? I'm just trying to make sense of such a UI - in practice. – Roko C. Buljan Jan 05 '23 at 20:41
  • Thanks for your time. There is a little website, Twitter, that also uses this idea. Check it out on a larger screen. – Thomas Jan 05 '23 at 20:43
  • @Thomas that's not correct. Twitter does not uses anything like this. Twitter has a global scrollbar, not two elements with distinct scrollbars. – Roko C. Buljan Jan 05 '23 at 20:46
  • Thanks for the clarification. That's the concept I am referring to. – Thomas Jan 05 '23 at 20:53
  • @Thomas Twitter simply uses `position: sticky` (and some small JS to set alternatively `top:` and `bottom:` positions). Now I'm sad you didn't mentioned that earlier. I knew it was an XY-Problem (as I mentioned in my second comment). :( – Roko C. Buljan Jan 05 '23 at 20:59
  • sorry you are sad :/. I didn't do a good job of conveying the concept. I was working on a page design and wanted to maximize the horizontal space while minimizing clutter. I recently got back into dev after decades away so I'm not up to date, yet. After you mentioned you still didn't know what I was trying to convey, I went searching for a site that used that concept. Twitter probably implemented best, IMO, of what I found. – Thomas Jan 05 '23 at 21:17