1 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
17 * JavaScript to handle drag operations, including automatic scrolling.
19 * Note: this module is defined statically. It is a singleton. You
20 * can only have one use of it active at any time. However, you
21 * can only drag one thing at a time, this is not a problem in practice.
23 * @module core/dragdrop
24 * @copyright 2016 The Open University
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 define(['jquery', 'core/autoscroll'], function($, autoScroll) {
31 * A boolean or options argument depending on whether browser supports passive events.
34 eventCaptureOptions: {passive: false, capture: true},
43 * Function called on move.
49 * Function called on drop.
55 * Initial position of proxy at drag start.
57 initialPosition: null,
60 * Initial page X of cursor at drag start.
65 * Initial page Y of cursor at drag start.
70 * If touch event is in progress, this will be the id, otherwise null
75 * Prepares to begin a drag operation - call with a mousedown or touchstart event.
77 * If the returned object has 'start' true, then you can set up a drag proxy, and call
78 * start. This function will call preventDefault automatically regardless of whether
82 * @param {Object} event Event (should be either mousedown or touchstart)
83 * @return {Object} Object with start (boolean flag) and x, y (only if flag true) values
85 prepare: function(event) {
86 event.preventDefault();
88 if (event.type === 'touchstart') {
89 // For touch, start if there's at least one touch and we are not currently doing
91 start = (dragdrop.touching === null) && event.changedTouches.length > 0;
93 // For mousedown, start if it's the left button.
94 start = event.which === 1;
97 var details = dragdrop.getEventXY(event);
101 return {start: false};
106 * Call to start a drag operation, in response to a mouse down or touch start event.
107 * Normally call this after calling prepare and receiving start true (you can probably
108 * skip prepare if only supporting drag not touch).
110 * Note: The caller is responsible for creating a 'drag proxy' which is the
111 * thing that actually gets dragged. At present, this doesn't really work
112 * properly unless it is added directly within the body tag.
114 * You also need to ensure that there is CSS so the proxy is absolutely positioned,
115 * and styled to look like it is floating.
117 * You also need to absolutely position the proxy where you want it to start.
120 * @param {Object} event Event (should be either mousedown or touchstart)
121 * @param {jQuery} dragProxy An absolute-positioned element for dragging
122 * @param {Object} onMove Function that receives X and Y page locations for a move
123 * @param {Object} onDrop Function that receives X and Y page locations when dropped
125 start: function(event, dragProxy, onMove, onDrop) {
126 var xy = dragdrop.getEventXY(event);
127 dragdrop.initialX = xy.x;
128 dragdrop.initialY = xy.y;
129 dragdrop.initialPosition = dragProxy.offset();
130 dragdrop.dragProxy = dragProxy;
131 dragdrop.onMove = onMove;
132 dragdrop.onDrop = onDrop;
134 switch (event.type) {
136 // Cannot use jQuery 'on' because events need to not be passive.
137 dragdrop.addEventSpecial('mousemove', dragdrop.mouseMove);
138 dragdrop.addEventSpecial('mouseup', dragdrop.mouseUp);
141 dragdrop.addEventSpecial('touchend', dragdrop.touchEnd);
142 dragdrop.addEventSpecial('touchcancel', dragdrop.touchEnd);
143 dragdrop.addEventSpecial('touchmove', dragdrop.touchMove);
144 dragdrop.touching = event.changedTouches[0].identifier;
147 throw new Error('Unexpected event type: ' + event.type);
149 autoScroll.start(dragdrop.scroll);
153 * Adds an event listener with special event capture options (capture, not passive). If the
154 * browser does not support passive events, it will fall back to the boolean for capture.
157 * @param {Object} event Event type string
158 * @param {Object} handler Handler function
160 addEventSpecial: function(event, handler) {
162 window.addEventListener(event, handler, dragdrop.eventCaptureOptions);
164 dragdrop.eventCaptureOptions = true;
165 window.addEventListener(event, handler, dragdrop.eventCaptureOptions);
170 * Gets X/Y co-ordinates of an event, which can be either touchstart or mousedown.
173 * @param {Object} event Event (should be either mousedown or touchstart)
174 * @return {Object} X/Y co-ordinates
176 getEventXY: function(event) {
177 switch (event.type) {
179 return {x: event.changedTouches[0].pageX,
180 y: event.changedTouches[0].pageY};
182 return {x: event.pageX, y: event.pageY};
184 throw new Error('Unexpected event type: ' + event.type);
189 * Event handler for touch move.
192 * @param {Object} e Event
194 touchMove: function(e) {
196 for (var i = 0; i < e.changedTouches.length; i++) {
197 if (e.changedTouches[i].identifier === dragdrop.touching) {
198 dragdrop.handleMove(e.changedTouches[i].pageX, e.changedTouches[i].pageY);
204 * Event handler for mouse move.
207 * @param {Object} e Event
209 mouseMove: function(e) {
210 dragdrop.handleMove(e.pageX, e.pageY);
214 * Shared handler for move event (mouse or touch).
217 * @param {number} pageX X co-ordinate
218 * @param {number} pageY Y co-ordinate
220 handleMove: function(pageX, pageY) {
221 // Move the drag proxy, not letting you move it out of screen or window bounds.
222 var current = dragdrop.dragProxy.offset();
223 var topOffset = current.top - parseInt(dragdrop.dragProxy.css('top'));
224 var leftOffset = current.left - parseInt(dragdrop.dragProxy.css('left'));
225 var maxY = $(document).height() - dragdrop.dragProxy.outerHeight() - topOffset;
226 var maxX = $(document).width() - dragdrop.dragProxy.outerWidth() - leftOffset;
227 var minY = -topOffset;
228 var minX = -leftOffset;
229 var initial = dragdrop.initialPosition;
231 top: Math.max(minY, Math.min(maxY, initial.top + (pageY - dragdrop.initialY) - topOffset)),
232 left: Math.max(minX, Math.min(maxX, initial.left + (pageX - dragdrop.initialX) - leftOffset))
234 dragdrop.dragProxy.css(position);
236 // Trigger move handler.
237 dragdrop.onMove(pageX, pageY, dragdrop.dragProxy);
241 * Event handler for touch end.
244 * @param {Object} e Event
246 touchEnd: function(e) {
248 for (var i = 0; i < e.changedTouches.length; i++) {
249 if (e.changedTouches[i].identifier === dragdrop.touching) {
250 dragdrop.handleEnd(e.changedTouches[i].pageX, e.changedTouches[i].pageY);
256 * Event handler for mouse up.
259 * @param {Object} e Event
261 mouseUp: function(e) {
262 dragdrop.handleEnd(e.pageX, e.pageY);
266 * Shared handler for end drag (mouse or touch).
269 * @param {number} pageX X
270 * @param {number} pageY Y
272 handleEnd: function(pageX, pageY) {
273 if (dragdrop.touching !== null) {
274 window.removeEventListener('touchend', dragdrop.touchEnd, dragdrop.eventCaptureOptions);
275 window.removeEventListener('touchcancel', dragdrop.touchEnd, dragdrop.eventCaptureOptions);
276 window.removeEventListener('touchmove', dragdrop.touchMove, dragdrop.eventCaptureOptions);
277 dragdrop.touching = null;
279 window.removeEventListener('mousemove', dragdrop.mouseMove, dragdrop.eventCaptureOptions);
280 window.removeEventListener('mouseup', dragdrop.mouseUp, dragdrop.eventCaptureOptions);
283 dragdrop.onDrop(pageX, pageY, dragdrop.dragProxy);
287 * Called when the page scrolls.
290 * @param {number} offset Amount of scroll
292 scroll: function(offset) {
293 // Move the proxy to match.
294 var maxY = $(document).height() - dragdrop.dragProxy.outerHeight();
295 var currentPosition = dragdrop.dragProxy.offset();
296 currentPosition.top = Math.min(maxY, currentPosition.top + offset);
297 dragdrop.dragProxy.css(currentPosition);
303 * Prepares to begin a drag operation - call with a mousedown or touchstart event.
305 * If the returned object has 'start' true, then you can set up a drag proxy, and call
306 * start. This function will call preventDefault automatically regardless of whether
309 * @param {Object} event Event (should be either mousedown or touchstart)
310 * @return {Object} Object with start (boolean flag) and x, y (only if flag true) values
312 prepare: dragdrop.prepare,
315 * Call to start a drag operation, in response to a mouse down or touch start event.
316 * Normally call this after calling prepare and receiving start true (you can probably
317 * skip prepare if only supporting drag not touch).
319 * Note: The caller is responsible for creating a 'drag proxy' which is the
320 * thing that actually gets dragged. At present, this doesn't really work
321 * properly unless it is added directly within the body tag.
323 * You also need to ensure that there is CSS so the proxy is absolutely positioned,
324 * and styled to look like it is floating.
326 * You also need to absolutely position the proxy where you want it to start.
328 * @param {Object} event Event (should be either mousedown or touchstart)
329 * @param {jQuery} dragProxy An absolute-positioned element for dragging
330 * @param {Object} onMove Function that receives X and Y page locations for a move
331 * @param {Object} onDrop Function that receives X and Y page locations when dropped
333 start: dragdrop.start