Merge branch 'MDL-81713-main' of https://github.com/junpataleta/moodle
[moodle.git] / lib / amd / src / dragdrop.js
blob6e050ebd53aab13faee887cd738c032dd975ed12
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.
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.
18  *
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.
22  *
23  * @module     core/dragdrop
24  * @copyright  2016 The Open University
25  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  * @since      3.6
27  */
28 define(['jquery', 'core/autoscroll'], function($, autoScroll) {
29     var dragdrop = {
30         /**
31          * A boolean or options argument depending on whether browser supports passive events.
32          * @private
33          */
34         eventCaptureOptions: {passive: false, capture: true},
36         /**
37          * Drag proxy if any.
38          * @private
39          */
40         dragProxy: null,
42         /**
43          * Function called on move.
44          * @private
45          */
46         onMove: null,
48         /**
49          * Function called on drop.
50          * @private
51          */
52         onDrop: null,
54         /**
55          * Initial position of proxy at drag start.
56          */
57         initialPosition: null,
59         /**
60          * Initial page X of cursor at drag start.
61          */
62         initialX: null,
64         /**
65          * Initial page Y of cursor at drag start.
66          */
67         initialY: null,
69         /**
70          * If touch event is in progress, this will be the id, otherwise null
71          */
72         touching: null,
74         /**
75          * Prepares to begin a drag operation - call with a mousedown or touchstart event.
76          *
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
79          * starting or not.
80          *
81          * @public
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
84          */
85         prepare: function(event) {
86             event.preventDefault();
87             var start;
88             if (event.type === 'touchstart') {
89                 // For touch, start if there's at least one touch and we are not currently doing
90                 // a touch event.
91                 start = (dragdrop.touching === null) && event.changedTouches.length > 0;
92             } else {
93                 // For mousedown, start if it's the left button.
94                 start = event.which === 1;
95             }
96             if (start) {
97                 var details = dragdrop.getEventXY(event);
98                 details.start = true;
99                 return details;
100             } else {
101                 return {start: false};
102             }
103         },
105         /**
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).
109          *
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.
113          *
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.
116          *
117          * You also need to absolutely position the proxy where you want it to start.
118          *
119          * @public
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
124          */
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) {
135                 case 'mousedown':
136                     // Cannot use jQuery 'on' because events need to not be passive.
137                     dragdrop.addEventSpecial('mousemove', dragdrop.mouseMove);
138                     dragdrop.addEventSpecial('mouseup', dragdrop.mouseUp);
139                     break;
140                 case 'touchstart':
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;
145                     break;
146                 default:
147                     throw new Error('Unexpected event type: ' + event.type);
148             }
149             autoScroll.start(dragdrop.scroll);
150         },
152         /**
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.
155          *
156          * @private
157          * @param {Object} event Event type string
158          * @param {Object} handler Handler function
159          */
160         addEventSpecial: function(event, handler) {
161             try {
162                 window.addEventListener(event, handler, dragdrop.eventCaptureOptions);
163             } catch (ex) {
164                 dragdrop.eventCaptureOptions = true;
165                 window.addEventListener(event, handler, dragdrop.eventCaptureOptions);
166             }
167         },
169         /**
170          * Gets X/Y co-ordinates of an event, which can be either touchstart or mousedown.
171          *
172          * @private
173          * @param {Object} event Event (should be either mousedown or touchstart)
174          * @return {Object} X/Y co-ordinates
175          */
176         getEventXY: function(event) {
177             switch (event.type) {
178                 case 'touchstart':
179                     return {x: event.changedTouches[0].pageX,
180                             y: event.changedTouches[0].pageY};
181                 case 'mousedown':
182                     return {x: event.pageX, y: event.pageY};
183                 default:
184                     throw new Error('Unexpected event type: ' + event.type);
185             }
186         },
188         /**
189          * Event handler for touch move.
190          *
191          * @private
192          * @param {Object} e Event
193          */
194         touchMove: function(e) {
195             e.preventDefault();
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);
199                 }
200             }
201         },
203         /**
204          * Event handler for mouse move.
205          *
206          * @private
207          * @param {Object} e Event
208          */
209         mouseMove: function(e) {
210             dragdrop.handleMove(e.pageX, e.pageY);
211         },
213         /**
214          * Shared handler for move event (mouse or touch).
215          *
216          * @private
217          * @param {number} pageX X co-ordinate
218          * @param {number} pageY Y co-ordinate
219          */
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;
230             var position = {
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))
233             };
234             dragdrop.dragProxy.css(position);
236             // Trigger move handler.
237             dragdrop.onMove(pageX, pageY, dragdrop.dragProxy);
238         },
240         /**
241          * Event handler for touch end.
242          *
243          * @private
244          * @param {Object} e Event
245          */
246         touchEnd: function(e) {
247             e.preventDefault();
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);
251                 }
252             }
253         },
255         /**
256          * Event handler for mouse up.
257          *
258          * @private
259          * @param {Object} e Event
260          */
261         mouseUp: function(e) {
262             dragdrop.handleEnd(e.pageX, e.pageY);
263         },
265         /**
266          * Shared handler for end drag (mouse or touch).
267          *
268          * @private
269          * @param {number} pageX X
270          * @param {number} pageY Y
271          */
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;
278             } else {
279                 window.removeEventListener('mousemove', dragdrop.mouseMove, dragdrop.eventCaptureOptions);
280                 window.removeEventListener('mouseup', dragdrop.mouseUp, dragdrop.eventCaptureOptions);
281             }
282             autoScroll.stop();
283             dragdrop.onDrop(pageX, pageY, dragdrop.dragProxy);
284         },
286         /**
287          * Called when the page scrolls.
288          *
289          * @private
290          * @param {number} offset Amount of scroll
291          */
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);
298         }
299     };
301     return {
302         /**
303          * Prepares to begin a drag operation - call with a mousedown or touchstart event.
304          *
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
307          * starting or not.
308          *
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
311          */
312         prepare: dragdrop.prepare,
314         /**
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).
318          *
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.
322          *
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.
325          *
326          * You also need to absolutely position the proxy where you want it to start.
327          *
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
332          */
333         start: dragdrop.start
334     };