pazcal
pazcal

Reputation: 938

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));
}

Upvotes: 2

Views: 2630

Answers (2)

Elliot B.
Elliot B.

Reputation: 17651

I tried the OPs solution, but I think jQuery UI draggable may have changed since you last made this post. In my tests, even after completely removing jquery-ui-touch-punch.js I found that the drag event was still being consumed by jQuery UI draggable. I didn't want to dig into the jQuery UI code and fork the library, so I devised a fairly simple alternative.

In my scenario I had an inbox list whose items that you could drag to the left or right to expose action buttons. The entire inbox item must be draggable -- so the use of a drag handle was not an option.

jQuery's draggable prevents vertical scrolling on touch screens if the touch was initiated inside a draggable element. So if the screen was filled with draggable inbox items, then the user would become trapped -- unable to scroll up or down.

The solution that worked for me was to measure any change in the cursor's vertical position and use window.scrollBy to manually scroll the window by the same amount:

var firstY = null;      
var lastY = null;
var currentY = null;
var vertScroll = false;
var initAdjustment = 0;

// record the initial position of the cursor on start of the touch
jqDraggableItem.on("touchstart", function(event) {
    lastY = currentY = firstY = event.originalEvent.touches[0].pageY;
});

// fires whenever the cursor moves
jqDraggableItem.on("touchmove", function(event) {
    currentY = event.originalEvent.touches[0].pageY;
    var adjustment = lastY-currentY;

    // Mimic native vertical scrolling where scrolling only starts after the
    // cursor has moved up or down from its original position by ~30 pixels.
    if (vertScroll == false && Math.abs(currentY-firstY) > 30) {
        vertScroll = true;
        initAdjustment = currentY-firstY;
    }

    // only apply the adjustment if the user has met the threshold for vertical scrolling
    if (vertScroll == true) {
        window.scrollBy(0,adjustment + initAdjustment);
        lastY = currentY + adjustment;
    }

});

// when the user lifts their finger, they will again need to meet the 
// threshold before vertical scrolling starts.
jqDraggableItem.on("touchend", function(event) {
    vertScroll = false;
});

This will closely mimic native scrolling on a touch device.

Upvotes: 1

pazcal
pazcal

Reputation: 938

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');
};

Upvotes: 0

Related Questions