Search code examples
javascripthtmlgoogle-chromemulti-touchinteract.js

multiple vertical scrollable divs on multitouch problem


i try to scroll 2 text fields vertically on a multitouch application, each one works for itself, but as soon as i try to scroll both, only the one touched first works.

is there a workaround here?

maybe an example of a custom scroller?

here's a example, please test on multitouch.

#window {
  height: 200px;
  border: 1px black solid;
}

.window {
  height: 200px;
  border: 1px black solid;
}


.scrollable-content {
  height: 180px;
  overflow: auto;
  background-color: blue;
}

#footer {
  height: 20px;
  background-color: green;
}
<div class="window">

  <div class="scrollable-content">
    <ul>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
    </ul>
  </div>

  

</div>
<div class="window">
  <div class="scrollable-content">
    <ul>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
      <li>Sample</li>
    </ul>
  </div>
</div>


Solution

  • If you want to always sync both Divs, you could listen for both Div's scroll events and then copy the target's scrollTop to the other div.

    var divs = document.querySelectorAll('.scrollable-content');
    for (var i = 0; i < divs.length; i++) {
      divs[i].addEventListener('scroll', function(event) {
        var scrollers = document.querySelectorAll('.scrollable-content');
        for (var i = 0; i < scrollers.length; i++) {
          if ( scrollers[i].isSameNode( event.target ) === false ) {
          scrollers[i].scrollTop = event.target.scrollTop;
          }
        }
      });
    }
    #window {
      height: 200px;
      border: 1px black solid;
    }
    .window {
      height: 200px;
      border: 1px black solid;
    }
    .scrollable-content {
      height: 180px;
      overflow: auto;
      background-color: blue;
    }
    #footer {
      height: 20px;
      background-color: green;
    }
    <div class="window">
      <div class="scrollable-content">
        <ul>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
        </ul>
      </div>
    </div>
    <div class="window">
      <div class="scrollable-content">
        <ul>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
        </ul>
      </div>
    </div>

    If you want to only scroll the second div on a double touch, things get slightly more complicated, assuming you would want both elements to scroll independently of each other's direction and distance.

    In short, you'll end up keeping track of touchstart events, finding out if it originated inside a div you want to scroll, jotting down the event's Y position, then in the touchmove event you'll have to calculate the difference and adjust the Div's scrollTop accordingly.

    One way of doing it would be like this;

    function isDescendant(child,queryString) 
    {
        if ( child.matches( queryString ) ) {
            return child;
        }
        var parent = child.parentNode;
        while( parent.matches ) {
            if ( parent.matches( queryString ) ) {
                return parent;
            }
            parent = parent.parentNode;
        }
        return false;
    }
    
    var touchedTargets = [];
    var touchedData = [];
    
    
    document.body.addEventListener('touchstart',function( event ){
        var insideTarget = isDescendant( event.target , ".scrollable-content" );
        if ( insideTarget ) {
            var index = touchedTargets.indexOf( event.target );
            if ( index < 0 ) {
                var data = {x:0,y:0,scroller:insideTarget};
                for ( var i=0; i<event.touches.length; i++ ) {
                    if ( event.touches[i].target.isSameNode( event.target ) ) {
                        data.x = event.touches[i].clientX;
                        data.y = event.touches[i].clientY;
                    }
                }
                touchedTargets.push( event.target );
                touchedData.push( data );
            }
        }
    });
    document.body.addEventListener('touchend',function( event ){
        var index = touchedTargets.indexOf( event.target );
        if ( index > -1 ) {
            touchedTargets.splice( index , 1 );
            touchedData.splice( index , 1 );
        }
    });
    document.body.addEventListener('touchmove',function( event ){
        for ( var i=0; i<event.touches.length; i++ ) {
            var index = touchedTargets.indexOf( event.touches[i].target );
            if ( index > -1 ) {
                touchedData[index].scroller.scrollTop = touchedData[index].scroller.scrollTop + ( event.touches[i].clientY - touchedData[index].y );
                touchedData[index].y = event.touches[i].clientY;
                event.preventDefault();
            }
        }
    },{ passive: false });
    #window {
      height: 200px;
      border: 1px black solid;
    }
    .window {
      height: 200px;
      border: 1px black solid;
    }
    .scrollable-content {
      height: 180px;
      overflow: auto;
      background-color: blue;
    }
    #footer {
      height: 20px;
      background-color: green;
    }
    <div class="window">
      <div class="scrollable-content">
        <ul>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
        </ul>
      </div>
    </div>
    <div class="window">
      <div class="scrollable-content">
        <ul>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
          <li>Sample</li>
        </ul>
      </div>
    </div>

    On touchstart the code checks to see if the event.target is actually, or is inside, the .scrollable-content element. If this is the case, it will look inside the event.touches list to find the corrosponding touch, the touch's clientX, clientY and the .scrollable-content element are saved in the touchedData array. The event.target is saved in the touchedTargets array.

    On touchend the code checks to see if the event.target is inside our touchedTargets array and deletes this entry if it's the case.

    On touchmove the code loops over the event.touches list and checks to see if their targets are inside the touchedTargets array. If this is the case, it will calculate the new scrollTop for that particular .scrollable-content and update it.