Search code examples
javascripthtmltypescriptscroll

Scrolling Multiple DIV with Variable Content Length Simultaneously at the Same Rate


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.


Solution

  • 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>