jquery.event.move.swipe.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. // jquery.event.move
  2. //
  3. // 1.3.1
  4. //
  5. // Stephen Band
  6. //
  7. // Triggers 'movestart', 'move' and 'moveend' events after
  8. // mousemoves following a mousedown cross a distance threshold,
  9. // similar to the native 'dragstart', 'drag' and 'dragend' events.
  10. // Move events are throttled to animation frames. Move event objects
  11. // have the properties:
  12. //
  13. // pageX:
  14. // pageY: Page coordinates of pointer.
  15. // startX:
  16. // startY: Page coordinates of pointer at movestart.
  17. // distX:
  18. // distY: Distance the pointer has moved since movestart.
  19. // deltaX:
  20. // deltaY: Distance the finger has moved since last event.
  21. // velocityX:
  22. // velocityY: Average velocity over last few events.
  23. (function (module) {
  24. if (typeof define === 'function' && define.amd) {
  25. // AMD. Register as an anonymous module.
  26. define(['jquery'], module);
  27. } else {
  28. // Browser globals
  29. module(jQuery);
  30. }
  31. })(function(jQuery, undefined){
  32. var // Number of pixels a pressed pointer travels before movestart
  33. // event is fired.
  34. threshold = 6,
  35. add = jQuery.event.add,
  36. remove = jQuery.event.remove,
  37. // Just sugar, so we can have arguments in the same order as
  38. // add and remove.
  39. trigger = function(node, type, data) {
  40. jQuery.event.trigger(type, data, node);
  41. },
  42. // Shim for requestAnimationFrame, falling back to timer. See:
  43. // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/
  44. requestFrame = (function(){
  45. return (
  46. window.requestAnimationFrame ||
  47. window.webkitRequestAnimationFrame ||
  48. window.mozRequestAnimationFrame ||
  49. window.oRequestAnimationFrame ||
  50. window.msRequestAnimationFrame ||
  51. function(fn, element){
  52. return window.setTimeout(function(){
  53. fn();
  54. }, 25);
  55. }
  56. );
  57. })(),
  58. ignoreTags = {
  59. textarea: true,
  60. input: true,
  61. select: true,
  62. button: true
  63. },
  64. mouseevents = {
  65. move: 'mousemove',
  66. cancel: 'mouseup dragstart',
  67. end: 'mouseup'
  68. },
  69. touchevents = {
  70. move: 'touchmove',
  71. cancel: 'touchend',
  72. end: 'touchend'
  73. };
  74. // Constructors
  75. function Timer(fn){
  76. var callback = fn,
  77. active = false,
  78. running = false;
  79. function trigger(time) {
  80. if (active){
  81. callback();
  82. requestFrame(trigger);
  83. running = true;
  84. active = false;
  85. }
  86. else {
  87. running = false;
  88. }
  89. }
  90. this.kick = function(fn) {
  91. active = true;
  92. if (!running) { trigger(); }
  93. };
  94. this.end = function(fn) {
  95. var cb = callback;
  96. if (!fn) { return; }
  97. // If the timer is not running, simply call the end callback.
  98. if (!running) {
  99. fn();
  100. }
  101. // If the timer is running, and has been kicked lately, then
  102. // queue up the current callback and the end callback, otherwise
  103. // just the end callback.
  104. else {
  105. callback = active ?
  106. function(){ cb(); fn(); } :
  107. fn ;
  108. active = true;
  109. }
  110. };
  111. }
  112. // Functions
  113. function returnTrue() {
  114. return true;
  115. }
  116. function returnFalse() {
  117. return false;
  118. }
  119. function preventDefault(e) {
  120. e.preventDefault();
  121. }
  122. function preventIgnoreTags(e) {
  123. // Don't prevent interaction with form elements.
  124. if (ignoreTags[ e.target.tagName.toLowerCase() ]) { return; }
  125. e.preventDefault();
  126. }
  127. function isLeftButton(e) {
  128. // Ignore mousedowns on any button other than the left (or primary)
  129. // mouse button, or when a modifier key is pressed.
  130. return (e.which === 1 && !e.ctrlKey && !e.altKey);
  131. }
  132. function identifiedTouch(touchList, id) {
  133. var i, l;
  134. if (touchList.identifiedTouch) {
  135. return touchList.identifiedTouch(id);
  136. }
  137. // touchList.identifiedTouch() does not exist in
  138. // webkit yet… we must do the search ourselves...
  139. i = -1;
  140. l = touchList.length;
  141. while (++i < l) {
  142. if (touchList[i].identifier === id) {
  143. return touchList[i];
  144. }
  145. }
  146. }
  147. function changedTouch(e, event) {
  148. var touch = identifiedTouch(e.changedTouches, event.identifier);
  149. // This isn't the touch you're looking for.
  150. if (!touch) { return; }
  151. // Chrome Android (at least) includes touches that have not
  152. // changed in e.changedTouches. That's a bit annoying. Check
  153. // that this touch has changed.
  154. if (touch.pageX === event.pageX && touch.pageY === event.pageY) { return; }
  155. return touch;
  156. }
  157. // Handlers that decide when the first movestart is triggered
  158. function mousedown(e){
  159. var data;
  160. if (!isLeftButton(e)) { return; }
  161. data = {
  162. target: e.target,
  163. startX: e.pageX,
  164. startY: e.pageY,
  165. timeStamp: e.timeStamp
  166. };
  167. add(document, mouseevents.move, mousemove, data);
  168. add(document, mouseevents.cancel, mouseend, data);
  169. }
  170. function mousemove(e){
  171. var data = e.data;
  172. checkThreshold(e, data, e, removeMouse);
  173. }
  174. function mouseend(e) {
  175. removeMouse();
  176. }
  177. function removeMouse() {
  178. remove(document, mouseevents.move, mousemove);
  179. remove(document, mouseevents.cancel, removeMouse);
  180. }
  181. function touchstart(e) {
  182. var touch, template;
  183. // Don't get in the way of interaction with form elements.
  184. if (ignoreTags[ e.target.tagName.toLowerCase() ]) { return; }
  185. touch = e.changedTouches[0];
  186. // iOS live updates the touch objects whereas Android gives us copies.
  187. // That means we can't trust the touchstart object to stay the same,
  188. // so we must copy the data. This object acts as a template for
  189. // movestart, move and moveend event objects.
  190. template = {
  191. target: touch.target,
  192. startX: touch.pageX,
  193. startY: touch.pageY,
  194. timeStamp: e.timeStamp,
  195. identifier: touch.identifier
  196. };
  197. // Use the touch identifier as a namespace, so that we can later
  198. // remove handlers pertaining only to this touch.
  199. add(document, touchevents.move + '.' + touch.identifier, touchmove, template);
  200. add(document, touchevents.cancel + '.' + touch.identifier, touchend, template);
  201. }
  202. function touchmove(e){
  203. var data = e.data,
  204. touch = changedTouch(e, data);
  205. if (!touch) { return; }
  206. checkThreshold(e, data, touch, removeTouch);
  207. }
  208. function touchend(e) {
  209. var template = e.data,
  210. touch = identifiedTouch(e.changedTouches, template.identifier);
  211. if (!touch) { return; }
  212. removeTouch(template.identifier);
  213. }
  214. function removeTouch(identifier) {
  215. remove(document, '.' + identifier, touchmove);
  216. remove(document, '.' + identifier, touchend);
  217. }
  218. // Logic for deciding when to trigger a movestart.
  219. function checkThreshold(e, template, touch, fn) {
  220. var distX = touch.pageX - template.startX,
  221. distY = touch.pageY - template.startY;
  222. // Do nothing if the threshold has not been crossed.
  223. if ((distX * distX) + (distY * distY) < (threshold * threshold)) { return; }
  224. triggerStart(e, template, touch, distX, distY, fn);
  225. }
  226. function handled() {
  227. // this._handled should return false once, and after return true.
  228. this._handled = returnTrue;
  229. return false;
  230. }
  231. function flagAsHandled(e) {
  232. e._handled();
  233. }
  234. function triggerStart(e, template, touch, distX, distY, fn) {
  235. var node = template.target,
  236. touches, time;
  237. touches = e.targetTouches;
  238. time = e.timeStamp - template.timeStamp;
  239. // Create a movestart object with some special properties that
  240. // are passed only to the movestart handlers.
  241. template.type = 'movestart';
  242. template.distX = distX;
  243. template.distY = distY;
  244. template.deltaX = distX;
  245. template.deltaY = distY;
  246. template.pageX = touch.pageX;
  247. template.pageY = touch.pageY;
  248. template.velocityX = distX / time;
  249. template.velocityY = distY / time;
  250. template.targetTouches = touches;
  251. template.finger = touches ?
  252. touches.length :
  253. 1 ;
  254. // The _handled method is fired to tell the default movestart
  255. // handler that one of the move events is bound.
  256. template._handled = handled;
  257. // Pass the touchmove event so it can be prevented if or when
  258. // movestart is handled.
  259. template._preventTouchmoveDefault = function() {
  260. e.preventDefault();
  261. };
  262. // Trigger the movestart event.
  263. trigger(template.target, template);
  264. // Unbind handlers that tracked the touch or mouse up till now.
  265. fn(template.identifier);
  266. }
  267. // Handlers that control what happens following a movestart
  268. function activeMousemove(e) {
  269. var event = e.data.event,
  270. timer = e.data.timer;
  271. updateEvent(event, e, e.timeStamp, timer);
  272. }
  273. function activeMouseend(e) {
  274. var event = e.data.event,
  275. timer = e.data.timer;
  276. removeActiveMouse();
  277. endEvent(event, timer, function() {
  278. // Unbind the click suppressor, waiting until after mouseup
  279. // has been handled.
  280. setTimeout(function(){
  281. remove(event.target, 'click', returnFalse);
  282. }, 0);
  283. });
  284. }
  285. function removeActiveMouse(event) {
  286. remove(document, mouseevents.move, activeMousemove);
  287. remove(document, mouseevents.end, activeMouseend);
  288. }
  289. function activeTouchmove(e) {
  290. var event = e.data.event,
  291. timer = e.data.timer,
  292. touch = changedTouch(e, event);
  293. if (!touch) { return; }
  294. // Stop the interface from gesturing
  295. e.preventDefault();
  296. event.targetTouches = e.targetTouches;
  297. updateEvent(event, touch, e.timeStamp, timer);
  298. }
  299. function activeTouchend(e) {
  300. var event = e.data.event,
  301. timer = e.data.timer,
  302. touch = identifiedTouch(e.changedTouches, event.identifier);
  303. // This isn't the touch you're looking for.
  304. if (!touch) { return; }
  305. removeActiveTouch(event);
  306. endEvent(event, timer);
  307. }
  308. function removeActiveTouch(event) {
  309. remove(document, '.' + event.identifier, activeTouchmove);
  310. remove(document, '.' + event.identifier, activeTouchend);
  311. }
  312. // Logic for triggering move and moveend events
  313. function updateEvent(event, touch, timeStamp, timer) {
  314. var time = timeStamp - event.timeStamp;
  315. event.type = 'move';
  316. event.distX = touch.pageX - event.startX;
  317. event.distY = touch.pageY - event.startY;
  318. event.deltaX = touch.pageX - event.pageX;
  319. event.deltaY = touch.pageY - event.pageY;
  320. // Average the velocity of the last few events using a decay
  321. // curve to even out spurious jumps in values.
  322. event.velocityX = 0.3 * event.velocityX + 0.7 * event.deltaX / time;
  323. event.velocityY = 0.3 * event.velocityY + 0.7 * event.deltaY / time;
  324. event.pageX = touch.pageX;
  325. event.pageY = touch.pageY;
  326. timer.kick();
  327. }
  328. function endEvent(event, timer, fn) {
  329. timer.end(function(){
  330. event.type = 'moveend';
  331. trigger(event.target, event);
  332. return fn && fn();
  333. });
  334. }
  335. // jQuery special event definition
  336. function setup(data, namespaces, eventHandle) {
  337. // Stop the node from being dragged
  338. //add(this, 'dragstart.move drag.move', preventDefault);
  339. // Prevent text selection and touch interface scrolling
  340. //add(this, 'mousedown.move', preventIgnoreTags);
  341. // Tell movestart default handler that we've handled this
  342. add(this, 'movestart.move', flagAsHandled);
  343. // Don't bind to the DOM. For speed.
  344. return true;
  345. }
  346. function teardown(namespaces) {
  347. remove(this, 'dragstart drag', preventDefault);
  348. remove(this, 'mousedown touchstart', preventIgnoreTags);
  349. remove(this, 'movestart', flagAsHandled);
  350. // Don't bind to the DOM. For speed.
  351. return true;
  352. }
  353. function addMethod(handleObj) {
  354. // We're not interested in preventing defaults for handlers that
  355. // come from internal move or moveend bindings
  356. if (handleObj.namespace === "move" || handleObj.namespace === "moveend") {
  357. return;
  358. }
  359. // Stop the node from being dragged
  360. add(this, 'dragstart.' + handleObj.guid + ' drag.' + handleObj.guid, preventDefault, undefined, handleObj.selector);
  361. // Prevent text selection and touch interface scrolling
  362. add(this, 'mousedown.' + handleObj.guid, preventIgnoreTags, undefined, handleObj.selector);
  363. }
  364. function removeMethod(handleObj) {
  365. if (handleObj.namespace === "move" || handleObj.namespace === "moveend") {
  366. return;
  367. }
  368. remove(this, 'dragstart.' + handleObj.guid + ' drag.' + handleObj.guid);
  369. remove(this, 'mousedown.' + handleObj.guid);
  370. }
  371. jQuery.event.special.movestart = {
  372. setup: setup,
  373. teardown: teardown,
  374. add: addMethod,
  375. remove: removeMethod,
  376. _default: function(e) {
  377. var template, data;
  378. // If no move events were bound to any ancestors of this
  379. // target, high tail it out of here.
  380. if (!e._handled()) { return; }
  381. template = {
  382. target: e.target,
  383. startX: e.startX,
  384. startY: e.startY,
  385. pageX: e.pageX,
  386. pageY: e.pageY,
  387. distX: e.distX,
  388. distY: e.distY,
  389. deltaX: e.deltaX,
  390. deltaY: e.deltaY,
  391. velocityX: e.velocityX,
  392. velocityY: e.velocityY,
  393. timeStamp: e.timeStamp,
  394. identifier: e.identifier,
  395. targetTouches: e.targetTouches,
  396. finger: e.finger
  397. };
  398. data = {
  399. event: template,
  400. timer: new Timer(function(time){
  401. trigger(e.target, template);
  402. })
  403. };
  404. if (e.identifier === undefined) {
  405. // We're dealing with a mouse
  406. // Stop clicks from propagating during a move
  407. add(e.target, 'click', returnFalse);
  408. add(document, mouseevents.move, activeMousemove, data);
  409. add(document, mouseevents.end, activeMouseend, data);
  410. }
  411. else {
  412. // We're dealing with a touch. Stop touchmove doing
  413. // anything defaulty.
  414. e._preventTouchmoveDefault();
  415. add(document, touchevents.move + '.' + e.identifier, activeTouchmove, data);
  416. add(document, touchevents.end + '.' + e.identifier, activeTouchend, data);
  417. }
  418. }
  419. };
  420. jQuery.event.special.move = {
  421. setup: function() {
  422. // Bind a noop to movestart. Why? It's the movestart
  423. // setup that decides whether other move events are fired.
  424. add(this, 'movestart.move', jQuery.noop);
  425. },
  426. teardown: function() {
  427. remove(this, 'movestart.move', jQuery.noop);
  428. }
  429. };
  430. jQuery.event.special.moveend = {
  431. setup: function() {
  432. // Bind a noop to movestart. Why? It's the movestart
  433. // setup that decides whether other move events are fired.
  434. add(this, 'movestart.moveend', jQuery.noop);
  435. },
  436. teardown: function() {
  437. remove(this, 'movestart.moveend', jQuery.noop);
  438. }
  439. };
  440. add(document, 'mousedown.move', mousedown);
  441. add(document, 'touchstart.move', touchstart);
  442. // Make jQuery copy touch event properties over to the jQuery event
  443. // object, if they are not already listed. But only do the ones we
  444. // really need. IE7/8 do not have Array#indexOf(), but nor do they
  445. // have touch events, so let's assume we can ignore them.
  446. if (typeof Array.prototype.indexOf === 'function') {
  447. (function(jQuery, undefined){
  448. var props = ["changedTouches", "targetTouches"],
  449. l = props.length;
  450. while (l--) {
  451. if (jQuery.event.props.indexOf(props[l]) === -1) {
  452. jQuery.event.props.push(props[l]);
  453. }
  454. }
  455. })(jQuery);
  456. };
  457. });
  458. // jQuery.event.swipe
  459. // 0.5
  460. // Stephen Band
  461. // Dependencies
  462. // jQuery.event.move 1.2
  463. // One of swipeleft, swiperight, swipeup or swipedown is triggered on
  464. // moveend, when the move has covered a threshold ratio of the dimension
  465. // of the target node, or has gone really fast. Threshold and velocity
  466. // sensitivity changed with:
  467. //
  468. // jQuery.event.special.swipe.settings.threshold
  469. // jQuery.event.special.swipe.settings.sensitivity
  470. (function (module) {
  471. if (typeof define === 'function' && define.amd) {
  472. // AMD. Register as an anonymous module.
  473. define(['jquery'], module);
  474. } else {
  475. // Browser globals
  476. module(jQuery);
  477. }
  478. })(function(jQuery, undefined){
  479. var add = jQuery.event.add,
  480. remove = jQuery.event.remove,
  481. // Just sugar, so we can have arguments in the same order as
  482. // add and remove.
  483. trigger = function(node, type, data) {
  484. jQuery.event.trigger(type, data, node);
  485. },
  486. settings = {
  487. // Ratio of distance over target finger must travel to be
  488. // considered a swipe.
  489. threshold: 0.2,
  490. // Faster fingers can travel shorter distances to be considered
  491. // swipes. 'sensitivity' controls how much. Bigger is shorter.
  492. sensitivity: 10
  493. };
  494. function moveend(e) {
  495. var w, h, event;
  496. w = e.target.offsetWidth;
  497. h = e.target.offsetHeight;
  498. // Copy over some useful properties from the move event
  499. event = {
  500. distX: e.distX,
  501. distY: e.distY,
  502. velocityX: e.velocityX,
  503. velocityY: e.velocityY,
  504. finger: e.finger
  505. };
  506. // Find out which of the four directions was swiped
  507. if (e.distX > e.distY) {
  508. if (e.distX > -e.distY) {
  509. if (e.distX/w > settings.threshold || e.velocityX * e.distX/w * settings.sensitivity > 1) {
  510. event.type = 'swiperight';
  511. trigger(e.currentTarget, event);
  512. }
  513. }
  514. else {
  515. if (-e.distY/h > settings.threshold || e.velocityY * e.distY/w * settings.sensitivity > 1) {
  516. event.type = 'swipeup';
  517. trigger(e.currentTarget, event);
  518. }
  519. }
  520. }
  521. else {
  522. if (e.distX > -e.distY) {
  523. if (e.distY/h > settings.threshold || e.velocityY * e.distY/w * settings.sensitivity > 1) {
  524. event.type = 'swipedown';
  525. trigger(e.currentTarget, event);
  526. }
  527. }
  528. else {
  529. if (-e.distX/w > settings.threshold || e.velocityX * e.distX/w * settings.sensitivity > 1) {
  530. event.type = 'swipeleft';
  531. trigger(e.currentTarget, event);
  532. }
  533. }
  534. }
  535. }
  536. function getData(node) {
  537. var data = jQuery.data(node, 'event_swipe');
  538. if (!data) {
  539. data = { count: 0 };
  540. jQuery.data(node, 'event_swipe', data);
  541. }
  542. return data;
  543. }
  544. jQuery.event.special.swipe =
  545. jQuery.event.special.swipeleft =
  546. jQuery.event.special.swiperight =
  547. jQuery.event.special.swipeup =
  548. jQuery.event.special.swipedown = {
  549. setup: function( data, namespaces, eventHandle ) {
  550. var data = getData(this);
  551. // If another swipe event is already setup, don't setup again.
  552. if (data.count++ > 0) { return; }
  553. add(this, 'moveend', moveend);
  554. return true;
  555. },
  556. teardown: function() {
  557. var data = getData(this);
  558. // If another swipe event is still setup, don't teardown.
  559. if (--data.count > 0) { return; }
  560. remove(this, 'moveend', moveend);
  561. return true;
  562. },
  563. settings: settings
  564. };
  565. });