Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Controlling vertical scrolling and horizontal swiping on a list in an iOS web app with JavaScript

I'm creating a web application specifically targeted for phones (primarily iPhone, but Android & WP are on the horizon...).

One of the screens contains a scrolling list of items. I would like the list to behave similarly to the built-in iOS Mail app.

In other words...

  1. If the user touches the list and moves up or down, the list scrolls vertically.
  2. If the user flicks up or down, the list scrolls vertically with natural momentum
  3. If the user touches the list and moves ONLY left - the particular item slides to the left revealing a delete button.
  4. IMPORTANTLY - the list should scroll OR the item should slide, BUT NEVER BOTH.

So - it's important to figure out what the user's intention is, which means I probably need to prevent ANY response until I figure out whether the user is moving her finger vertically or horizontally.

By simply setting these CSS styles on the list container...

overflow-y: auto;
-webkit-overflow-scrolling: touch;

... I get #1 & #2 above. So, I need to figure out how implement #3.

My first thought was to implement something like this (pseudocode)...

  1. Create a touchstart event listener on the list container. In the callback, store the x- and y-coordinates of the user's starting touch position.
  2. Create a touchmove event listener on the list container. In the callback, figure out how far the user's finger has moved (e.g., delta_x and delta_y)
  3. If delta_x AND delta_y are both less than 10 pixels - don't do anything (don't scroll the list or slide the item) - since we haven't yet figured out whether the user plans to move up/down or left/right.
  4. If EITHER delta_x OR delta_y are more than 10 pixels - we can assume the user has moved far enough to express her intention. If delta_y > delta_x, assume she's moving up/down, and allow the list to scroll, but don't slide the item. If delta_x > delta_y, assume she's moving left/right, so we should allow the item to slide, but not allow the list to scroll.

I expected that I would use event.preventDefault() in either the touchstart or touchmove to control when scrolling should begin. E.g.,

div.addEventListener("touchstart", function(e) {
    touchStart = {
        x: e.touches[0].pageX,
        y: e.touches[0].pageY
    }
}, false);
div.addEventListener("touchmove", function(e) {
    touchNow = {
        x: e.touches[0].pageX,
        y: e.touches[0].pageY
    }
    var
        dx = touchStart.x - touchNow.x,
        dy = touchStart.y - touchNow.y;
    if ((Math.abs(dx) < 10) && (Math.abs(dy) < 10)) {
        // prevent scrolling
        e.preventDefault();
    } else if (Math.abs(dx) > Math.abs(dy) < 10) {
        // moving right/left - slide item
    } else {
        // moving up/down - allow scrolling
    }
}, false);

However - this doesn't work. Regardless of how far you move, the list NEVER scrolls.

Obviously - I'm misunderstanding what triggers the scrolling, and what event.preventDefault() is supposed to do in this context.

So - is there a way to accomplish what I'm after?

I'm hoping for a pure JavaScript solution (so I understand it better), but a jQuery approach would be fine to. I'm definitely hoping to avoid a jQuery plugin/library/framework if at all possible...

Thanks in advance!

like image 609
mattstuehler Avatar asked Nov 21 '13 19:11

mattstuehler


People also ask

How do I scroll horizontally on a Web page?

Horizontal scrolling can be achieved by clicking and dragging a horizontal scroll bar, swiping sideways on a desktop trackpad or trackpad mouse, pressing left and right arrow keys, or swiping sideways with one's finger on a touchscreen.

How do I create a horizontal scrolling container?

On our container, we want to turn off vertical scrolling (overflow-y) and enable horizontal scrolling (overflow-x). Then with each card, we want to set it to display with inline-block so they all display in a row. The line of CSS you probably are unfamiliar with is white-space: nowrap.

How do I stop horizontal scrolling on web pages?

To hide the horizontal scrollbar and prevent horizontal scrolling, use overflow-x: hidden: HTML.

What is horizontal and vertical scrolling?

A horizontal scroll bar enables the user to scroll the content of a window to the left or right. A vertical scroll bar enables the user to scroll the content up or down.


3 Answers

I don't suggest reinventing the wheel. There are a number of libraries out there that supports gesture detection. Out of which, I suggest using Hammer.js to detect the touch events.

It doesn't have any dependencies, and it's small, only 3.96 kB minified + gzipped!

And it is all about handling touch events, nothing else.

