Is there a way to prevent fastclick from firing “active” state on scroll?
Asked Answered
S

2

14

I’m using FastClick on a page with large links because I want to bypass the 300ms delay for taps in mobile browsers. I have a “highlight” style for the links’ :active states and it is properly firing quickly, thanks to FastClick.

My problem is that – in Mobile Safari at least – it also fires while you’re tapping and swiping to scroll the page. This makes it feel like you can’t scroll the page without it thinking you’re trying to tap the links.

Is there a way to prevent it from firing when someone scrolls?

Segmental answered 20/1, 2016 at 21:1 Comment(0)
G
2

Maybe you can add the needsclick class to the body?

<body class="needsclick">

...

</body>

Just an idea :)

Guidry answered 22/1, 2016 at 21:35 Comment(1)
Thanks… that would actually prevent FastClick from working for the controls at all.Segmental
S
2

Nice question! +1

This problem has nothing to do with FastClick, but FastClick does make the solution to your problem more complex. So I will stick to pure JavaScript and raw Touch Events )

On a mobile touch device the implementation of the webview is significantly different from the desktop web browser for reasons specific to the platform. One feature that is important in this case is momentum scrolling in the webview. Momentum Scrolling is a hardware accelerated feature of the device. So when a scrollable area is touched on the screen, the hardware identifies the touch and attaches a 200 millisecond countdown timer to the scrollable area, that when triggered, puts the area into a hardware accelerated scroll. When the hardware is in the state it does not broadcast touch events because they are specific to the hardware accelerated scroll. The timer can be cancelled, and momentum scrolling prevented by using preventDefault on the touch event within the provided 200 milliseconds.

Here is how I approach this problem. I assume you are using native scroll i.e overflow:scroll. The method is to attach a touchstart event to the element you want touchable. Once the touchstart event has fired, the event handler attaches touchend, touchmove and touchcancel events to the target element. A setTimout timer is initiated that removes the newly added events after 140ms.

Here are some clips for my production code: I have two event methods for adding and removing events from collections:

var eventify = (function () {
    function eventify(type, el, callback, phase) {
        phase = phase || false;
        if ((el.constructor.toString().contains('NodeList')) || (el.constructor.toString().contains('HTMLCollection'))) {
            [].forEach.call(el, function (element) {
                if (!element.hasEvent(type)) {
                    element.addEvent(type);
                    HTMLElement.prototype.addEventListener.apply(element, [type, callback, phase]);
                }
            });
        } else {
            if (!el.hasEvent(type)) {
                el.addEvent(type);
                HTMLElement.prototype.addEventListener.apply(el, [type, callback, phase]);
            }
        }
        return callback;
    }

    return eventify
})();

var uneventify = (function () {
    function uneventify(type, el, callback) {
        if ((el.constructor.toString().contains('NodeList')) || (el.constructor.toString().contains('HTMLCollection'))) {
            [].forEach.call(el, function (element) {
                if (element.hasEvent(type)) {
                    element.removeEvent(type);
                    HTMLElement.prototype.removeEventListener.apply(element, [type, callback]);
                }
            });
        } else {
            if (el.hasEvent(type)) {
                el.removeEvent(type);
                HTMLElement.prototype.removeEventListener.apply(el, [type, callback]);
            }
        }
    }

    return uneventify
})();

Then I have a tapify method for tap events on the scroller:

var tapify = (function () {
    function tapify(el, callback) {
        eventify('touchstart', el, function (e) {
            var that = this;
            var start = e.pageY;
            var target = e.target;
            function dynamicEvents() {
                var endfn = eventify('touchend', target, function (evt) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                    evt.preventDefault();
                    evt.stopImmediatePropagation();
                    uneventify('touchmove', target, movefn);
                    uneventify('touchend', target, endfn);
                    uneventify('touchcancel', target, cancelfn);
                    callback && callback(target);
                });
                var cancelfn = eventify('touchcancel', target, function (evt) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                    evt.preventDefault();
                    evt.stopImmediatePropagation();
                    uneventify('touchmove', target, movefn);
                    uneventify('touchend', target, endfn);
                    uneventify('touchcancel', target, cancelfn);
                    callback && callback(target);
                });
                var movefn = eventify('touchmove', target, function (evt) {
                    var distance = start - evt.pageY;
                    if (distance > 20) {
                        uneventify('touchend', target, endfn);
                        uneventify('touchcancel', target, cancelfn);
                        uneventify('touchmove', el, movefn);
                    }
                });
                setTimeout(function () {
                    uneventify('touchmove', target, movefn);
                    uneventify('touchend', target, endfn);
                    uneventify('touchcancel', target, cancelfn);
                }, 140);
            }
            if (global.isIos) setTimeout(function () {
                dynamicEvents();
            }, 60);
            else dynamicEvents();
        }, false);
    }
    return tapify;
})();

I use global.isIos to identify the target device. Android stops sending touch events to webview ater 200ms.

Then to attach the event to a element or a collection of elements, use :

tapify(document.querySelectorAll('button'), function (e) {
      //your event handler here!! 
});

Hope this helps

Simmons answered 26/1, 2016 at 9:1 Comment(3)
It is possible to use this same method for swipe and drag events on a native scrolling surface.Simmons
Thanks for the reply! Right now I was hoping to try something compatible with FastClick and … as a JS newbie I’m admittedly in over my head with your answer but I’m not seeing how it connects with FastClick. But if I get brave I’ll attempt adding some of your code into my project.Segmental
i suggest you attach FastClick only on the elements that require it and not the document.bodySimmons

© 2022 - 2024 — McMap. All rights reserved.