Search code examples
javascriptjqueryhtmlmobile

Horizontal dragging is blocking the default vertical scroll of the page


I have created a codepen of what I'm trying to achieve.

I'm creating a timeline overview with a couple of horizontal draggable items. Next to jquery and jquery-ui, I am also using jquery-ui-touch-punch for make all the gestures possible on mobile devices. On desktop everything works fine but not on mobile.

Unfortunately is the $(".timeLineItemContainer").draggable({ ... }); extremely aggressive that it also swallows my vertical scrolling behavior which results in a page where I can't scroll down on mobile when I touch 1 of the items.

Somehow I need to enable or disable the drag capability of an item, depending on the direction of my scroll action. When I disable the event.preventDefault(); in the jquery-ui-touch-punch.js it scrolls down, only then the click and horizontal move doesn't work properly (a horizontal swipe on mobile refers to a history.goback, which I want to prevent as I need it for my gesture).

To sum things up: All I want is to prevent the default behaviors, except for the vertical scroll.

Any thoughts of how to achieve this? Or are there any other ideas/approaches to achieve this?

HTML (just a simple list of multiple items)

        <div class="timeLineItemContainer">
        <div class="subItemMiddleContainer">
            <div class="timeLineHeader">
                <div>
                    <span>A TITLE</span>
                </div>
                <div>
                    <div>Some other text text</div>
                </div>
            </div>
        </div>
        <div class="subItemBottomContainer">
            <ul>
                <li class="static">
                    <div class="timeLineRight timeLineInfo">
                        <span class="info">Thu 12 March</span>
                    </div>
                </li>
                <li class="static">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">some additional text</span>
                        <span class="info time strikethrough">bla</span>
                        <div class="info time attention">
                            <span>yup</span>
                        </div>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">Text</span>
                        <span class="info">content</span>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">title</span>
                        <span class="info">value</span>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">Another title</span>
                        <span class="info">another value</span>
                    </div>
                </li>
                <li class="static">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">always visible</span>
                        <span class="info time strikethrough">just because</span>
                        <div class="info attention">
                            <span>attention!</span>
                        </div>
                    </div>
                </li>
            </ul>
        </div>
    </div>
    <div class="timeLineItemContainer">
        <div class="subItemMiddleContainer">
            <div class="timeLineLeft">
                <div class="timeLinePipe"></div>
            </div>
            <div class="timeLineHeader">
                <div>
                    <span>Some other text</span>
                </div>
                <div>
                    <div>Ola!</div>
                </div>
            </div>
        </div>
        <div class="subItemBottomContainer">
            <ul>

                <li class="static">
                    <div class="timeLineRight timeLineInfo">
                        <span class="info">A date</span>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">label</span>
                        <span class="info">value</span>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">anotehr label</span>
                        <span class="info time">different value</span>
                    </div>
                </li>
            </ul>
        </div>
    </div>
    <div class="timeLineItemContainer">
        <div class="subItemMiddleContainer">
            <div class="timeLineHeader">
                <div>
                    <span>A TITLE</span>
                </div>
                <div>
                    <div>Some other text text</div>
                </div>
            </div>
        </div>
        <div class="subItemBottomContainer">
            <ul>
                <li class="static">
                    <div class="timeLineRight timeLineInfo">
                        <span class="info">Thu 12 March</span>
                    </div>
                </li>
                <li class="static">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">some additional text</span>
                        <span class="info time strikethrough">bla</span>
                        <div class="info time attention">
                            <span>yup</span>
                        </div>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">Text</span>
                        <span class="info">content</span>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">title</span>
                        <span class="info">value</span>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">Another title</span>
                        <span class="info">another value</span>
                    </div>
                </li>
                <li class="static">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">always visible</span>
                        <span class="info time strikethrough">just because</span>
                        <div class="info attention">
                            <span>attention!</span>
                        </div>
                    </div>
                </li>
            </ul>
        </div>
    </div>
    <div class="timeLineItemContainer">
        <div class="subItemMiddleContainer">
            <div class="timeLineLeft">
                <div class="timeLinePipe"></div>
            </div>
            <div class="timeLineHeader">
                <div>
                    <span>Some other text</span>
                </div>
                <div>
                    <div>Ola!</div>
                </div>
            </div>
        </div>
        <div class="subItemBottomContainer">
            <ul>

                <li class="static">
                    <div class="timeLineRight timeLineInfo">
                        <span class="info">A date</span>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">label</span>
                        <span class="info">value</span>
                    </div>
                </li>
                <li class="toggable closed">
                    <div class="timeLineRight timeLineInfo">
                        <span class="label">anotehr label</span>
                        <span class="info time">different value</span>
                    </div>
                </li>
            </ul>
        </div>
    </div>

