lib/amd/src/dragdrop.js

  1. // This file is part of Moodle - http://moodle.org/
  2. //
  3. // Moodle is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. //
  8. // Moodle is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  15. /*
  16. * JavaScript to handle drag operations, including automatic scrolling.
  17. *
  18. * Note: this module is defined statically. It is a singleton. You
  19. * can only have one use of it active at any time. However, you
  20. * can only drag one thing at a time, this is not a problem in practice.
  21. *
  22. * @module core/dragdrop
  23. * @copyright 2016 The Open University
  24. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25. * @since 3.6
  26. */
  27. define(['jquery', 'core/autoscroll'], function($, autoScroll) {
  28. var dragdrop = {
  29. /**
  30. * A boolean or options argument depending on whether browser supports passive events.
  31. * @private
  32. */
  33. eventCaptureOptions: {passive: false, capture: true},
  34. /**
  35. * Drag proxy if any.
  36. * @private
  37. */
  38. dragProxy: null,
  39. /**
  40. * Function called on move.
  41. * @private
  42. */
  43. onMove: null,
  44. /**
  45. * Function called on drop.
  46. * @private
  47. */
  48. onDrop: null,
  49. /**
  50. * Initial position of proxy at drag start.
  51. */
  52. initialPosition: null,
  53. /**
  54. * Initial page X of cursor at drag start.
  55. */
  56. initialX: null,
  57. /**
  58. * Initial page Y of cursor at drag start.
  59. */
  60. initialY: null,
  61. /**
  62. * If touch event is in progress, this will be the id, otherwise null
  63. */
  64. touching: null,
  65. /**
  66. * Prepares to begin a drag operation - call with a mousedown or touchstart event.
  67. *
  68. * If the returned object has 'start' true, then you can set up a drag proxy, and call
  69. * start. This function will call preventDefault automatically regardless of whether
  70. * starting or not.
  71. *
  72. * @public
  73. * @param {Object} event Event (should be either mousedown or touchstart)
  74. * @return {Object} Object with start (boolean flag) and x, y (only if flag true) values
  75. */
  76. prepare: function(event) {
  77. event.preventDefault();
  78. var start;
  79. if (event.type === 'touchstart') {
  80. // For touch, start if there's at least one touch and we are not currently doing
  81. // a touch event.
  82. start = (dragdrop.touching === null) && event.changedTouches.length > 0;
  83. } else {
  84. // For mousedown, start if it's the left button.
  85. start = event.which === 1;
  86. }
  87. if (start) {
  88. var details = dragdrop.getEventXY(event);
  89. details.start = true;
  90. return details;
  91. } else {
  92. return {start: false};
  93. }
  94. },
  95. /**
  96. * Call to start a drag operation, in response to a mouse down or touch start event.
  97. * Normally call this after calling prepare and receiving start true (you can probably
  98. * skip prepare if only supporting drag not touch).
  99. *
  100. * Note: The caller is responsible for creating a 'drag proxy' which is the
  101. * thing that actually gets dragged. At present, this doesn't really work
  102. * properly unless it is added directly within the body tag.
  103. *
  104. * You also need to ensure that there is CSS so the proxy is absolutely positioned,
  105. * and styled to look like it is floating.
  106. *
  107. * You also need to absolutely position the proxy where you want it to start.
  108. *
  109. * @public
  110. * @param {Object} event Event (should be either mousedown or touchstart)
  111. * @param {jQuery} dragProxy An absolute-positioned element for dragging
  112. * @param {Object} onMove Function that receives X and Y page locations for a move
  113. * @param {Object} onDrop Function that receives X and Y page locations when dropped
  114. */
  115. start: function(event, dragProxy, onMove, onDrop) {
  116. var xy = dragdrop.getEventXY(event);
  117. dragdrop.initialX = xy.x;
  118. dragdrop.initialY = xy.y;
  119. dragdrop.initialPosition = dragProxy.offset();
  120. dragdrop.dragProxy = dragProxy;
  121. dragdrop.onMove = onMove;
  122. dragdrop.onDrop = onDrop;
  123. switch (event.type) {
  124. case 'mousedown':
  125. // Cannot use jQuery 'on' because events need to not be passive.
  126. dragdrop.addEventSpecial('mousemove', dragdrop.mouseMove);
  127. dragdrop.addEventSpecial('mouseup', dragdrop.mouseUp);
  128. break;
  129. case 'touchstart':
  130. dragdrop.addEventSpecial('touchend', dragdrop.touchEnd);
  131. dragdrop.addEventSpecial('touchcancel', dragdrop.touchEnd);
  132. dragdrop.addEventSpecial('touchmove', dragdrop.touchMove);
  133. dragdrop.touching = event.changedTouches[0].identifier;
  134. break;
  135. default:
  136. throw new Error('Unexpected event type: ' + event.type);
  137. }
  138. autoScroll.start(dragdrop.scroll);
  139. },
  140. /**
  141. * Adds an event listener with special event capture options (capture, not passive). If the
  142. * browser does not support passive events, it will fall back to the boolean for capture.
  143. *
  144. * @private
  145. * @param {Object} event Event type string
  146. * @param {Object} handler Handler function
  147. */
  148. addEventSpecial: function(event, handler) {
  149. try {
  150. window.addEventListener(event, handler, dragdrop.eventCaptureOptions);
  151. } catch (ex) {
  152. dragdrop.eventCaptureOptions = true;
  153. window.addEventListener(event, handler, dragdrop.eventCaptureOptions);
  154. }
  155. },
  156. /**
  157. * Gets X/Y co-ordinates of an event, which can be either touchstart or mousedown.
  158. *
  159. * @private
  160. * @param {Object} event Event (should be either mousedown or touchstart)
  161. * @return {Object} X/Y co-ordinates
  162. */
  163. getEventXY: function(event) {
  164. switch (event.type) {
  165. case 'touchstart':
  166. return {x: event.changedTouches[0].pageX,
  167. y: event.changedTouches[0].pageY};
  168. case 'mousedown':
  169. return {x: event.pageX, y: event.pageY};
  170. default:
  171. throw new Error('Unexpected event type: ' + event.type);
  172. }
  173. },
  174. /**
  175. * Event handler for touch move.
  176. *
  177. * @private
  178. * @param {Object} e Event
  179. */
  180. touchMove: function(e) {
  181. e.preventDefault();
  182. for (var i = 0; i < e.changedTouches.length; i++) {
  183. if (e.changedTouches[i].identifier === dragdrop.touching) {
  184. dragdrop.handleMove(e.changedTouches[i].pageX, e.changedTouches[i].pageY);
  185. }
  186. }
  187. },
  188. /**
  189. * Event handler for mouse move.
  190. *
  191. * @private
  192. * @param {Object} e Event
  193. */
  194. mouseMove: function(e) {
  195. dragdrop.handleMove(e.pageX, e.pageY);
  196. },
  197. /**
  198. * Shared handler for move event (mouse or touch).
  199. *
  200. * @private
  201. * @param {number} pageX X co-ordinate
  202. * @param {number} pageY Y co-ordinate
  203. */
  204. handleMove: function(pageX, pageY) {
  205. // Move the drag proxy, not letting you move it out of screen or window bounds.
  206. var current = dragdrop.dragProxy.offset();
  207. var topOffset = current.top - parseInt(dragdrop.dragProxy.css('top'));
  208. var leftOffset = current.left - parseInt(dragdrop.dragProxy.css('left'));
  209. var maxY = $(document).height() - dragdrop.dragProxy.outerHeight() - topOffset;
  210. var maxX = $(document).width() - dragdrop.dragProxy.outerWidth() - leftOffset;
  211. var minY = -topOffset;
  212. var minX = -leftOffset;
  213. var initial = dragdrop.initialPosition;
  214. var position = {
  215. top: Math.max(minY, Math.min(maxY, initial.top + (pageY - dragdrop.initialY) - topOffset)),
  216. left: Math.max(minX, Math.min(maxX, initial.left + (pageX - dragdrop.initialX) - leftOffset))
  217. };
  218. dragdrop.dragProxy.css(position);
  219. // Trigger move handler.
  220. dragdrop.onMove(pageX, pageY, dragdrop.dragProxy);
  221. },
  222. /**
  223. * Event handler for touch end.
  224. *
  225. * @private
  226. * @param {Object} e Event
  227. */
  228. touchEnd: function(e) {
  229. e.preventDefault();
  230. for (var i = 0; i < e.changedTouches.length; i++) {
  231. if (e.changedTouches[i].identifier === dragdrop.touching) {
  232. dragdrop.handleEnd(e.changedTouches[i].pageX, e.changedTouches[i].pageY);
  233. }
  234. }
  235. },
  236. /**
  237. * Event handler for mouse up.
  238. *
  239. * @private
  240. * @param {Object} e Event
  241. */
  242. mouseUp: function(e) {
  243. dragdrop.handleEnd(e.pageX, e.pageY);
  244. },
  245. /**
  246. * Shared handler for end drag (mouse or touch).
  247. *
  248. * @private
  249. * @param {number} pageX X
  250. * @param {number} pageY Y
  251. */
  252. handleEnd: function(pageX, pageY) {
  253. if (dragdrop.touching !== null) {
  254. window.removeEventListener('touchend', dragdrop.touchEnd, dragdrop.eventCaptureOptions);
  255. window.removeEventListener('touchcancel', dragdrop.touchEnd, dragdrop.eventCaptureOptions);
  256. window.removeEventListener('touchmove', dragdrop.touchMove, dragdrop.eventCaptureOptions);
  257. dragdrop.touching = null;
  258. } else {
  259. window.removeEventListener('mousemove', dragdrop.mouseMove, dragdrop.eventCaptureOptions);
  260. window.removeEventListener('mouseup', dragdrop.mouseUp, dragdrop.eventCaptureOptions);
  261. }
  262. autoScroll.stop();
  263. dragdrop.onDrop(pageX, pageY, dragdrop.dragProxy);
  264. },
  265. /**
  266. * Called when the page scrolls.
  267. *
  268. * @private
  269. * @param {number} offset Amount of scroll
  270. */
  271. scroll: function(offset) {
  272. // Move the proxy to match.
  273. var maxY = $(document).height() - dragdrop.dragProxy.outerHeight();
  274. var currentPosition = dragdrop.dragProxy.offset();
  275. currentPosition.top = Math.min(maxY, currentPosition.top + offset);
  276. dragdrop.dragProxy.css(currentPosition);
  277. }
  278. };
  279. return {
  280. /**
  281. * Prepares to begin a drag operation - call with a mousedown or touchstart event.
  282. *
  283. * If the returned object has 'start' true, then you can set up a drag proxy, and call
  284. * start. This function will call preventDefault automatically regardless of whether
  285. * starting or not.
  286. *
  287. * @param {Object} event Event (should be either mousedown or touchstart)
  288. * @return {Object} Object with start (boolean flag) and x, y (only if flag true) values
  289. */
  290. prepare: dragdrop.prepare,
  291. /**
  292. * Call to start a drag operation, in response to a mouse down or touch start event.
  293. * Normally call this after calling prepare and receiving start true (you can probably
  294. * skip prepare if only supporting drag not touch).
  295. *
  296. * Note: The caller is responsible for creating a 'drag proxy' which is the
  297. * thing that actually gets dragged. At present, this doesn't really work
  298. * properly unless it is added directly within the body tag.
  299. *
  300. * You also need to ensure that there is CSS so the proxy is absolutely positioned,
  301. * and styled to look like it is floating.
  302. *
  303. * You also need to absolutely position the proxy where you want it to start.
  304. *
  305. * @param {Object} event Event (should be either mousedown or touchstart)
  306. * @param {jQuery} dragProxy An absolute-positioned element for dragging
  307. * @param {Object} onMove Function that receives X and Y page locations for a move
  308. * @param {Object} onDrop Function that receives X and Y page locations when dropped
  309. */
  310. start: dragdrop.start
  311. };
  312. });