simulate.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. (function($) {
  2. /* General Utils
  3. ----------------------------------------------------------------------------------------------------------------------*/
  4. $.simulateByPoint = function(type, options) {
  5. var docEl = $(document);
  6. var point = options.point;
  7. var clientX, clientY;
  8. var node;
  9. if (point) {
  10. clientX = point.left - docEl.scrollLeft();
  11. clientY = point.top - docEl.scrollTop();
  12. node = document.elementFromPoint(clientX, clientY);
  13. $(node).simulate(type, options);
  14. }
  15. };
  16. /* Touch
  17. ----------------------------------------------------------------------------------------------------------------------*/
  18. var origSimulateEvent = $.simulate.prototype.simulateEvent;
  19. var touchUID = Date.now();
  20. $.simulate.prototype.simulateEvent = function(elem, type, options) {
  21. if (/^touch/.test(type)) {
  22. return this.simulateTouchEvent(elem, type, options);
  23. }
  24. else {
  25. return origSimulateEvent.apply(this, arguments);
  26. }
  27. };
  28. $.simulate.prototype.simulateTouchEvent = function(elem, type, options) {
  29. // http://stackoverflow.com/a/29019278/96342
  30. var event = document.createEvent('Event');
  31. event.initEvent(type, true, true); // cancelable, bubbleable
  32. event.touches = [{
  33. target: elem,
  34. identifier: touchUID++,
  35. pageX: options.clientX,
  36. pageY: options.clientY,
  37. screenX: options.clientX,
  38. screenY: options.clientY,
  39. clientX: options.clientX,
  40. clientY: options.clientY
  41. }];
  42. this.dispatchEvent(elem, type, event, options);
  43. };
  44. $.simulateMouseClick = function(elem) {
  45. var $elem = $(elem);
  46. var clientCoords = {
  47. clientX: $elem.offset().left + $elem.outerWidth() / 2,
  48. clientY: $elem.offset().top + $elem.outerHeight() / 2
  49. };
  50. $elem.simulate('mousemove', clientCoords);
  51. $elem.simulate('mousedown', clientCoords);
  52. $elem.simulate('mouseup', clientCoords);
  53. $elem.simulate('click', clientCoords);
  54. };
  55. $.simulateTouchClick = function(elem) {
  56. var $elem = $(elem);
  57. var clientCoords = {
  58. clientX: $elem.offset().left + $elem.outerWidth() / 2,
  59. clientY: $elem.offset().top + $elem.outerHeight() / 2
  60. };
  61. $elem.simulate('touchstart', clientCoords);
  62. $elem.simulate('touchend', clientCoords);
  63. $elem.simulate('mousemove', clientCoords);
  64. $elem.simulate('mousedown', clientCoords);
  65. $elem.simulate('mouseup', clientCoords);
  66. $elem.simulate('click', clientCoords);
  67. };
  68. /* Drag-n-drop
  69. ----------------------------------------------------------------------------------------------------------------------*/
  70. var DEBUG_DELAY = 500;
  71. var DEBUG_MIN_DURATION = 2000;
  72. var DEBUG_MIN_MOVES = 100;
  73. var DRAG_DEFAULTS = {
  74. point: null, // the start point
  75. localPoint: { left: '50%', top: '50%' },
  76. end: null, // can be a point or an el
  77. localEndPoint: { left: '50%', top: '50%' },
  78. dx: 0,
  79. dy: 0,
  80. moves: 5,
  81. duration: 100 // ms
  82. };
  83. var dragStackCnt = 0;
  84. $.simulate.prototype.simulateDrag = function() {
  85. var targetNode = this.target;
  86. var targetEl = $(targetNode);
  87. var options = $.extend({}, DRAG_DEFAULTS, this.options);
  88. var dx = options.dx;
  89. var dy = options.dy;
  90. var duration = options.duration;
  91. var moves = options.moves;
  92. var startPoint;
  93. var endEl;
  94. var endPoint;
  95. var localPoint;
  96. var offset;
  97. // compute start point
  98. if (options.point) {
  99. startPoint = options.point;
  100. }
  101. else {
  102. localPoint = normalizeElPoint(options.localPoint, targetEl);
  103. offset = targetEl.offset();
  104. startPoint = {
  105. left: offset.left + localPoint.left,
  106. top: offset.top + localPoint.top
  107. };
  108. }
  109. // compute end point
  110. if (options.end) {
  111. if (isPoint(options.end)) {
  112. endPoint = options.end;
  113. }
  114. else { // assume options.end is an element
  115. endEl = $(options.end);
  116. localPoint = normalizeElPoint(options.localEndPoint, endEl);
  117. offset = endEl.offset();
  118. endPoint = {
  119. left: offset.left + localPoint.left,
  120. top: offset.top + localPoint.top
  121. };
  122. }
  123. }
  124. if (endPoint) {
  125. dx = endPoint.left - startPoint.left;
  126. dy = endPoint.top - startPoint.top;
  127. }
  128. moves = Math.max(moves, options.debug ? DEBUG_MIN_MOVES : 1);
  129. duration = Math.max(duration, options.debug ? DEBUG_MIN_DURATION : 10);
  130. simulateDrag(
  131. this,
  132. targetNode,
  133. startPoint,
  134. dx,
  135. dy,
  136. moves,
  137. duration,
  138. options
  139. );
  140. };
  141. function simulateDrag(self, targetNode, startPoint, dx, dy, moveCnt, duration, options) {
  142. var debug = options.debug;
  143. var isTouch = options.isTouch;
  144. var docNode = targetNode.ownerDocument;
  145. var docEl = $(docNode);
  146. var waitTime = duration / moveCnt;
  147. var moveIndex = 0;
  148. var clientCoords;
  149. var intervalId;
  150. var dotEl;
  151. var dragId;
  152. if (debug) {
  153. dotEl = $('<div>')
  154. .css({
  155. position: 'absolute',
  156. zIndex: 99999,
  157. border: '5px solid red',
  158. borderRadius: '5px',
  159. margin: '-5px 0 0 -5px'
  160. })
  161. .appendTo('body');
  162. }
  163. function updateCoords() {
  164. var progress = moveIndex / moveCnt;
  165. var left = startPoint.left + dx * progress;
  166. var top = startPoint.top + dy * progress;
  167. clientCoords = {
  168. clientX: left - docEl.scrollLeft(),
  169. clientY: top - docEl.scrollTop()
  170. };
  171. if (debug) {
  172. dotEl.css({ left: left, top: top });
  173. }
  174. }
  175. function startDrag() {
  176. updateCoords();
  177. dragId = ++dragStackCnt;
  178. // simulate a drag-start only if another drag isn't already happening
  179. if (dragStackCnt === 1) {
  180. self.simulateEvent(targetNode, isTouch ? 'touchstart' : 'mousedown', clientCoords);
  181. }
  182. var delay = options.delay || 0;
  183. if (debug) {
  184. delay = Math.max(delay, DEBUG_DELAY);
  185. }
  186. if (delay) {
  187. setTimeout(function() {
  188. startMoving();
  189. }, delay);
  190. }
  191. else {
  192. startMoving();
  193. }
  194. }
  195. function startMoving() {
  196. intervalId = setInterval(tick, waitTime);
  197. }
  198. function tick() { // called one interval after start
  199. moveIndex++;
  200. updateCoords(); // update clientCoords before mousemove
  201. self.simulateEvent(docNode, isTouch ? 'touchmove' : 'mousemove', clientCoords);
  202. if (moveIndex >= moveCnt) {
  203. stopMoving();
  204. }
  205. }
  206. function stopMoving() {
  207. clearInterval(intervalId);
  208. if (debug) {
  209. setTimeout(function() {
  210. dotEl.remove(); // do this before calling stopDrag/callback. don't want dot picked up by elementFromPoint
  211. stopDrag();
  212. }, DEBUG_DELAY);
  213. }
  214. else {
  215. stopDrag();
  216. }
  217. }
  218. function stopDrag() { // progress at 1, coords already up to date at this point
  219. (options.onBeforeRelease || function() {})();
  220. // only simulate a drop if the current drag is still the active one.
  221. // otherwise, this means another drag has begun via onBeforeRelease.
  222. if (dragId === dragStackCnt) {
  223. if ($.contains(docNode, targetNode)) {
  224. self.simulateEvent(targetNode, isTouch ? 'touchend' : 'mouseup', clientCoords);
  225. self.simulateEvent(targetNode, 'click', clientCoords);
  226. }
  227. else {
  228. self.simulateEvent(docNode, isTouch ? 'touchend' : 'mouseup', clientCoords);
  229. }
  230. }
  231. dragStackCnt--;
  232. (options.onRelease || options.callback || function() {})(); // TODO: deprecate "callback" ?
  233. }
  234. startDrag();
  235. }
  236. function normalizeElPoint(point, el) {
  237. var left = point.left;
  238. var top = point.top;
  239. if (/%$/.test(left)) {
  240. left = parseInt(left) / 100 * el.outerWidth();
  241. }
  242. if (/%$/.test(top)) {
  243. top = parseInt(top) / 100 * el.outerHeight();
  244. }
  245. return { left: left, top: top };
  246. }
  247. function isPoint(input) {
  248. return typeof input === 'object' && // `in` operator only works on objects
  249. 'left' in input && 'top' in input;
  250. }
  251. })(jQuery);