| 1 | efrain | 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 | /*
 | 
        
           |  |  | 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},
 | 
        
           |  |  | 35 |   | 
        
           |  |  | 36 |         /**
 | 
        
           |  |  | 37 |          * Drag proxy if any.
 | 
        
           |  |  | 38 |          * @private
 | 
        
           |  |  | 39 |          */
 | 
        
           |  |  | 40 |         dragProxy: null,
 | 
        
           |  |  | 41 |   | 
        
           |  |  | 42 |         /**
 | 
        
           |  |  | 43 |          * Function called on move.
 | 
        
           |  |  | 44 |          * @private
 | 
        
           |  |  | 45 |          */
 | 
        
           |  |  | 46 |         onMove: null,
 | 
        
           |  |  | 47 |   | 
        
           |  |  | 48 |         /**
 | 
        
           |  |  | 49 |          * Function called on drop.
 | 
        
           |  |  | 50 |          * @private
 | 
        
           |  |  | 51 |          */
 | 
        
           |  |  | 52 |         onDrop: null,
 | 
        
           |  |  | 53 |   | 
        
           |  |  | 54 |         /**
 | 
        
           |  |  | 55 |          * Initial position of proxy at drag start.
 | 
        
           |  |  | 56 |          */
 | 
        
           |  |  | 57 |         initialPosition: null,
 | 
        
           |  |  | 58 |   | 
        
           |  |  | 59 |         /**
 | 
        
           |  |  | 60 |          * Initial page X of cursor at drag start.
 | 
        
           |  |  | 61 |          */
 | 
        
           |  |  | 62 |         initialX: null,
 | 
        
           |  |  | 63 |   | 
        
           |  |  | 64 |         /**
 | 
        
           |  |  | 65 |          * Initial page Y of cursor at drag start.
 | 
        
           |  |  | 66 |          */
 | 
        
           |  |  | 67 |         initialY: null,
 | 
        
           |  |  | 68 |   | 
        
           |  |  | 69 |         /**
 | 
        
           |  |  | 70 |          * If touch event is in progress, this will be the id, otherwise null
 | 
        
           |  |  | 71 |          */
 | 
        
           |  |  | 72 |         touching: null,
 | 
        
           |  |  | 73 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 104 |   | 
        
           |  |  | 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;
 | 
        
           |  |  | 133 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 151 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 168 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 187 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 202 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 212 |   | 
        
           |  |  | 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);
 | 
        
           |  |  | 235 |   | 
        
           |  |  | 236 |             // Trigger move handler.
 | 
        
           |  |  | 237 |             dragdrop.onMove(pageX, pageY, dragdrop.dragProxy);
 | 
        
           |  |  | 238 |         },
 | 
        
           |  |  | 239 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 254 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 264 |   | 
        
           |  |  | 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 |         },
 | 
        
           |  |  | 285 |   | 
        
           |  |  | 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 |     };
 | 
        
           |  |  | 300 |   | 
        
           |  |  | 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,
 | 
        
           |  |  | 313 |   | 
        
           |  |  | 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 |     };
 | 
        
           |  |  | 335 | });
 |