Javascript

var sw = screen.width;
var thresholdPercentageSwipeTimeLineItem = 15;
var thresholdPercentageSwipeDetailScreen = 10;    
    
$(".timeLineItemContainer").draggable({
    scroll: false,
    axis: "x",
    drag: function (event, ui) {
        if (ui.position.left < 0) {
            ui.position.left = 0;
        }
        else {
          if (calculateOffsetPercentage(ui.position.left) > thresholdPercentageSwipeTimeLineItem) {
              return false;
          };
        }
    },
    stop: function (event, ui) {
        $(this).animate({
           left: 0,
        }, {
           duration: 200,
           queue: false
        });
    }
}).click(function () {
    var nextObjs = $(this).toggleClass("visible").find(".toggable");

    $.each(nextObjs, function () {
        $(this).stop().animate({
            height: "toggle",
            queue: false
        }).toggleClass("closed");
    });
   
});

function calculateOffsetPercentage(screenValue) {
    return (100 / (sw / screenValue));
}

Solution

  • Nailed it!

    On first thoughts it looks like the $(".timeLineItemContainer").draggable({ ... }); is consuming all of the events, but it is actually the jquery-ui-touch-punch.js who is preventing everything.

    jquery-ui-touch-punch.js works in a way that all the touch events results are prevented to execute the default behavior. Therefore I tweaked the jquery-ui-touch-punch.js a bit to get the right behavior.

    1. I removed the event.preventDefault() from the simulateMouseEvent() function.

    2. I store the start of the touch in the mouseProto._touchStart event

      tsx = event.originalEvent.touches[0].clientX; tsy = event.originalEvent.touches[0].clientY;

    3. I added a condition inside the mouseProto._touchMove event to check if I'm scrolling in a horizontal or vertical direction. In case of a horizontal direction I'm executing the event.preventDefault(). To get to that, you can use:

      // Higher then 1 means a (more) horizontal direction // Lower then 1 means a (more) vertical direction // 1 is an exact 45 degrees swipe, which will be handled as a vertical swipe if ((Math.abs(CurrentX - StartX)) / Math.abs(CurrentY - StartY) > 1) { event.preventDefault(); }

    The endresult of the mouseProto._touchStart and the mouseProto._touchMove function look like this:

    var tsx = 0, 
        tsy = 0, 
        currentx = 0, 
        currenty = 0;
    
    mouseProto._touchStart = function (event) {
        var self = this;
    
        tsx = event.originalEvent.touches[0].clientX;
        tsy = event.originalEvent.touches[0].clientY;
    
        // Ignore the event if another widget is already being handled
        if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) {
            return;
        }
    
        // Set the flag to prevent other widgets from inheriting the touch event
        touchHandled = true;
    
        // Track movement to determine if interaction was a click
        self._touchMoved = false;
    
        //// Simulate the mouseover event
        simulateMouseEvent(event, 'mouseover');
    
        //// Simulate the mousemove event
        simulateMouseEvent(event, 'mousemove');
    
        // Simulate the mousedown event
        simulateMouseEvent(event, 'mousedown');
    
    };
    
    mouseProto._touchMove = function (event) {
        // Ignore event if not handled
        if (!touchHandled) {
            return;
        }
    
        // Higher then 1 means a (more) horizontal direction
        // Lower then 1 means a (more) vertical direction
        // 1 is an exact 45 degrees swipe, which will be handled as a vertical swipe
        if ((Math.abs(event.originalEvent.changedTouches[0].clientX - tsx)) / Math.abs(event.originalEvent.changedTouches[0].clientY - tsy) > 1) {
            event.preventDefault();
        }
    
        // Interaction was not a click
        this._touchMoved = true;
    
        // Simulate the mousemove event
        simulateMouseEvent(event, 'mousemove');
    };