In your case, Hammer has inbuilt swipe detection.

You can customize the default swipe gesture by specifying :

  • direction: direction in which you want to detect the swipe gesture (more info)
  • threshold: Minimal distance required before recognizing
  • velocity : Minimal velocity required before recognizing (unit is in px per ms)

    and more.

Following is a simple example (Stack Snippet seems to have some issue emulating touch events, fiddle works fine):

var myElement = document.getElementById("container");
var hammertime = new Hammer(myElement, {});
hammertime.get('swipe').set({
  direction: 2 // 2 stands for left
})
hammertime.on('swipe', function(event) {
  event.target.querySelector(".delete").classList.add("show");
});
* {
  margin: 0;
  padding: 0;
}
#container {
  width: 250px;
  height: 300px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: dodgerblue;
}
#list {
  height: 100%;
  list-style: none;
}
#list li {
  position: relative;
  box-sizing: border-box;
  width: 100%;
  height: 50px;
  border: 2px solid #fff;
}
#list li span.delete {
  display: inline-block;
  position: absolute;
  left: 250px;
  width: 50px;
  height: 40px;
  line-height: 40px;
  margin: 3px;
  text-align: center;
  background: #fff;
  transition: left 0.5s ease-in;
}
#list li span.delete.show {
  left: 170px;
}
<script src="http://cdn.jsdelivr.net/hammerjs/2.0.4/hammer.min.js"></script>
<div id="container">
  <ul id="list">
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
    <li class="item"><span class="delete">&#215;</span>
    </li>
  </ul>
</div>

If you are more interested in learning how the touch event works rather than getting the job done , then I suggest looking under the hood of Hammer.


There is a little Hammer.js jQuery plugin as well, for those who can't part with jQuery.

like image 137
T J Avatar answered Nov 04 '22 19:11

T J


I had a similar problem, when I was implementing a slideshow. That's the solution that worked for me, hope it will help you:

function touchStart(event){
   if (!event) event = window.event;
   if(!touched){ 
      touched = true;
      var touchObj = event.changedTouches[0];
      touchXPos  = parseInt(touchObj.clientX);
      touchYPos  = parseInt(touchObj.clientY);
   }    
   return false;
}

When the user touches the screen, the cooridnate positions are stored in touchXPos, touchYPos and the boolean variable "touched" is set to true.

function touchMove(event){
   if (!event) event = window.event;

   if(touched){
       var touchObj = event.changedTouches[0];
       var distanceX = touchXPos - parseInt(touchObj.clientX);
       var distanceY = touchYPos - parseInt(touchObj.clientY);


   if(!touchDirection) {
       if(Math.abs(distanceX) > Math.abs(distanceY)){
          if (distanceX > 0)  touchDirection = "right";
          else if (distanceX < 0) touchDirection = "left";
       }

       else{
          if (distanceY > 0) { touchDirection = "up"; }
          else if (distanceY < 0) {touchDirection = "down"; }
       }
   } 

   if (((touchDirection == "right") )|| ((touchDirection == "left"))){

       //update touchDirection
       if(Math.abs(distanceX) > Math.abs(distanceY)){
          if (distanceX > 0)  touchDirection = "right";
          else if (distanceX < 0) touchDirection = "left";
       }

       touchXPos = parseInt(touchObj.clientX);
       slideshow_mask.scrollLeft += distanceX;
   }

   else if (((touchDirection == "up") ) || ((touchDirection == "down") )){
       return false;
   }

   event.preventDefault();
   return false;
}

Similar to your approach, I determine the delta-x and delta-y whenever the touch moves, which I named distanceX and distanceY. When this is the first "move" I determine the touchdirection, that can be right, left, up or down. The next movement that gets registered, can only stay within this initial drection, i.e. up/down OR left/right. So, if the initial direction was for example "right", the user can only slide horizontally (left or right) but never up/down.

Since my code was designed to move a slideshow horizontally, I was just interested in the horizontal movement (-> in the code snippet you can see, that I updated the direction (left, right), stored the new xPosition and moved the slideshow in this direction), but it shouldn't be too hard to adapt it to vertical scrolling.

like image 28
iris Avatar answered Nov 04 '22 19:11

iris


I had the same task, and wrote a pure Javascript lib swiped.js for a horizontal swiping on a list

like image 42
marsh Avatar answered Nov 04 '22 21:11

marsh