simulate.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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. /* Drag-n-drop
  45. ----------------------------------------------------------------------------------------------------------------------*/
  46. var DEBUG_DELAY = 500;
  47. var DEBUG_MIN_DURATION = 2000;
  48. var DEBUG_MIN_MOVES = 100;
  49. var DRAG_DEFAULTS = {
  50. point: null, // the start point
  51. localPoint: { left: '50%', top: '50%' },
  52. end: null, // can be a point or an el
  53. localEndPoint: { left: '50%', top: '50%' },
  54. dx: 0,
  55. dy: 0,
  56. moves: 5,
  57. duration: 100 // ms
  58. };
  59. var dragStackCnt = 0;
  60. $.simulate.prototype.simulateDrag = function() {
  61. var targetNode = this.target;
  62. var targetEl = $(targetNode);
  63. var options = $.extend({}, DRAG_DEFAULTS, this.options);
  64. var dx = options.dx;
  65. var dy = options.dy;
  66. var duration = options.duration;
  67. var moves = options.moves;
  68. var startPoint;
  69. var endEl;
  70. var endPoint;
  71. var localPoint;
  72. var offset;
  73. // compute start point
  74. if (options.point) {
  75. startPoint = options.point;
  76. }
  77. else {
  78. localPoint = normalizeElPoint(options.localPoint, targetEl);
  79. offset = targetEl.offset();
  80. startPoint = {
  81. left: offset.left + localPoint.left,
  82. top: offset.top + localPoint.top
  83. };
  84. }
  85. // compute end point
  86. if (options.end) {
  87. if (isPoint(options.end)) {
  88. endPoint = options.end;
  89. }
  90. else { // assume options.end is an element
  91. endEl = $(options.end);
  92. localPoint = normalizeElPoint(options.localEndPoint, endEl);
  93. offset = endEl.offset();
  94. endPoint = {
  95. left: offset.left + localPoint.left,
  96. top: offset.top + localPoint.top
  97. };
  98. }
  99. }
  100. if (endPoint) {
  101. dx = endPoint.left - startPoint.left;
  102. dy = endPoint.top - startPoint.top;
  103. }
  104. moves = Math.max(moves, options.debug ? DEBUG_MIN_MOVES : 1);
  105. duration = Math.max(duration, options.debug ? DEBUG_MIN_DURATION : 10);
  106. simulateDrag(
  107. this,
  108. targetNode,
  109. startPoint,
  110. dx,
  111. dy,
  112. moves,
  113. duration,
  114. options
  115. );
  116. };
  117. function simulateDrag(self, targetNode, startPoint, dx, dy, moveCnt, duration, options) {
  118. var debug = options.debug;
  119. var isTouch = options.isTouch;
  120. var docNode = targetNode.ownerDocument;
  121. var docEl = $(docNode);
  122. var waitTime = duration / moveCnt;
  123. var moveIndex = 0;
  124. var clientCoords;
  125. var intervalId;
  126. var dotEl;
  127. var dragId;
  128. if (debug) {
  129. dotEl = $('<div>')
  130. .css({
  131. position: 'absolute',
  132. zIndex: 99999,
  133. border: '5px solid red',
  134. borderRadius: '5px',
  135. margin: '-5px 0 0 -5px'
  136. })
  137. .appendTo('body');
  138. }
  139. function updateCoords() {
  140. var progress = moveIndex / moveCnt;
  141. var left = startPoint.left + dx * progress;
  142. var top = startPoint.top + dy * progress;
  143. clientCoords = {
  144. clientX: left - docEl.scrollLeft(),
  145. clientY: top - docEl.scrollTop()
  146. };
  147. if (debug) {
  148. dotEl.css({ left: left, top: top });
  149. }
  150. }
  151. function startDrag() {
  152. updateCoords();
  153. dragId = ++dragStackCnt;
  154. // simulate a drag-start only if another drag isn't already happening
  155. if (dragStackCnt === 1) {
  156. self.simulateEvent(targetNode, isTouch ? 'touchstart' : 'mousedown', clientCoords);
  157. }
  158. var delay = options.delay || 0;
  159. if (debug) {
  160. delay = Math.max(delay, DEBUG_DELAY);
  161. }
  162. if (delay) {
  163. setTimeout(function() {
  164. startMoving();
  165. }, delay);
  166. }
  167. else {
  168. startMoving();
  169. }
  170. }
  171. function startMoving() {
  172. intervalId = setInterval(tick, waitTime);
  173. }
  174. function tick() { // called one interval after start
  175. moveIndex++;
  176. updateCoords(); // update clientCoords before mousemove
  177. self.simulateEvent(docNode, isTouch ? 'touchmove' : 'mousemove', clientCoords);
  178. if (moveIndex >= moveCnt) {
  179. stopMoving();
  180. }
  181. }
  182. function stopMoving() {
  183. clearInterval(intervalId);
  184. if (debug) {
  185. setTimeout(function() {
  186. dotEl.remove(); // do this before calling stopDrag/callback. don't want dot picked up by elementFromPoint
  187. stopDrag();
  188. }, DEBUG_DELAY);
  189. }
  190. else {
  191. stopDrag();
  192. }
  193. }
  194. function stopDrag() { // progress at 1, coords already up to date at this point
  195. (options.onBeforeRelease || function() {})();
  196. // only simulate a drop if the current drag is still the active one.
  197. // otherwise, this means another drag has begun via onBeforeRelease.
  198. if (dragId === dragStackCnt) {
  199. if ($.contains(docNode, targetNode)) {
  200. self.simulateEvent(targetNode, isTouch ? 'touchend' : 'mouseup', clientCoords);
  201. self.simulateEvent(targetNode, 'click', clientCoords);
  202. }
  203. else {
  204. self.simulateEvent(docNode, isTouch ? 'touchend' : 'mouseup', clientCoords);
  205. }
  206. }
  207. dragStackCnt--;
  208. (options.onRelease || options.callback || function() {})(); // TODO: deprecate "callback" ?
  209. }
  210. startDrag();
  211. }
  212. function normalizeElPoint(point, el) {
  213. var left = point.left;
  214. var top = point.top;
  215. if (/%$/.test(left)) {
  216. left = parseInt(left) / 100 * el.outerWidth();
  217. }
  218. if (/%$/.test(top)) {
  219. top = parseInt(top) / 100 * el.outerHeight();
  220. }
  221. return { left: left, top: top };
  222. }
  223. function isPoint(input) {
  224. return typeof input === 'object' && // `in` operator only works on objects
  225. 'left' in input && 'top' in input;
  226. }
  227. })(jQuery);