// jquery.event.move // // 1.3.1 // // stephen band // // triggers 'movestart', 'move' and 'moveend' events after // mousemoves following a mousedown cross a distance threshold, // similar to the native 'dragstart', 'drag' and 'dragend' events. // move events are throttled to animation frames. move event objects // have the properties: // // pagex: // pagey: page coordinates of pointer. // startx: // starty: page coordinates of pointer at movestart. // distx: // disty: distance the pointer has moved since movestart. // deltax: // deltay: distance the finger has moved since last event. // velocityx: // velocityy: average velocity over last few events. (function (module) { if (typeof define === 'function' && define.amd) { // amd. register as an anonymous module. define(['jquery'], module); } else { // browser globals module(jquery); } })(function(jquery, undefined){ var // number of pixels a pressed pointer travels before movestart // event is fired. threshold = 6, add = jquery.event.add, remove = jquery.event.remove, // just sugar, so we can have arguments in the same order as // add and remove. trigger = function(node, type, data) { jquery.event.trigger(type, data, node); }, // shim for requestanimationframe, falling back to timer. see: // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ requestframe = (function(){ return ( window.requestanimationframe || window.webkitrequestanimationframe || window.mozrequestanimationframe || window.orequestanimationframe || window.msrequestanimationframe || function(fn, element){ return window.settimeout(function(){ fn(); }, 25); } ); })(), ignoretags = { textarea: true, input: true, select: true, button: true }, mouseevents = { move: 'mousemove', cancel: 'mouseup dragstart', end: 'mouseup' }, touchevents = { move: 'touchmove', cancel: 'touchend', end: 'touchend' }; // constructors function timer(fn){ var callback = fn, active = false, running = false; function trigger(time) { if (active){ callback(); requestframe(trigger); running = true; active = false; } else { running = false; } } this.kick = function(fn) { active = true; if (!running) { trigger(); } }; this.end = function(fn) { var cb = callback; if (!fn) { return; } // if the timer is not running, simply call the end callback. if (!running) { fn(); } // if the timer is running, and has been kicked lately, then // queue up the current callback and the end callback, otherwise // just the end callback. else { callback = active ? function(){ cb(); fn(); } : fn ; active = true; } }; } // functions function returntrue() { return true; } function returnfalse() { return false; } function preventdefault(e) { e.preventdefault(); } function preventignoretags(e) { // don't prevent interaction with form elements. if (ignoretags[ e.target.tagname.tolowercase() ]) { return; } e.preventdefault(); } function isleftbutton(e) { // ignore mousedowns on any button other than the left (or primary) // mouse button, or when a modifier key is pressed. return (e.which === 1 && !e.ctrlkey && !e.altkey); } function identifiedtouch(touchlist, id) { var i, l; if (touchlist.identifiedtouch) { return touchlist.identifiedtouch(id); } // touchlist.identifiedtouch() does not exist in // webkit yet… we must do the search ourselves... i = -1; l = touchlist.length; while (++i < l) { if (touchlist[i].identifier === id) { return touchlist[i]; } } } function changedtouch(e, event) { var touch = identifiedtouch(e.changedtouches, event.identifier); // this isn't the touch you're looking for. if (!touch) { return; } // chrome android (at least) includes touches that have not // changed in e.changedtouches. that's a bit annoying. check // that this touch has changed. if (touch.pagex === event.pagex && touch.pagey === event.pagey) { return; } return touch; } // handlers that decide when the first movestart is triggered function mousedown(e){ var data; if (!isleftbutton(e)) { return; } data = { target: e.target, startx: e.pagex, starty: e.pagey, timestamp: e.timestamp }; add(document, mouseevents.move, mousemove, data); add(document, mouseevents.cancel, mouseend, data); } function mousemove(e){ var data = e.data; checkthreshold(e, data, e, removemouse); } function mouseend(e) { removemouse(); } function removemouse() { remove(document, mouseevents.move, mousemove); remove(document, mouseevents.cancel, mouseend); } function touchstart(e) { var touch, template; // don't get in the way of interaction with form elements. if (ignoretags[ e.target.tagname.tolowercase() ]) { return; } touch = e.changedtouches[0]; // ios live updates the touch objects whereas android gives us copies. // that means we can't trust the touchstart object to stay the same, // so we must copy the data. this object acts as a template for // movestart, move and moveend event objects. template = { target: touch.target, startx: touch.pagex, starty: touch.pagey, timestamp: e.timestamp, identifier: touch.identifier }; // use the touch identifier as a namespace, so that we can later // remove handlers pertaining only to this touch. add(document, touchevents.move + '.' + touch.identifier, touchmove, template); add(document, touchevents.cancel + '.' + touch.identifier, touchend, template); } function touchmove(e){ var data = e.data, touch = changedtouch(e, data); if (!touch) { return; } checkthreshold(e, data, touch, removetouch); } function touchend(e) { var template = e.data, touch = identifiedtouch(e.changedtouches, template.identifier); if (!touch) { return; } removetouch(template.identifier); } function removetouch(identifier) { remove(document, '.' + identifier, touchmove); remove(document, '.' + identifier, touchend); } // logic for deciding when to trigger a movestart. function checkthreshold(e, template, touch, fn) { var distx = touch.pagex - template.startx, disty = touch.pagey - template.starty; // do nothing if the threshold has not been crossed. if ((distx * distx) + (disty * disty) < (threshold * threshold)) { return; } triggerstart(e, template, touch, distx, disty, fn); } function handled() { // this._handled should return false once, and after return true. this._handled = returntrue; return false; } function flagashandled(e) { e._handled(); } function triggerstart(e, template, touch, distx, disty, fn) { var node = template.target, touches, time; touches = e.targettouches; time = e.timestamp - template.timestamp; // create a movestart object with some special properties that // are passed only to the movestart handlers. template.type = 'movestart'; template.distx = distx; template.disty = disty; template.deltax = distx; template.deltay = disty; template.pagex = touch.pagex; template.pagey = touch.pagey; template.velocityx = distx / time; template.velocityy = disty / time; template.targettouches = touches; template.finger = touches ? touches.length : 1 ; // the _handled method is fired to tell the default movestart // handler that one of the move events is bound. template._handled = handled; // pass the touchmove event so it can be prevented if or when // movestart is handled. template._preventtouchmovedefault = function() { e.preventdefault(); }; // trigger the movestart event. trigger(template.target, template); // unbind handlers that tracked the touch or mouse up till now. fn(template.identifier); } // handlers that control what happens following a movestart function activemousemove(e) { var event = e.data.event, timer = e.data.timer; updateevent(event, e, e.timestamp, timer); } function activemouseend(e) { var event = e.data.event, timer = e.data.timer; removeactivemouse(); endevent(event, timer, function() { // unbind the click suppressor, waiting until after mouseup // has been handled. settimeout(function(){ remove(event.target, 'click', returnfalse); }, 0); }); } function removeactivemouse(event) { remove(document, mouseevents.move, activemousemove); remove(document, mouseevents.end, activemouseend); } function activetouchmove(e) { var event = e.data.event, timer = e.data.timer, touch = changedtouch(e, event); if (!touch) { return; } // stop the interface from gesturing e.preventdefault(); event.targettouches = e.targettouches; updateevent(event, touch, e.timestamp, timer); } function activetouchend(e) { var event = e.data.event, timer = e.data.timer, touch = identifiedtouch(e.changedtouches, event.identifier); // this isn't the touch you're looking for. if (!touch) { return; } removeactivetouch(event); endevent(event, timer); } function removeactivetouch(event) { remove(document, '.' + event.identifier, activetouchmove); remove(document, '.' + event.identifier, activetouchend); } // logic for triggering move and moveend events function updateevent(event, touch, timestamp, timer) { var time = timestamp - event.timestamp; event.type = 'move'; event.distx = touch.pagex - event.startx; event.disty = touch.pagey - event.starty; event.deltax = touch.pagex - event.pagex; event.deltay = touch.pagey - event.pagey; // average the velocity of the last few events using a decay // curve to even out spurious jumps in values. event.velocityx = 0.3 * event.velocityx + 0.7 * event.deltax / time; event.velocityy = 0.3 * event.velocityy + 0.7 * event.deltay / time; event.pagex = touch.pagex; event.pagey = touch.pagey; timer.kick(); } function endevent(event, timer, fn) { timer.end(function(){ event.type = 'moveend'; trigger(event.target, event); return fn && fn(); }); } // jquery special event definition function setup(data, namespaces, eventhandle) { // stop the node from being dragged //add(this, 'dragstart.move drag.move', preventdefault); // prevent text selection and touch interface scrolling //add(this, 'mousedown.move', preventignoretags); // tell movestart default handler that we've handled this add(this, 'movestart.move', flagashandled); // don't bind to the dom. for speed. return true; } function teardown(namespaces) { remove(this, 'dragstart drag', preventdefault); remove(this, 'mousedown touchstart', preventignoretags); remove(this, 'movestart', flagashandled); // don't bind to the dom. for speed. return true; } function addmethod(handleobj) { // we're not interested in preventing defaults for handlers that // come from internal move or moveend bindings if (handleobj.namespace === "move" || handleobj.namespace === "moveend") { return; } // stop the node from being dragged add(this, 'dragstart.' + handleobj.guid + ' drag.' + handleobj.guid, preventdefault, undefined, handleobj.selector); // prevent text selection and touch interface scrolling add(this, 'mousedown.' + handleobj.guid, preventignoretags, undefined, handleobj.selector); } function removemethod(handleobj) { if (handleobj.namespace === "move" || handleobj.namespace === "moveend") { return; } remove(this, 'dragstart.' + handleobj.guid + ' drag.' + handleobj.guid); remove(this, 'mousedown.' + handleobj.guid); } jquery.event.special.movestart = { setup: setup, teardown: teardown, add: addmethod, remove: removemethod, _default: function(e) { var template, data; // if no move events were bound to any ancestors of this // target, high tail it out of here. if (!e._handled()) { return; } template = { target: e.target, startx: e.startx, starty: e.starty, pagex: e.pagex, pagey: e.pagey, distx: e.distx, disty: e.disty, deltax: e.deltax, deltay: e.deltay, velocityx: e.velocityx, velocityy: e.velocityy, timestamp: e.timestamp, identifier: e.identifier, targettouches: e.targettouches, finger: e.finger }; data = { event: template, timer: new timer(function(time){ trigger(e.target, template); }) }; if (e.identifier === undefined) { // we're dealing with a mouse // stop clicks from propagating during a move add(e.target, 'click', returnfalse); add(document, mouseevents.move, activemousemove, data); add(document, mouseevents.end, activemouseend, data); } else { // we're dealing with a touch. stop touchmove doing // anything defaulty. e._preventtouchmovedefault(); add(document, touchevents.move + '.' + e.identifier, activetouchmove, data); add(document, touchevents.end + '.' + e.identifier, activetouchend, data); } } }; jquery.event.special.move = { setup: function() { // bind a noop to movestart. why? it's the movestart // setup that decides whether other move events are fired. add(this, 'movestart.move', jquery.noop); }, teardown: function() { remove(this, 'movestart.move', jquery.noop); } }; jquery.event.special.moveend = { setup: function() { // bind a noop to movestart. why? it's the movestart // setup that decides whether other move events are fired. add(this, 'movestart.moveend', jquery.noop); }, teardown: function() { remove(this, 'movestart.moveend', jquery.noop); } }; add(document, 'mousedown.move', mousedown); add(document, 'touchstart.move', touchstart); // make jquery copy touch event properties over to the jquery event // object, if they are not already listed. but only do the ones we // really need. ie7/8 do not have array#indexof(), but nor do they // have touch events, so let's assume we can ignore them. if (typeof array.prototype.indexof === 'function') { (function(jquery, undefined){ var props = ["changedtouches", "targettouches"], l = props.length; while (l--) { if (jquery.event.props.indexof(props[l]) === -1) { jquery.event.props.push(props[l]); } } })(jquery); }; });