Jump To …
Home | View this file on Github

jquery.pointerEvents.js

Pointer Events polyfill for jQuery

(function ($, window, document, undefined) {

    var support = {
        touch: "ontouchend" in document,
        pointer: !! (navigator.pointerEnabled || navigator.msPointerEnabled)
    };

    $.extend($.support, support);

    function triggerCustomEvent(elem, eventType, originalEvent) {

support for IE7-IE8

        originalEvent = originalEvent || window.event;


Create a writable copy of the event object and normalize some properties

        var event = new jQuery.Event(originalEvent);
        event.type = eventType;


Copy over properties for ease of access

        var i, copy = $.event.props.concat($.event.pointerHooks.props);
        i = copy.length;
        while (i--) {
            var prop = copy[i];
            event[prop] = originalEvent[prop];
        }


Support: IE<9 Fix target property (#1925)

        if (!event.target) {
            event.target = originalEvent.srcElement || document;
        }


Support: Chrome 23+, Safari? Target should not be a text node (#504, #13143)

        if (event.target.nodeType === 3) {
            event.target = event.target.parentNode;
        }


Support: IE<9 For mouse/key events, metaKey==false if it's undefined (#3368, #11328)

        event.metaKey = !! event.metaKey;


run the filter now

        event = $.event.pointerHooks.filter(event, originalEvent);


trigger the emulated pointer event the filter can return an array (only if the original was a touchmove), which means we need to trigger independent events

        if ($.isArray(event)) {
            $.each(event, function (i, ev) {
                $.event.dispatch.call(elem, ev);
            });
        } else {
            $.event.dispatch.call(elem, event);
        }


return the manipulated jQuery event

        return event;
    }

    function addEvent(elem, type, selector, func) {

when we have a selector, let jQuery do the delegation

        if (selector) {
            func._pointerEventWrapper = function (event) {
                return func.call(elem, event.originalEvent);
            };

            $(elem).on(type, selector, func._pointerEventWrapper);
        }


if we do not have a selector, we optimize by cutting jQuery out

        else {
            if (elem.addEventListener) {
                elem.addEventListener(type, func, false);
            } else if (elem.attachEvent) {


bind the function to correct "this" for IE8-

                func._pointerEventWrapper = function (e) {
                    return func.call(elem, e);
                };

                elem.attachEvent("on" + type, func._pointerEventWrapper);
            }
        }
    }

    function removeEvent(elem, type, selector, func) {

Make sure for IE8- we unbind the wrapper

        if (func._pointerEventWrapper) {
            func = func._pointerEventWrapper;
        }

        if (selector) {
            $(elem).off(type, selector, func);
        } else {
            $.removeEvent(elem, type, func);
        }
    }


get the standardized "buttons" property as per the Pointer Events spec from a mouse event

    function getStandardizedButtonsProperty(event) {

in the DOM LEVEL 3 spec there is a new standard for the "buttons" property sadly, no browser currently supports this and only sends us the single "button" property

        if (event.buttons) {
            return event.buttons;
        }


standardize "which" property for use

        var which = event.which;
        if (!which && event.button !== undefined) {
            which = (event.button & 1 ? 1 : (event.button & 2 ? 3 : (event.button & 4 ? 2 : 0)));
        }


no button down (can happen on mousemove)

        if (which === 0) {
            return 0;
        }

left button

        else if (which === 1) {
            return 1;
        }

middle mouse

        else if (which === 2) {
            return 4;
        }

right mouse

        else if (which === 3) {
            return 2;
        }


unknown?

        return 0;
    }

    var POINTER_TYPE_UNAVAILABLE = "unavailable";
    var POINTER_TYPE_TOUCH = "touch";
    var POINTER_TYPE_PEN = "pen";
    var POINTER_TYPE_MOUSE = "mouse";


signal to mark if the pointer is down and which button(s) are depressed used only as part of the polyfill for Touch and Mouse Events API

    var _isPointerDown = false;


storage of the last seen touches provided by the native touch events spec

    var _lastTouches = [];


------ NOTE: THIS IS UNUSED, WE DO NOT ASSIGN BUTTON ------ pointer events defines the "button" property as: mouse move (no buttons down) -1 left mouse, touch contact and normal pen contact 0 middle mouse 1 right mouse, pen with barrel button pressed 2 x1 (back button on mouse) 3 x2 (forward button on mouse) 4 pen contact with eraser button pressed 5 ------ NOTE: THIS IS UNUSED, WE DO NOT ASSIGN BUTTON ------



pointer events defines the "buttons" property as: mouse move (no buttons down) 0 left mouse, touch contact, and normal pen contact 1 middle mouse 4 right mouse, pen contact with barrel button pressed 2 x1 (back) mouse 8 x2 (forward) mouse 16 pen contact with eraser button pressed 32



add our own pointer event hook/filter

    $.event.pointerHooks = {
        props: "pointerType pointerId buttons clientX clientY relatedTarget fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
        filter: function (event, original) {

Calculate pageX/Y if missing and clientX/Y available this is just copied from jQuery's standard pageX/pageY fix

            if (!original.touches && event.pageX == null && original.clientX != null) {
                var eventDoc = event.target.ownerDocument || document;
                var doc = eventDoc.documentElement;
                var body = eventDoc.body;

                event.pageX = original.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
                event.pageY = original.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
            }


Add relatedTarget, if necessary also copied from jQuery's standard event fix

            if (!event.relatedTarget && original.fromElement) {
                event.relatedTarget = original.fromElement === event.target ? original.toElement : original.fromElement;
            }


Add pointerType

            if (!event.pointerType || typeof event.pointerType == "number") {
                if (event.pointerType == 2) {
                    event.pointerType = POINTER_TYPE_TOUCH;
                } else if (event.pointerType == 3) {
                    event.pointerType = POINTER_TYPE_PEN;
                } else if (event.pointerType == 4) {
                    event.pointerType = POINTER_TYPE_MOUSE;
                } else if (/^touch/i.test(original.type)) {
                    event.pointerType = POINTER_TYPE_TOUCH;
                    event.buttons = original.type === "touchend" ? 0 : 1;
                } else if (/^mouse/i.test(original.type) || original.type == "click") {
                    event.pointerId = 1; // as per the pointer events spec, the mouse is always pointer id 1
                    event.pointerType = POINTER_TYPE_MOUSE;
                    event.buttons = original.type === "mouseup" ? 0 : getStandardizedButtonsProperty(original);
                } else {
                    event.pointerType = POINTER_TYPE_UNAVAILABLE;
                    event.buttons = 0;
                }
            }


if we have the bitmask for the depressed buttons from the mouse events polyfill, use it to mimic buttons for browsers that do not support the HTML DOM LEVEL 3 events spec

            if (event.type === "pointermove" && typeof _isPointerDown !== "boolean" && _isPointerDown !== event.buttons) {
                event.buttons = _isPointerDown;
            }


touch events send an array of touches we need to convert to the pointer events format which means we need to fire multiple events per touch

            if (original.touches) {
                var touches = original.touches;
                var events = [];
                var ev, i, j;


the problem with this is that on touchend it will remove the touch which has ended from the touches list, this means we do not want to fire pointerup for touches that are still there, we instead want to send a pointerup with the removed touch's identifier

                if (event.type === "pointerup") {

convert TouchList to a standard array

                    _lastTouches = Array.prototype.slice.call(_lastTouches);


find the touch that was removed

                    for (i = 0; i < original.touches.length; i++) {
                        for (j = 0; j < _lastTouches.length; j++) {
                            if (_lastTouches[j].identifier === original.touches[i].identifier) {
                                _lastTouches.splice(j, 1);
                            }
                        }
                    }


if we narrowed down the ended touch to one, then we found it

                    if (_lastTouches.length === 1) {
                        event.pointerId = _lastTouches[0].identifier;
                        _lastTouches = original.touches;
                        return event;
                    }
                }

on pointerdown we need to only trigger a new pointerdown for the touch, and not the touches that were already there

                else if (event.type === "pointerdown") {

convert TouchList to a standard array

                    touches = Array.prototype.slice.call(original.touches);


find the new touch that was just added

                    for (i = 0; i < touches.length; i++) {

last touches will be a list with one less touch

                        for (j = 0; j < _lastTouches.length; j++) {
                            if (touches[i].identifier === _lastTouches[j].identifier) {
                                touches.splice(i, 1);
                            }
                        }
                    }
                }


this will be used on pointermove and pointerdown

                for (i = 0; i < original.touches.length; i++) {
                    var touch = original.touches[i];
                    ev = $.extend({}, event);

copy over information from the touch to the event

                    ev.clientX = touch.clientX;
                    ev.clientY = touch.clientY;
                    ev.pageX = touch.pageX;
                    ev.pageY = touch.pageY;
                    ev.screenX = touch.screenX;
                    ev.screenY = touch.screenY;

the touch id on emulated touch events from chrome is always 0 (zero)

                    ev.pointerId = touch.identifier;
                    events.push(ev);
                }


do as little processing as you can here, this is done on touchmove and there can be a lot of those events firing quickly, we do not want the polyfill slowing down the application

                _lastTouches = original.touches;
                return events;
            }

            return event;
        }
    };

    $.event.delegateSpecial = function (setup) {
        return function (handleObj) {
            var thisObject = this,
                data = jQuery._data(thisObject);

            if (!data.pointerEvents) {
                data.pointerEvents = {};
            }

            if (!data.pointerEvents[handleObj.type]) {
                data.pointerEvents[handleObj.type] = [];
            }

            if (!data.pointerEvents[handleObj.type].length) {
                setup.call(thisObject, handleObj);
            }

            data.pointerEvents[handleObj.type].push(handleObj);
        };
    };

    $.event.delegateSpecial.remove = function (teardown) {
        return function (handleObj) {
            var handlers,
                thisObject = this,
                data = jQuery._data(thisObject);

            if (!data.pointerEvents) {
                data.pointerEvents = {};
            }

            handlers = data.pointerEvents[handleObj.type];

            handlers.splice(handlers.indexOf(handleObj), 1);

            if (!handlers.length) {
                teardown.call(thisObject, handleObj);
            }
        };
    };


allow jQuery's native $.event.fix to find our pointer hooks

    $.extend($.event.fixHooks, {
        pointerdown: $.event.pointerHooks,
        pointerup: $.event.pointerHooks,
        pointermove: $.event.pointerHooks,
        pointerover: $.event.pointerHooks,
        pointerout: $.event.pointerHooks,
        pointercancel: $.event.pointerHooks
    });


if browser does not natively handle pointer events, create special custom events to mimic them

    if (!support.pointer) {

stores the scroll-y offest on touchstart and is compared on touchend to see if we should trigger a click event

        var _startScrollOffset;


utility to return the scroll-y position

        var scrollY = function () {
            return Math.floor(window.scrollY || $(window).scrollTop());
        };

        $.event.special.pointerdown = {
            touch: function (event) {

prevent default to prevent the emulated "mousedown" event from being triggered, we will force-emulate the click event again from within tounend:pointerup event.preventDefault();


                triggerCustomEvent(this, "pointerdown", event);


set the pointer as currently down to prevent chorded "pointerdown" events

                _isPointerDown = true;


set the scroll offset which is compared on touchend

                _startScrollOffset = scrollY();
            },
            mouse: function (event) {

_isPointerDown is true when touch is down, this means we do not want to listen to mouse events too

                if (_isPointerDown === true) {
                    return;
                }


do not trigger another "pointerdown" event if currently down, prevent chorded "pointerdown" events

                if (_isPointerDown !== false) {
                    var button = getStandardizedButtonsProperty(event);
                    if (_isPointerDown !== button) {
                        _isPointerDown |= button;

as per the pointer event spec, when the active "buttons" change it fires a new "pointermove" with the new buttons, but not a new pointerdown event (chorded)

                        triggerCustomEvent(this, "pointermove", event);
                        return;
                    }
                }

                var jEvent = triggerCustomEvent(this, "pointerdown", event);


set the pointer as currently down to prevent chorded "pointerdown" events

                _isPointerDown = jEvent.buttons;
            },
            add: $.event.delegateSpecial(function (handleObj) {

bind to touch events, some devices (chromebook) can send both touch and mouse events

                if (support.touch) {
                    addEvent(this, "touchstart", handleObj.selector, $.event.special.pointerdown.touch);
                }


bind to mouse events

                addEvent(this, "mousedown", handleObj.selector, $.event.special.pointerdown.mouse);


ensure we also bind to "pointerup" to properly clear signals and fire click event on "touchend"

                handleObj.pointerup = $.noop;
                $(this).on("pointerup", handleObj.selector, handleObj.pointerup);
            }),
            remove: $.event.delegateSpecial.remove(function (handleObj) {

unbind touch events

                if (support.touch) {
                    removeEvent(this, "touchstart", handleObj.selector, $.event.special.pointerdown.touch);
                }


unbind mouse events

                removeEvent(this, "mousedown", handleObj.selector, $.event.special.pointerdown.mouse);


unbind the special "pointerup" we added for cleanup

                if (handleObj.pointerup) {
                    $(this).off("pointerup", handleObj.selector, handleObj.pointerup);
                }
            })
        };

        $.event.special.pointerup = {
            touch: function (event) {

prevent default to prevent the emulated "mouseup" event from being triggered

                event.preventDefault();

                triggerCustomEvent(this, "pointerup", event);


release the pointerdown lock

                _isPointerDown = false;


on "touchend", calling prevent default prevents the "mouseup" and "click" event however on native "mouseup" events preventing default does not cancel the "click" event as per the pointer event spec on "pointerup" preventing default should not cancel the "click" event

we really do want to call this all the time, because if the function binded to this emulated "poiunterup" triggered above called prevent default it would also prevent the click, which would cause inconsistent behavior. To prevent the possibility of two click events though, we want to call prevent default all the time (as we do above) and then force trigger the click here

we confirm that the user did not scroll, as touch events are very related to scrolling on touch devices, it's possible we may mis-fire a click event on an anchor tag causing navigation even though this was a scroll attempt. Do the the browser's built in threshold to prevent accidental scrolling we do not add a threshold here.

                if (event.target && event.target.click && _startScrollOffset === scrollY()) {
                    event.target.click();
                }
            },
            mouse: function (event) {

the Mouse Events API provides the button on mouseup

                var button = getStandardizedButtonsProperty(event);


remove the button from the current pointers down signal _isPointerDown can be false here if two "mouseup" events are received in parallel, which can happen, say, if you bind to "pointerup" on a parent and a child (body and a link)

                if (_isPointerDown !== false) {
                    _isPointerDown ^= button;
                }


reset _isPointerDown to a boolean if no buttons are down

                if (_isPointerDown === 0) {
                    _isPointerDown = false;
                }


do not trigger another "pointerdown" event if currently down, prevent chorded pointerdown events

                if (_isPointerDown) {

the mouse events spec shows that upon "mouseup" it fires a "mousemove" afterwards, which will trigger the "pointermove" we need to trigger to follow the pointer events spec

                    return;
                }

                var jEvent = triggerCustomEvent(this, "pointerup", event);


set the pointer as currently down to prevent chorded "pointerdown" events

                _isPointerDown = jEvent.buttons;


release the pointer down lock on "mouseup"

                _isPointerDown = false;
            },
            add: $.event.delegateSpecial(function (handleObj) {

bind to touch events, some devices (chromebook) can send both touch and mouse events

                if (support.touch) {
                    addEvent(this, "touchend", handleObj.selector, $.event.special.pointerup.touch);
                }


bind mouse events

                addEvent(this, "mouseup", handleObj.selector, $.event.special.pointerup.mouse);
            }),
            remove: $.event.delegateSpecial.remove(function (handleObj) {

unbind touch events

                if (support.touch) {
                    removeEvent(this, "touchend", handleObj.selector, $.event.special.pointerup.touch);
                }


unbind mouse events

                removeEvent(this, "mouseup", handleObj.selector, $.event.special.pointerup.mouse);
            })
        };

        $.event.special.pointermove = {
            touch: function (event) {
                triggerCustomEvent(this, "pointermove", event);
            },
            mouse: function (event) {

_isPointerDown will be true if they currently have their finger (touch only) down because we cannot call preventDefault on the "touchmove" we get double triggers and we prevent it with this signal check. preventing default on "touchmove" prevents scrolling on mobile devices

                if (_isPointerDown === true) {
                    return false;
                }

                triggerCustomEvent(this, "pointermove", event);
            },
            add: $.event.delegateSpecial(function (handleObj) {

bind to touch events, some devices (chromebook) can send both touch and mouse events

                if (support.touch) {
                    addEvent(this, "touchmove", handleObj.selector, $.event.special.pointermove.touch);
                }


bind mouse events

                addEvent(this, "mousemove", handleObj.selector, $.event.special.pointermove.mouse);
            }),
            remove: $.event.delegateSpecial.remove(function (handleObj) {

unbind touch events

                if (support.touch) {
                    removeEvent(this, "touchmove", handleObj.selector, $.event.special.pointermove.touch);
                }


unbind mouse events

                removeEvent(this, "mousemove", handleObj.selector, $.event.special.pointermove.mouse);
            })
        };

        jQuery.each({
            pointerover: {
                mouse: "mouseover"
            },
            pointerout: {
                mouse: "mouseout"
            },
            pointercancel: {
                touch: "touchcancel"
            }
        }, function (pointerEventType, natives) {
            function onTouch(event) {
                event.preventDefault();
                triggerCustomEvent(this, pointerEventType, event);
            }

            function onMouse(event) {
                triggerCustomEvent(this, pointerEventType, event);
            }

            $.event.special[pointerEventType] = {
                setup: function () {
                    if (support.touch && natives.touch) {
                        addEvent(this, natives.touch, null, onTouch);
                    }
                    if (natives.mouse) {
                        addEvent(this, natives.mouse, null, onMouse);
                    }
                },
                teardown: function () {
                    if (support.touch && natives.touch) {
                        removeEvent(this, natives.touch, null, onTouch);
                    }
                    if (natives.mouse) {
                        removeEvent(this, natives.mouse, null, onMouse);
                    }
                }
            };
        });
    }


for IE10 specific, we proxy though events so we do not need to deal with the various names or renaming of events.

    else if (navigator.msPointerEnabled && !navigator.pointerEnabled) {
        $.extend($.event.special, {
            pointerdown: {
                delegateType: "MSPointerDown",
                bindType: "MSPointerDown"
            },
            pointerup: {
                delegateType: "MSPointerUp",
                bindType: "MSPointerUp"
            },
            pointermove: {
                delegateType: "MSPointerMove",
                bindType: "MSPointerMove"
            },
            pointerover: {
                delegateType: "MSPointerOver",
                bindType: "MSPointerOver"
            },
            pointerout: {
                delegateType: "MSPointerOut",
                bindType: "MSPointerOut"
            },
            pointercancel: {
                delegateType: "MSPointerCancel",
                bindType: "MSPointerCancel"
            }
        });

        $.extend($.event.fixHooks, {
            MSPointerDown: $.event.pointerHooks,
            MSPointerUp: $.event.pointerHooks,
            MSPointerMove: $.event.pointerHooks,
            MSPointerOver: $.event.pointerHooks,
            MSPointerOut: $.event.pointerHooks,
            MSPointerCancel: $.event.pointerHooks
        });
    }

})(jQuery, window, document);