| 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 |  * A javascript module to handle list items drag and drop
 | 
        
           |  |  | 18 |  *
 | 
        
           |  |  | 19 |  * Example of usage:
 | 
        
           |  |  | 20 |  *
 | 
        
           |  |  | 21 |  * Create a list (for example `<ul>` or `<tbody>`) where each draggable element has a drag handle.
 | 
        
           |  |  | 22 |  * The best practice is to use the template core/drag_handle:
 | 
        
           |  |  | 23 |  * $OUTPUT->render_from_template('core/drag_handle', ['movetitle' => get_string('movecontent', 'moodle', ELEMENTNAME)]);
 | 
        
           |  |  | 24 |  *
 | 
        
           |  |  | 25 |  * Attach this JS module to this list:
 | 
        
           |  |  | 26 |  *
 | 
        
           |  |  | 27 |  * Space between define and ( critical in comment but not allowed in code in order to function
 | 
        
           |  |  | 28 |  * correctly with Moodle's requirejs.php
 | 
        
           |  |  | 29 |  *
 | 
        
           |  |  | 30 |  * For the full list of possible parameters see var defaultParameters below.
 | 
        
           |  |  | 31 |  *
 | 
        
           |  |  | 32 |  * The following jQuery events are fired:
 | 
        
           |  |  | 33 |  * - SortableList.EVENTS.DRAGSTART : when user started dragging a list element
 | 
        
           |  |  | 34 |  * - SortableList.EVENTS.DRAG : when user dragged a list element to a new position
 | 
        
           |  |  | 35 |  * - SortableList.EVENTS.DROP : when user dropped a list element
 | 
        
           |  |  | 36 |  * - SortableList.EVENTS.DROPEND : when user finished dragging - either fired right after dropping or
 | 
        
           |  |  | 37 |  *                          if "Esc" was pressed during dragging
 | 
        
           |  |  | 38 |  *
 | 
        
           |  |  | 39 |  * @example
 | 
        
           |  |  | 40 |  * define (['jquery', 'core/sortable_list'], function($, SortableList) {
 | 
        
           |  |  | 41 |  *     var list = new SortableList('ul.my-awesome-list'); // source list (usually <ul> or <tbody>) - selector or element
 | 
        
           |  |  | 42 |  *
 | 
        
           |  |  | 43 |  *     // Listen to the events when element is dragged.
 | 
        
           |  |  | 44 |  *     $('ul.my-awesome-list > *').on(SortableList.EVENTS.DROP, function(evt, info) {
 | 
        
           |  |  | 45 |  *         console.log(info);
 | 
        
           |  |  | 46 |  *     });
 | 
        
           |  |  | 47 |  *
 | 
        
           |  |  | 48 |  *     // Advanced usage. Overwrite methods getElementName, getDestinationName, moveDialogueTitle, for example:
 | 
        
           |  |  | 49 |  *     list.getElementName = function(element) {
 | 
        
           |  |  | 50 |  *         return $.Deferred().resolve(element.attr('data-name'));
 | 
        
           |  |  | 51 |  *     }
 | 
        
           |  |  | 52 |  * });
 | 
        
           |  |  | 53 |  *
 | 
        
           |  |  | 54 |  * @module     core/sortable_list
 | 
        
           |  |  | 55 |  * @class      core/sortable_list
 | 
        
           |  |  | 56 |  * @copyright  2018 Marina Glancy
 | 
        
           |  |  | 57 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 58 |  */
 | 
        
           | 1441 | ariadna | 59 | define([
 | 
        
           |  |  | 60 |     'jquery',
 | 
        
           |  |  | 61 |     'core/log',
 | 
        
           |  |  | 62 |     'core/autoscroll',
 | 
        
           |  |  | 63 |     'core/event_dispatcher',
 | 
        
           |  |  | 64 |     'core/str',
 | 
        
           |  |  | 65 |     'core/modal_cancel',
 | 
        
           |  |  | 66 |     'core/modal_events',
 | 
        
           |  |  | 67 |     'core/notification',
 | 
        
           |  |  | 68 | ], function($, log, autoScroll, EventDispatcher, str, ModalCancel, ModalEvents, Notification) {
 | 
        
           | 1 | efrain | 69 |   | 
        
           |  |  | 70 |     /**
 | 
        
           |  |  | 71 |      * Default parameters
 | 
        
           |  |  | 72 |      *
 | 
        
           |  |  | 73 |      * @private
 | 
        
           |  |  | 74 |      * @type {Object}
 | 
        
           |  |  | 75 |      */
 | 
        
           |  |  | 76 |     var defaultParameters = {
 | 
        
           |  |  | 77 |         targetListSelector: null,
 | 
        
           |  |  | 78 |         moveHandlerSelector: '[data-drag-type=move]',
 | 
        
           |  |  | 79 |         isHorizontal: false,
 | 
        
           |  |  | 80 |         autoScroll: true
 | 
        
           |  |  | 81 |     };
 | 
        
           |  |  | 82 |   | 
        
           |  |  | 83 |     /**
 | 
        
           |  |  | 84 |      * Class names for different elements that may be changed during sorting
 | 
        
           |  |  | 85 |      *
 | 
        
           |  |  | 86 |      * @private
 | 
        
           |  |  | 87 |      * @type {Object}
 | 
        
           |  |  | 88 |      */
 | 
        
           |  |  | 89 |     var CSS = {
 | 
        
           |  |  | 90 |         keyboardDragClass: 'dragdrop-keyboard-drag',
 | 
        
           |  |  | 91 |         isDraggedClass: 'sortable-list-is-dragged',
 | 
        
           |  |  | 92 |         isDroppedClass: 'sortable-list-is-dropped',
 | 
        
           |  |  | 93 |         currentPositionClass: 'sortable-list-current-position',
 | 
        
           |  |  | 94 |         sourceListClass: 'sortable-list-source',
 | 
        
           |  |  | 95 |         targetListClass: 'sortable-list-target',
 | 
        
           |  |  | 96 |         overElementClass: 'sortable-list-over-element'
 | 
        
           |  |  | 97 |     };
 | 
        
           |  |  | 98 |   | 
        
           |  |  | 99 |     /**
 | 
        
           |  |  | 100 |      * Test the browser support for options objects on event listeners.
 | 
        
           |  |  | 101 |      * @return {Boolean}
 | 
        
           |  |  | 102 |      */
 | 
        
           |  |  | 103 |     var eventListenerOptionsSupported = function() {
 | 
        
           |  |  | 104 |         var passivesupported = false,
 | 
        
           |  |  | 105 |             options,
 | 
        
           |  |  | 106 |             testeventname = "testpassiveeventoptions";
 | 
        
           |  |  | 107 |   | 
        
           |  |  | 108 |         // Options support testing example from:
 | 
        
           |  |  | 109 |         // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
 | 
        
           |  |  | 110 |   | 
        
           |  |  | 111 |         try {
 | 
        
           |  |  | 112 |             options = Object.defineProperty({}, "passive", {
 | 
        
           |  |  | 113 |                 // eslint-disable-next-line getter-return
 | 
        
           |  |  | 114 |                 get: function() {
 | 
        
           |  |  | 115 |                     passivesupported = true;
 | 
        
           |  |  | 116 |                 }
 | 
        
           |  |  | 117 |             });
 | 
        
           |  |  | 118 |   | 
        
           |  |  | 119 |             // We use an event name that is not likely to conflict with any real event.
 | 
        
           |  |  | 120 |             document.addEventListener(testeventname, options, options);
 | 
        
           |  |  | 121 |             // We remove the event listener as we have tested the options already.
 | 
        
           |  |  | 122 |             document.removeEventListener(testeventname, options, options);
 | 
        
           |  |  | 123 |         } catch (err) {
 | 
        
           |  |  | 124 |             // It's already false.
 | 
        
           |  |  | 125 |             passivesupported = false;
 | 
        
           |  |  | 126 |         }
 | 
        
           |  |  | 127 |         return passivesupported;
 | 
        
           |  |  | 128 |     };
 | 
        
           |  |  | 129 |   | 
        
           |  |  | 130 |     /**
 | 
        
           |  |  | 131 |      * Allow to create non-passive touchstart listeners and prevent page scrolling when dragging
 | 
        
           |  |  | 132 |      * From: https://stackoverflow.com/a/48098097
 | 
        
           |  |  | 133 |      *
 | 
        
           |  |  | 134 |      * @param {string} eventname
 | 
        
           |  |  | 135 |      * @returns {object}
 | 
        
           |  |  | 136 |      */
 | 
        
           |  |  | 137 |     var registerNotPassiveListeners = function(eventname) {
 | 
        
           |  |  | 138 |         return {
 | 
        
           |  |  | 139 |             setup: function(x, ns, handle) {
 | 
        
           |  |  | 140 |                 if (ns.includes('notPassive')) {
 | 
        
           |  |  | 141 |                     this.addEventListener(eventname, handle, {passive: false});
 | 
        
           |  |  | 142 |                     return true;
 | 
        
           |  |  | 143 |                 } else {
 | 
        
           |  |  | 144 |                     return false;
 | 
        
           |  |  | 145 |                 }
 | 
        
           |  |  | 146 |             }
 | 
        
           |  |  | 147 |         };
 | 
        
           |  |  | 148 |     };
 | 
        
           |  |  | 149 |   | 
        
           |  |  | 150 |     if (eventListenerOptionsSupported) {
 | 
        
           |  |  | 151 |         $.event.special.touchstart = registerNotPassiveListeners('touchstart');
 | 
        
           |  |  | 152 |         $.event.special.touchmove = registerNotPassiveListeners('touchmove');
 | 
        
           |  |  | 153 |         $.event.special.touchend = registerNotPassiveListeners('touchend');
 | 
        
           |  |  | 154 |     }
 | 
        
           |  |  | 155 |   | 
        
           |  |  | 156 |     /**
 | 
        
           |  |  | 157 |      * Initialise sortable list.
 | 
        
           |  |  | 158 |      *
 | 
        
           |  |  | 159 |      * @param {(String|jQuery|Element)} root JQuery/DOM element representing sortable list (i.e. <ul>, <tbody>) or CSS selector
 | 
        
           |  |  | 160 |      * @param {Object} config Parameters for the list. See defaultParameters above for examples.
 | 
        
           |  |  | 161 |      * @param {(String|jQuery|Element)} config.targetListSelector target lists, by default same as root
 | 
        
           |  |  | 162 |      * @param {String} config.moveHandlerSelector  CSS selector for a drag handle. By default '[data-drag-type=move]'
 | 
        
           |  |  | 163 |      * @param {String} config.listSelector   CSS selector for target lists. By default the same as root
 | 
        
           |  |  | 164 |      * @param {(Boolean|Function)} config.isHorizontal Set to true if the list is horizontal (can also be a callback
 | 
        
           |  |  | 165 |      *                                                 with list as an argument)
 | 
        
           |  |  | 166 |      * @param {Boolean} config.autoScroll Engages autoscroll module for automatic vertical scrolling of the whole page,
 | 
        
           |  |  | 167 |      *                                    by default true
 | 
        
           |  |  | 168 |      */
 | 
        
           |  |  | 169 |     var SortableList = function(root, config) {
 | 
        
           |  |  | 170 |   | 
        
           |  |  | 171 |         this.info = null;
 | 
        
           |  |  | 172 |         this.proxy = null;
 | 
        
           |  |  | 173 |         this.proxyDelta = null;
 | 
        
           |  |  | 174 |         this.dragCounter = 0;
 | 
        
           |  |  | 175 |         this.lastEvent = null;
 | 
        
           |  |  | 176 |   | 
        
           |  |  | 177 |         this.config = $.extend({}, defaultParameters, config || {});
 | 
        
           |  |  | 178 |         this.config.listSelector = root;
 | 
        
           |  |  | 179 |         if (!this.config.targetListSelector) {
 | 
        
           |  |  | 180 |             this.config.targetListSelector = root;
 | 
        
           |  |  | 181 |         }
 | 
        
           |  |  | 182 |         if (typeof this.config.listSelector === 'object') {
 | 
        
           |  |  | 183 |             // The root is an element on the page. Register a listener for this element.
 | 
        
           |  |  | 184 |             $(this.config.listSelector).on('mousedown touchstart.notPassive', $.proxy(this.dragStartHandler, this));
 | 
        
           |  |  | 185 |         } else {
 | 
        
           |  |  | 186 |             // The root is a CSS selector. Register a listener that picks up the element dynamically.
 | 
        
           |  |  | 187 |             $('body').on('mousedown touchstart.notPassive', this.config.listSelector, $.proxy(this.dragStartHandler, this));
 | 
        
           |  |  | 188 |         }
 | 
        
           |  |  | 189 |         if (this.config.moveHandlerSelector !== null) {
 | 
        
           |  |  | 190 |             $('body').on('click keypress', this.config.moveHandlerSelector, $.proxy(this.clickHandler, this));
 | 
        
           |  |  | 191 |         }
 | 
        
           |  |  | 192 |   | 
        
           |  |  | 193 |     };
 | 
        
           |  |  | 194 |   | 
        
           |  |  | 195 |     /**
 | 
        
           |  |  | 196 |      * Events fired by this entity
 | 
        
           |  |  | 197 |      *
 | 
        
           |  |  | 198 |      * @public
 | 
        
           |  |  | 199 |      * @type {Object}
 | 
        
           |  |  | 200 |      */
 | 
        
           |  |  | 201 |     SortableList.EVENTS = {
 | 
        
           | 1441 | ariadna | 202 |         // Legacy jQuery events.
 | 
        
           | 1 | efrain | 203 |         DRAGSTART: 'sortablelist-dragstart',
 | 
        
           |  |  | 204 |         DRAG: 'sortablelist-drag',
 | 
        
           |  |  | 205 |         DROP: 'sortablelist-drop',
 | 
        
           | 1441 | ariadna | 206 |         DRAGEND: 'sortablelist-dragend',
 | 
        
           |  |  | 207 |         // Native Javascript events.
 | 
        
           |  |  | 208 |         elementDragStart: 'core/sortable_list:dragStart',
 | 
        
           |  |  | 209 |         elementDrag: 'core/sortable_list:drag',
 | 
        
           |  |  | 210 |         elementDrop: 'core/sortable_list:drop',
 | 
        
           |  |  | 211 |         elementDragEnd: 'core/sortable_list:dragEnd',
 | 
        
           | 1 | efrain | 212 |     };
 | 
        
           |  |  | 213 |   | 
        
           |  |  | 214 |     /**
 | 
        
           |  |  | 215 |      * Resets the temporary classes assigned during dragging
 | 
        
           |  |  | 216 |      * @private
 | 
        
           |  |  | 217 |      */
 | 
        
           |  |  | 218 |      SortableList.prototype.resetDraggedClasses = function() {
 | 
        
           |  |  | 219 |         var classes = [
 | 
        
           |  |  | 220 |             CSS.isDraggedClass,
 | 
        
           |  |  | 221 |             CSS.currentPositionClass,
 | 
        
           |  |  | 222 |             CSS.overElementClass,
 | 
        
           |  |  | 223 |             CSS.targetListClass,
 | 
        
           |  |  | 224 |         ];
 | 
        
           |  |  | 225 |         for (var i in classes) {
 | 
        
           |  |  | 226 |             $('.' + classes[i]).removeClass(classes[i]);
 | 
        
           |  |  | 227 |         }
 | 
        
           |  |  | 228 |         if (this.proxy) {
 | 
        
           |  |  | 229 |             this.proxy.remove();
 | 
        
           |  |  | 230 |             this.proxy = $();
 | 
        
           |  |  | 231 |         }
 | 
        
           |  |  | 232 |     };
 | 
        
           |  |  | 233 |   | 
        
           |  |  | 234 |     /**
 | 
        
           |  |  | 235 |      * Calculates evt.pageX, evt.pageY, evt.clientX and evt.clientY
 | 
        
           |  |  | 236 |      *
 | 
        
           |  |  | 237 |      * For touch events pageX and pageY are taken from the first touch;
 | 
        
           |  |  | 238 |      * For the emulated mousemove event they are taken from the last real event.
 | 
        
           |  |  | 239 |      *
 | 
        
           |  |  | 240 |      * @private
 | 
        
           |  |  | 241 |      * @param {Event} evt
 | 
        
           |  |  | 242 |      */
 | 
        
           |  |  | 243 |     SortableList.prototype.calculatePositionOnPage = function(evt) {
 | 
        
           |  |  | 244 |   | 
        
           |  |  | 245 |         if (evt.originalEvent && evt.originalEvent.touches && evt.originalEvent.touches[0] !== undefined) {
 | 
        
           |  |  | 246 |             // This is a touchmove or touchstart event, get position from the first touch position.
 | 
        
           |  |  | 247 |             var touch = evt.originalEvent.touches[0];
 | 
        
           |  |  | 248 |             evt.pageX = touch.pageX;
 | 
        
           |  |  | 249 |             evt.pageY = touch.pageY;
 | 
        
           |  |  | 250 |         }
 | 
        
           |  |  | 251 |   | 
        
           |  |  | 252 |         if (evt.pageX === undefined) {
 | 
        
           |  |  | 253 |             // Information is not present in case of touchend or when event was emulated by autoScroll.
 | 
        
           |  |  | 254 |             // Take the absolute mouse position from the last event.
 | 
        
           |  |  | 255 |             evt.pageX = this.lastEvent.pageX;
 | 
        
           |  |  | 256 |             evt.pageY = this.lastEvent.pageY;
 | 
        
           |  |  | 257 |         } else {
 | 
        
           |  |  | 258 |             this.lastEvent = evt;
 | 
        
           |  |  | 259 |         }
 | 
        
           |  |  | 260 |   | 
        
           |  |  | 261 |         if (evt.clientX === undefined) {
 | 
        
           |  |  | 262 |             // If not provided in event calculate relative mouse position.
 | 
        
           |  |  | 263 |             evt.clientX = Math.round(evt.pageX - $(window).scrollLeft());
 | 
        
           |  |  | 264 |             evt.clientY = Math.round(evt.pageY - $(window).scrollTop());
 | 
        
           |  |  | 265 |         }
 | 
        
           |  |  | 266 |     };
 | 
        
           |  |  | 267 |   | 
        
           |  |  | 268 |     /**
 | 
        
           |  |  | 269 |      * Handler from dragstart event
 | 
        
           |  |  | 270 |      *
 | 
        
           |  |  | 271 |      * @private
 | 
        
           |  |  | 272 |      * @param {Event} evt
 | 
        
           |  |  | 273 |      */
 | 
        
           |  |  | 274 |     SortableList.prototype.dragStartHandler = function(evt) {
 | 
        
           |  |  | 275 |         if (this.info !== null) {
 | 
        
           |  |  | 276 |             if (this.info.type === 'click' || this.info.type === 'touchend') {
 | 
        
           |  |  | 277 |                 // Ignore double click.
 | 
        
           |  |  | 278 |                 return;
 | 
        
           |  |  | 279 |             }
 | 
        
           |  |  | 280 |             // Mouse down or touch while already dragging, cancel previous dragging.
 | 
        
           |  |  | 281 |             this.moveElement(this.info.sourceList, this.info.sourceNextElement);
 | 
        
           |  |  | 282 |             this.finishDragging();
 | 
        
           |  |  | 283 |         }
 | 
        
           |  |  | 284 |   | 
        
           |  |  | 285 |         if (evt.type === 'mousedown' && evt.which !== 1) {
 | 
        
           |  |  | 286 |             // We only need left mouse click. If this is a mousedown event with right/middle click ignore it.
 | 
        
           |  |  | 287 |             return;
 | 
        
           |  |  | 288 |         }
 | 
        
           |  |  | 289 |   | 
        
           |  |  | 290 |         this.calculatePositionOnPage(evt);
 | 
        
           |  |  | 291 |         var movedElement = $(evt.target).closest($(evt.currentTarget).children());
 | 
        
           |  |  | 292 |         if (!movedElement.length) {
 | 
        
           |  |  | 293 |             // Can't find the element user wants to drag. They clicked on the list but outside of any element of the list.
 | 
        
           |  |  | 294 |             return;
 | 
        
           |  |  | 295 |         }
 | 
        
           |  |  | 296 |   | 
        
           |  |  | 297 |         // Check that we grabbed the element by the handle.
 | 
        
           |  |  | 298 |         if (this.config.moveHandlerSelector !== null) {
 | 
        
           |  |  | 299 |             if (!$(evt.target).closest(this.config.moveHandlerSelector, movedElement).length) {
 | 
        
           |  |  | 300 |                 return;
 | 
        
           |  |  | 301 |             }
 | 
        
           |  |  | 302 |         }
 | 
        
           |  |  | 303 |   | 
        
           |  |  | 304 |         evt.stopPropagation();
 | 
        
           |  |  | 305 |         evt.preventDefault();
 | 
        
           |  |  | 306 |   | 
        
           |  |  | 307 |         // Information about moved element with original location.
 | 
        
           |  |  | 308 |         // This object is passed to event observers.
 | 
        
           |  |  | 309 |         this.dragCounter++;
 | 
        
           |  |  | 310 |         this.info = {
 | 
        
           |  |  | 311 |             element: movedElement,
 | 
        
           |  |  | 312 |             sourceNextElement: movedElement.next(),
 | 
        
           |  |  | 313 |             sourceList: movedElement.parent(),
 | 
        
           |  |  | 314 |             targetNextElement: movedElement.next(),
 | 
        
           |  |  | 315 |             targetList: movedElement.parent(),
 | 
        
           |  |  | 316 |             type: evt.type,
 | 
        
           |  |  | 317 |             dropped: false,
 | 
        
           |  |  | 318 |             startX: evt.pageX,
 | 
        
           |  |  | 319 |             startY: evt.pageY,
 | 
        
           |  |  | 320 |             startTime: new Date().getTime()
 | 
        
           |  |  | 321 |         };
 | 
        
           |  |  | 322 |   | 
        
           |  |  | 323 |         $(this.config.targetListSelector).addClass(CSS.targetListClass);
 | 
        
           |  |  | 324 |   | 
        
           |  |  | 325 |         var offset = movedElement.offset();
 | 
        
           |  |  | 326 |         movedElement.addClass(CSS.currentPositionClass);
 | 
        
           |  |  | 327 |         this.proxyDelta = {x: offset.left - evt.pageX, y: offset.top - evt.pageY};
 | 
        
           |  |  | 328 |         this.proxy = $();
 | 
        
           |  |  | 329 |         var thisDragCounter = this.dragCounter;
 | 
        
           |  |  | 330 |         setTimeout($.proxy(function() {
 | 
        
           |  |  | 331 |             // This mousedown event may in fact be a beginning of a 'click' event. Use timeout before showing the
 | 
        
           |  |  | 332 |             // dragged object so we can catch click event. When timeout finishes make sure that click event
 | 
        
           |  |  | 333 |             // has not happened during this half a second.
 | 
        
           |  |  | 334 |             // Verify dragcounter to make sure the user did not manage to do two very fast drag actions one after another.
 | 
        
           |  |  | 335 |             if (this.info === null || this.info.type === 'click' || this.info.type === 'keypress'
 | 
        
           |  |  | 336 |                     || this.dragCounter !== thisDragCounter) {
 | 
        
           |  |  | 337 |                 return;
 | 
        
           |  |  | 338 |             }
 | 
        
           |  |  | 339 |   | 
        
           |  |  | 340 |             // Create a proxy - the copy of the dragged element that moves together with a mouse.
 | 
        
           |  |  | 341 |             this.createProxy();
 | 
        
           |  |  | 342 |         }, this), 500);
 | 
        
           |  |  | 343 |   | 
        
           |  |  | 344 |         // Start drag.
 | 
        
           |  |  | 345 |         $(window).on('mousemove touchmove.notPassive mouseup touchend.notPassive', $.proxy(this.dragHandler, this));
 | 
        
           |  |  | 346 |         $(window).on('keypress', $.proxy(this.dragcancelHandler, this));
 | 
        
           |  |  | 347 |   | 
        
           |  |  | 348 |         // Start autoscrolling. Every time the page is scrolled emulate the mousemove event.
 | 
        
           |  |  | 349 |         if (this.config.autoScroll) {
 | 
        
           |  |  | 350 |             autoScroll.start(function() {
 | 
        
           |  |  | 351 |                 $(window).trigger('mousemove');
 | 
        
           |  |  | 352 |             });
 | 
        
           |  |  | 353 |         }
 | 
        
           |  |  | 354 |   | 
        
           | 1441 | ariadna | 355 |        this.executeCallback(SortableList.EVENTS.elementDragStart);
 | 
        
           | 1 | efrain | 356 |     };
 | 
        
           |  |  | 357 |   | 
        
           |  |  | 358 |     /**
 | 
        
           |  |  | 359 |      * Creates a "proxy" object - a copy of the element that is being moved that always follows the mouse
 | 
        
           |  |  | 360 |      * @private
 | 
        
           |  |  | 361 |      */
 | 
        
           |  |  | 362 |     SortableList.prototype.createProxy = function() {
 | 
        
           |  |  | 363 |         this.proxy = this.info.element.clone();
 | 
        
           |  |  | 364 |         this.info.sourceList.append(this.proxy);
 | 
        
           |  |  | 365 |         this.proxy.removeAttr('id').removeClass(CSS.currentPositionClass)
 | 
        
           |  |  | 366 |             .addClass(CSS.isDraggedClass).css({position: 'fixed'});
 | 
        
           |  |  | 367 |         this.proxy.offset({top: this.proxyDelta.y + this.lastEvent.pageY, left: this.proxyDelta.x + this.lastEvent.pageX});
 | 
        
           |  |  | 368 |     };
 | 
        
           |  |  | 369 |   | 
        
           |  |  | 370 |     /**
 | 
        
           |  |  | 371 |      * Handler for click event - when user clicks on the drag handler or presses Enter on keyboard
 | 
        
           |  |  | 372 |      *
 | 
        
           |  |  | 373 |      * @private
 | 
        
           |  |  | 374 |      * @param {Event} evt
 | 
        
           |  |  | 375 |      */
 | 
        
           |  |  | 376 |     SortableList.prototype.clickHandler = function(evt) {
 | 
        
           |  |  | 377 |         if (evt.type === 'keypress' && evt.originalEvent.keyCode !== 13 && evt.originalEvent.keyCode !== 32) {
 | 
        
           |  |  | 378 |             return;
 | 
        
           |  |  | 379 |         }
 | 
        
           |  |  | 380 |         if (this.info !== null) {
 | 
        
           |  |  | 381 |             // Ignore double click.
 | 
        
           |  |  | 382 |             return;
 | 
        
           |  |  | 383 |         }
 | 
        
           |  |  | 384 |   | 
        
           |  |  | 385 |         // Find the element that this draghandle belongs to.
 | 
        
           |  |  | 386 |         var clickedElement = $(evt.target).closest(this.config.moveHandlerSelector),
 | 
        
           |  |  | 387 |             sourceList = clickedElement.closest(this.config.listSelector),
 | 
        
           |  |  | 388 |             movedElement = clickedElement.closest(sourceList.children());
 | 
        
           |  |  | 389 |         if (!movedElement.length) {
 | 
        
           |  |  | 390 |             return;
 | 
        
           |  |  | 391 |         }
 | 
        
           |  |  | 392 |   | 
        
           |  |  | 393 |         evt.preventDefault();
 | 
        
           |  |  | 394 |         evt.stopPropagation();
 | 
        
           |  |  | 395 |   | 
        
           |  |  | 396 |         // Store information about moved element with original location.
 | 
        
           |  |  | 397 |         this.dragCounter++;
 | 
        
           |  |  | 398 |         this.info = {
 | 
        
           |  |  | 399 |             element: movedElement,
 | 
        
           |  |  | 400 |             sourceNextElement: movedElement.next(),
 | 
        
           |  |  | 401 |             sourceList: sourceList,
 | 
        
           |  |  | 402 |             targetNextElement: movedElement.next(),
 | 
        
           |  |  | 403 |             targetList: sourceList,
 | 
        
           |  |  | 404 |             dropped: false,
 | 
        
           |  |  | 405 |             type: evt.type,
 | 
        
           |  |  | 406 |             startTime: new Date().getTime()
 | 
        
           |  |  | 407 |         };
 | 
        
           |  |  | 408 |   | 
        
           | 1441 | ariadna | 409 |         this.executeCallback(SortableList.EVENTS.elementDragStart);
 | 
        
           | 1 | efrain | 410 |         this.displayMoveDialogue(clickedElement);
 | 
        
           |  |  | 411 |     };
 | 
        
           |  |  | 412 |   | 
        
           |  |  | 413 |     /**
 | 
        
           |  |  | 414 |      * Finds the position of the mouse inside the element - on the top, on the bottom, on the right or on the left\
 | 
        
           |  |  | 415 |      *
 | 
        
           |  |  | 416 |      * Used to determine if the moved element should be moved after or before the current element
 | 
        
           |  |  | 417 |      *
 | 
        
           |  |  | 418 |      * @private
 | 
        
           |  |  | 419 |      * @param {Number} pageX
 | 
        
           |  |  | 420 |      * @param {Number} pageY
 | 
        
           |  |  | 421 |      * @param {jQuery} element
 | 
        
           |  |  | 422 |      * @returns {(Object|null)}
 | 
        
           |  |  | 423 |      */
 | 
        
           |  |  | 424 |     SortableList.prototype.getPositionInNode = function(pageX, pageY, element) {
 | 
        
           |  |  | 425 |         if (!element.length) {
 | 
        
           |  |  | 426 |             return null;
 | 
        
           |  |  | 427 |         }
 | 
        
           |  |  | 428 |         var node = element[0],
 | 
        
           |  |  | 429 |             offset = 0,
 | 
        
           |  |  | 430 |             rect = node.getBoundingClientRect(),
 | 
        
           |  |  | 431 |             y = pageY - (rect.top + window.scrollY),
 | 
        
           |  |  | 432 |             x = pageX - (rect.left + window.scrollX);
 | 
        
           |  |  | 433 |         if (x >= -offset && x <= rect.width + offset && y >= -offset && y <= rect.height + offset) {
 | 
        
           |  |  | 434 |             return {
 | 
        
           |  |  | 435 |                 x: x,
 | 
        
           |  |  | 436 |                 y: y,
 | 
        
           |  |  | 437 |                 xRatio: rect.width ? (x / rect.width) : 0,
 | 
        
           |  |  | 438 |                 yRatio: rect.height ? (y / rect.height) : 0
 | 
        
           |  |  | 439 |             };
 | 
        
           |  |  | 440 |         }
 | 
        
           |  |  | 441 |         return null;
 | 
        
           |  |  | 442 |     };
 | 
        
           |  |  | 443 |   | 
        
           |  |  | 444 |     /**
 | 
        
           |  |  | 445 |      * Check if list is horizontal
 | 
        
           |  |  | 446 |      *
 | 
        
           |  |  | 447 |      * @param {jQuery} element
 | 
        
           |  |  | 448 |      * @return {Boolean}
 | 
        
           |  |  | 449 |      */
 | 
        
           |  |  | 450 |     SortableList.prototype.isListHorizontal = function(element) {
 | 
        
           |  |  | 451 |         var isHorizontal = this.config.isHorizontal;
 | 
        
           |  |  | 452 |         if (isHorizontal === true || isHorizontal === false) {
 | 
        
           |  |  | 453 |             return isHorizontal;
 | 
        
           |  |  | 454 |         }
 | 
        
           |  |  | 455 |         return isHorizontal(element);
 | 
        
           |  |  | 456 |     };
 | 
        
           |  |  | 457 |   | 
        
           |  |  | 458 |     /**
 | 
        
           |  |  | 459 |      * Handler for events mousemove touchmove mouseup touchend
 | 
        
           |  |  | 460 |      *
 | 
        
           |  |  | 461 |      * @private
 | 
        
           |  |  | 462 |      * @param {Event} evt
 | 
        
           |  |  | 463 |      */
 | 
        
           |  |  | 464 |     SortableList.prototype.dragHandler = function(evt) {
 | 
        
           |  |  | 465 |   | 
        
           |  |  | 466 |         evt.preventDefault();
 | 
        
           |  |  | 467 |         evt.stopPropagation();
 | 
        
           |  |  | 468 |   | 
        
           |  |  | 469 |         this.calculatePositionOnPage(evt);
 | 
        
           |  |  | 470 |   | 
        
           |  |  | 471 |         // We can not use evt.target here because it will most likely be our proxy.
 | 
        
           |  |  | 472 |         // Move the proxy out of the way so we can find the element at the current mouse position.
 | 
        
           |  |  | 473 |         this.proxy.offset({top: -1000, left: -1000});
 | 
        
           |  |  | 474 |         // Find the element at the current mouse position.
 | 
        
           |  |  | 475 |         var element = $(document.elementFromPoint(evt.clientX, evt.clientY));
 | 
        
           |  |  | 476 |   | 
        
           |  |  | 477 |         // Find the list element and the list over the mouse position.
 | 
        
           |  |  | 478 |         var mainElement = this.info.element[0],
 | 
        
           |  |  | 479 |             isNotSelf = function() {
 | 
        
           |  |  | 480 |                 return this !== mainElement;
 | 
        
           |  |  | 481 |             },
 | 
        
           |  |  | 482 |             current = element.closest('.' + CSS.targetListClass + ' > :not(.' + CSS.isDraggedClass + ')').filter(isNotSelf),
 | 
        
           |  |  | 483 |             currentList = element.closest('.' + CSS.targetListClass),
 | 
        
           |  |  | 484 |             proxy = this.proxy,
 | 
        
           |  |  | 485 |             isNotProxy = function() {
 | 
        
           |  |  | 486 |                 return !proxy || !proxy.length || this !== proxy[0];
 | 
        
           |  |  | 487 |             };
 | 
        
           |  |  | 488 |   | 
        
           |  |  | 489 |         // Add the specified class to the list element we are hovering.
 | 
        
           |  |  | 490 |         $('.' + CSS.overElementClass).removeClass(CSS.overElementClass);
 | 
        
           |  |  | 491 |         current.addClass(CSS.overElementClass);
 | 
        
           |  |  | 492 |   | 
        
           |  |  | 493 |         // Move proxy to the current position.
 | 
        
           |  |  | 494 |         this.proxy.offset({top: this.proxyDelta.y + evt.pageY, left: this.proxyDelta.x + evt.pageX});
 | 
        
           |  |  | 495 |   | 
        
           |  |  | 496 |         if (currentList.length && !currentList.children().filter(isNotProxy).length) {
 | 
        
           |  |  | 497 |             // Mouse is over an empty list.
 | 
        
           |  |  | 498 |             this.moveElement(currentList, $());
 | 
        
           |  |  | 499 |         } else if (current.length === 1 && !this.info.element.find(current[0]).length) {
 | 
        
           |  |  | 500 |             // Mouse is over an element in a list - find whether we should move the current position
 | 
        
           |  |  | 501 |             // above or below this element.
 | 
        
           |  |  | 502 |             var coordinates = this.getPositionInNode(evt.pageX, evt.pageY, current);
 | 
        
           |  |  | 503 |             if (coordinates) {
 | 
        
           |  |  | 504 |                 var parent = current.parent(),
 | 
        
           |  |  | 505 |                     ratio = this.isListHorizontal(parent) ? coordinates.xRatio : coordinates.yRatio,
 | 
        
           |  |  | 506 |                     subList = current.find('.' + CSS.targetListClass),
 | 
        
           |  |  | 507 |                     subListEmpty = !subList.children().filter(isNotProxy).filter(isNotSelf).length;
 | 
        
           |  |  | 508 |                 if (subList.length && subListEmpty && ratio > 0.2 && ratio < 0.8) {
 | 
        
           |  |  | 509 |                     // This is an element that is a parent of an empty list and we are around the middle of this element.
 | 
        
           |  |  | 510 |                     // Treat it as if we are over this empty list.
 | 
        
           |  |  | 511 |                    this.moveElement(subList, $());
 | 
        
           |  |  | 512 |                 } else if (ratio > 0.5) {
 | 
        
           |  |  | 513 |                     // Insert after this element.
 | 
        
           |  |  | 514 |                    this.moveElement(parent, current.next().filter(isNotProxy));
 | 
        
           |  |  | 515 |                 } else {
 | 
        
           |  |  | 516 |                     // Insert before this element.
 | 
        
           |  |  | 517 |                    this.moveElement(parent, current);
 | 
        
           |  |  | 518 |                 }
 | 
        
           |  |  | 519 |             }
 | 
        
           |  |  | 520 |         }
 | 
        
           |  |  | 521 |   | 
        
           |  |  | 522 |         if (evt.type === 'mouseup' || evt.type === 'touchend') {
 | 
        
           |  |  | 523 |             // Drop the moved element.
 | 
        
           |  |  | 524 |             this.info.endX = evt.pageX;
 | 
        
           |  |  | 525 |             this.info.endY = evt.pageY;
 | 
        
           |  |  | 526 |             this.info.endTime = new Date().getTime();
 | 
        
           |  |  | 527 |             this.info.dropped = true;
 | 
        
           |  |  | 528 |             this.info.positionChanged = this.hasPositionChanged(this.info);
 | 
        
           |  |  | 529 |             var oldinfo = this.info;
 | 
        
           | 1441 | ariadna | 530 |             this.executeCallback(SortableList.EVENTS.elementDrop);
 | 
        
           | 1 | efrain | 531 |             this.finishDragging();
 | 
        
           |  |  | 532 |   | 
        
           |  |  | 533 |             if (evt.type === 'touchend'
 | 
        
           |  |  | 534 |                     && this.config.moveHandlerSelector !== null
 | 
        
           |  |  | 535 |                     && (oldinfo.endTime - oldinfo.startTime < 500)
 | 
        
           |  |  | 536 |                     && !oldinfo.positionChanged) {
 | 
        
           |  |  | 537 |                 // The click event is not triggered on touch screens because we call preventDefault in touchstart handler.
 | 
        
           |  |  | 538 |                 // If the touchend quickly followed touchstart without moving, consider it a "click".
 | 
        
           |  |  | 539 |                 this.clickHandler(evt);
 | 
        
           |  |  | 540 |             } else if (oldinfo.positionChanged) {
 | 
        
           |  |  | 541 |                 mainElement.classList.add(CSS.isDroppedClass);
 | 
        
           |  |  | 542 |             }
 | 
        
           |  |  | 543 |         }
 | 
        
           |  |  | 544 |     };
 | 
        
           |  |  | 545 |   | 
        
           |  |  | 546 |     /**
 | 
        
           |  |  | 547 |      * Checks if the position of the dragged element in the list has changed
 | 
        
           |  |  | 548 |      *
 | 
        
           |  |  | 549 |      * @private
 | 
        
           |  |  | 550 |      * @param {Object} info
 | 
        
           |  |  | 551 |      * @return {Boolean}
 | 
        
           |  |  | 552 |      */
 | 
        
           |  |  | 553 |     SortableList.prototype.hasPositionChanged = function(info) {
 | 
        
           |  |  | 554 |         return info.sourceList[0] !== info.targetList[0] ||
 | 
        
           |  |  | 555 |             info.sourceNextElement.length !== info.targetNextElement.length ||
 | 
        
           |  |  | 556 |             (info.sourceNextElement.length && info.sourceNextElement[0] !== info.targetNextElement[0]);
 | 
        
           |  |  | 557 |     };
 | 
        
           |  |  | 558 |   | 
        
           |  |  | 559 |     /**
 | 
        
           |  |  | 560 |      * Moves the current position of the dragged element
 | 
        
           |  |  | 561 |      *
 | 
        
           |  |  | 562 |      * @private
 | 
        
           |  |  | 563 |      * @param {jQuery} parentElement
 | 
        
           |  |  | 564 |      * @param {jQuery} beforeElement
 | 
        
           |  |  | 565 |      */
 | 
        
           |  |  | 566 |     SortableList.prototype.moveElement = function(parentElement, beforeElement) {
 | 
        
           |  |  | 567 |         var dragEl = this.info.element;
 | 
        
           |  |  | 568 |         if (beforeElement.length && beforeElement[0] === dragEl[0]) {
 | 
        
           |  |  | 569 |             // Insert before the current position of the dragged element - nothing to do.
 | 
        
           |  |  | 570 |             return;
 | 
        
           |  |  | 571 |         }
 | 
        
           |  |  | 572 |         if (parentElement[0] === this.info.targetList[0] &&
 | 
        
           |  |  | 573 |                 beforeElement.length === this.info.targetNextElement.length &&
 | 
        
           |  |  | 574 |                 beforeElement[0] === this.info.targetNextElement[0]) {
 | 
        
           |  |  | 575 |             // Insert in the same location as the current position - nothing to do.
 | 
        
           |  |  | 576 |             return;
 | 
        
           |  |  | 577 |         }
 | 
        
           |  |  | 578 |   | 
        
           |  |  | 579 |         if (beforeElement.length) {
 | 
        
           |  |  | 580 |             // Move the dragged element before the specified element.
 | 
        
           |  |  | 581 |             parentElement[0].insertBefore(dragEl[0], beforeElement[0]);
 | 
        
           |  |  | 582 |         } else if (this.proxy && this.proxy.parent().length && this.proxy.parent()[0] === parentElement[0]) {
 | 
        
           |  |  | 583 |             // We need to move to the end of the list but the last element in this list is a proxy.
 | 
        
           |  |  | 584 |             // Always leave the proxy in the end of the list.
 | 
        
           |  |  | 585 |             parentElement[0].insertBefore(dragEl[0], this.proxy[0]);
 | 
        
           |  |  | 586 |         } else {
 | 
        
           |  |  | 587 |             // Insert in the end of a list (when proxy is in another list).
 | 
        
           |  |  | 588 |             parentElement[0].appendChild(dragEl[0]);
 | 
        
           |  |  | 589 |         }
 | 
        
           |  |  | 590 |   | 
        
           |  |  | 591 |         // Save the current position of the dragged element in the list.
 | 
        
           |  |  | 592 |         this.info.targetList = parentElement;
 | 
        
           |  |  | 593 |         this.info.targetNextElement = beforeElement;
 | 
        
           | 1441 | ariadna | 594 |         this.executeCallback(SortableList.EVENTS.elementDrag);
 | 
        
           | 1 | efrain | 595 |     };
 | 
        
           |  |  | 596 |   | 
        
           |  |  | 597 |     /**
 | 
        
           |  |  | 598 |      * Finish dragging (when dropped or cancelled).
 | 
        
           |  |  | 599 |      * @private
 | 
        
           |  |  | 600 |      */
 | 
        
           |  |  | 601 |     SortableList.prototype.finishDragging = function() {
 | 
        
           |  |  | 602 |         this.resetDraggedClasses();
 | 
        
           |  |  | 603 |         if (this.config.autoScroll) {
 | 
        
           |  |  | 604 |             autoScroll.stop();
 | 
        
           |  |  | 605 |         }
 | 
        
           |  |  | 606 |         $(window).off('mousemove touchmove.notPassive mouseup touchend.notPassive', $.proxy(this.dragHandler, this));
 | 
        
           |  |  | 607 |         $(window).off('keypress', $.proxy(this.dragcancelHandler, this));
 | 
        
           | 1441 | ariadna | 608 |         this.executeCallback(SortableList.EVENTS.elementDragEnd);
 | 
        
           | 1 | efrain | 609 |         this.info = null;
 | 
        
           |  |  | 610 |     };
 | 
        
           |  |  | 611 |   | 
        
           |  |  | 612 |     /**
 | 
        
           |  |  | 613 |      * Executes callback specified in sortable list parameters
 | 
        
           |  |  | 614 |      *
 | 
        
           |  |  | 615 |      * @private
 | 
        
           |  |  | 616 |      * @param {String} eventName
 | 
        
           |  |  | 617 |      */
 | 
        
           |  |  | 618 |     SortableList.prototype.executeCallback = function(eventName) {
 | 
        
           | 1441 | ariadna | 619 |         EventDispatcher.dispatchEvent(eventName, this.info, this.info.element[0]);
 | 
        
           |  |  | 620 |   | 
        
           |  |  | 621 |         // The following event trigger is legacy and will be removed in the future.
 | 
        
           |  |  | 622 |         // This approach provides a backwards-compatibility layer for the new events.
 | 
        
           |  |  | 623 |         // Code should be updated to make use of native events.
 | 
        
           |  |  | 624 |         const legacyEventNamesMap = new Map([
 | 
        
           |  |  | 625 |             [SortableList.EVENTS.elementDragStart, SortableList.EVENTS.DRAGSTART],
 | 
        
           |  |  | 626 |             [SortableList.EVENTS.elementDrag, SortableList.EVENTS.DRAG],
 | 
        
           |  |  | 627 |             [SortableList.EVENTS.elementDrop, SortableList.EVENTS.DROP],
 | 
        
           |  |  | 628 |             [SortableList.EVENTS.elementDragEnd, SortableList.EVENTS.DRAGEND],
 | 
        
           |  |  | 629 |         ]);
 | 
        
           |  |  | 630 |         this.info.element.trigger(legacyEventNamesMap.get(eventName), this.info);
 | 
        
           | 1 | efrain | 631 |     };
 | 
        
           |  |  | 632 |   | 
        
           |  |  | 633 |     /**
 | 
        
           |  |  | 634 |      * Handler from keypress event (cancel dragging when Esc is pressed)
 | 
        
           |  |  | 635 |      *
 | 
        
           |  |  | 636 |      * @private
 | 
        
           |  |  | 637 |      * @param {Event} evt
 | 
        
           |  |  | 638 |      */
 | 
        
           |  |  | 639 |     SortableList.prototype.dragcancelHandler = function(evt) {
 | 
        
           |  |  | 640 |         if (evt.type !== 'keypress' || evt.originalEvent.keyCode !== 27) {
 | 
        
           |  |  | 641 |             // Only cancel dragging when Esc was pressed.
 | 
        
           |  |  | 642 |             return;
 | 
        
           |  |  | 643 |         }
 | 
        
           |  |  | 644 |         // Dragging was cancelled. Return item to the original position.
 | 
        
           |  |  | 645 |         this.moveElement(this.info.sourceList, this.info.sourceNextElement);
 | 
        
           |  |  | 646 |         this.finishDragging();
 | 
        
           |  |  | 647 |     };
 | 
        
           |  |  | 648 |   | 
        
           |  |  | 649 |     /**
 | 
        
           |  |  | 650 |      * Returns the name of the current element to be used in the move dialogue
 | 
        
           |  |  | 651 |      *
 | 
        
           |  |  | 652 |      * @public
 | 
        
           |  |  | 653 |      * @param {jQuery} element
 | 
        
           |  |  | 654 |      * @return {Promise}
 | 
        
           |  |  | 655 |      */
 | 
        
           |  |  | 656 |     SortableList.prototype.getElementName = function(element) {
 | 
        
           |  |  | 657 |         return $.Deferred().resolve(element.text());
 | 
        
           |  |  | 658 |     };
 | 
        
           |  |  | 659 |   | 
        
           |  |  | 660 |     /**
 | 
        
           |  |  | 661 |      * Returns the label for the potential move destination, i.e. "After ElementX" or "To the top of the list"
 | 
        
           |  |  | 662 |      *
 | 
        
           |  |  | 663 |      * Note that we use "after" in the label for better UX
 | 
        
           |  |  | 664 |      *
 | 
        
           |  |  | 665 |      * @public
 | 
        
           |  |  | 666 |      * @param {jQuery} parentElement
 | 
        
           |  |  | 667 |      * @param {jQuery} afterElement
 | 
        
           |  |  | 668 |      * @return {Promise}
 | 
        
           |  |  | 669 |      */
 | 
        
           |  |  | 670 |     SortableList.prototype.getDestinationName = function(parentElement, afterElement) {
 | 
        
           |  |  | 671 |         if (!afterElement.length) {
 | 
        
           |  |  | 672 |             return str.get_string('movecontenttothetop', 'moodle');
 | 
        
           |  |  | 673 |         } else {
 | 
        
           |  |  | 674 |             return this.getElementName(afterElement)
 | 
        
           |  |  | 675 |                 .then(function(name) {
 | 
        
           |  |  | 676 |                     return str.get_string('movecontentafter', 'moodle', name);
 | 
        
           |  |  | 677 |                 });
 | 
        
           |  |  | 678 |         }
 | 
        
           |  |  | 679 |     };
 | 
        
           |  |  | 680 |   | 
        
           |  |  | 681 |     /**
 | 
        
           |  |  | 682 |      * Returns the title for the move dialogue ("Move elementY")
 | 
        
           |  |  | 683 |      *
 | 
        
           |  |  | 684 |      * @public
 | 
        
           |  |  | 685 |      * @param {jQuery} element
 | 
        
           |  |  | 686 |      * @param {jQuery} handler
 | 
        
           |  |  | 687 |      * @return {Promise}
 | 
        
           |  |  | 688 |      */
 | 
        
           |  |  | 689 |     SortableList.prototype.getMoveDialogueTitle = function(element, handler) {
 | 
        
           |  |  | 690 |         if (handler.attr('title')) {
 | 
        
           |  |  | 691 |             return $.Deferred().resolve(handler.attr('title'));
 | 
        
           |  |  | 692 |         }
 | 
        
           |  |  | 693 |         return this.getElementName(element).then(function(name) {
 | 
        
           |  |  | 694 |             return str.get_string('movecontent', 'moodle', name);
 | 
        
           |  |  | 695 |         });
 | 
        
           |  |  | 696 |     };
 | 
        
           |  |  | 697 |   | 
        
           |  |  | 698 |     /**
 | 
        
           |  |  | 699 |      * Returns the list of possible move destinations
 | 
        
           |  |  | 700 |      *
 | 
        
           |  |  | 701 |      * @private
 | 
        
           |  |  | 702 |      * @return {Promise}
 | 
        
           |  |  | 703 |      */
 | 
        
           |  |  | 704 |     SortableList.prototype.getDestinationsList = function() {
 | 
        
           |  |  | 705 |         var addedLists = [],
 | 
        
           |  |  | 706 |             targets = $(this.config.targetListSelector),
 | 
        
           |  |  | 707 |             destinations = $('<ul/>').addClass(CSS.keyboardDragClass),
 | 
        
           |  |  | 708 |             result = $.when().then(function() {
 | 
        
           |  |  | 709 |                 return destinations;
 | 
        
           |  |  | 710 |             }),
 | 
        
           |  |  | 711 |             createLink = $.proxy(function(parentElement, beforeElement, afterElement) {
 | 
        
           |  |  | 712 |                 if (beforeElement.is(this.info.element) || afterElement.is(this.info.element)) {
 | 
        
           |  |  | 713 |                     // Can not move before or after itself.
 | 
        
           |  |  | 714 |                     return;
 | 
        
           |  |  | 715 |                 }
 | 
        
           |  |  | 716 |                 if ($.contains(this.info.element[0], parentElement[0])) {
 | 
        
           |  |  | 717 |                     // Can not move to its own child.
 | 
        
           |  |  | 718 |                     return;
 | 
        
           |  |  | 719 |                 }
 | 
        
           |  |  | 720 |                 result = result
 | 
        
           |  |  | 721 |                 .then($.proxy(function() {
 | 
        
           |  |  | 722 |                     return this.getDestinationName(parentElement, afterElement);
 | 
        
           |  |  | 723 |                 }, this))
 | 
        
           |  |  | 724 |                 .then(function(txt) {
 | 
        
           |  |  | 725 |                     var li = $('<li/>').appendTo(destinations);
 | 
        
           |  |  | 726 |                     var a = $('<a href="#"/>').attr('data-core_sortable_list-quickmove', 1).appendTo(li);
 | 
        
           |  |  | 727 |                     a.data('parent-element', parentElement).data('before-element', beforeElement).text(txt);
 | 
        
           |  |  | 728 |                     return destinations;
 | 
        
           |  |  | 729 |                 });
 | 
        
           |  |  | 730 |             }, this),
 | 
        
           |  |  | 731 |             addList = function() {
 | 
        
           |  |  | 732 |                 // Destination lists may be nested. We want to add all move destinations in the same
 | 
        
           |  |  | 733 |                 // order they appear on the screen for the user.
 | 
        
           |  |  | 734 |                 if ($.inArray(this, addedLists) !== -1) {
 | 
        
           |  |  | 735 |                     return;
 | 
        
           |  |  | 736 |                 }
 | 
        
           |  |  | 737 |                 addedLists.push(this);
 | 
        
           |  |  | 738 |                 var list = $(this),
 | 
        
           |  |  | 739 |                     children = list.children();
 | 
        
           |  |  | 740 |                 children.each(function() {
 | 
        
           |  |  | 741 |                     var element = $(this);
 | 
        
           |  |  | 742 |                     createLink(list, element, element.prev());
 | 
        
           |  |  | 743 |                     // Add all nested lists.
 | 
        
           |  |  | 744 |                     element.find(targets).each(addList);
 | 
        
           |  |  | 745 |                 });
 | 
        
           |  |  | 746 |                 createLink(list, $(), children.last());
 | 
        
           |  |  | 747 |             };
 | 
        
           |  |  | 748 |         targets.each(addList);
 | 
        
           |  |  | 749 |         return result;
 | 
        
           |  |  | 750 |     };
 | 
        
           |  |  | 751 |   | 
        
           |  |  | 752 |     /**
 | 
        
           |  |  | 753 |      * Displays the dialogue to move element.
 | 
        
           |  |  | 754 |      * @param {jQuery} clickedElement element to return focus to after the modal is closed
 | 
        
           |  |  | 755 |      * @private
 | 
        
           |  |  | 756 |      */
 | 
        
           |  |  | 757 |     SortableList.prototype.displayMoveDialogue = function(clickedElement) {
 | 
        
           |  |  | 758 |         ModalCancel.create({
 | 
        
           |  |  | 759 |             title: this.getMoveDialogueTitle(this.info.element, clickedElement),
 | 
        
           |  |  | 760 |             body: this.getDestinationsList()
 | 
        
           |  |  | 761 |         }).then($.proxy(function(modal) {
 | 
        
           |  |  | 762 |             var quickMoveHandler = $.proxy(function(e) {
 | 
        
           |  |  | 763 |                 e.preventDefault();
 | 
        
           |  |  | 764 |                 e.stopPropagation();
 | 
        
           |  |  | 765 |                 this.moveElement($(e.currentTarget).data('parent-element'), $(e.currentTarget).data('before-element'));
 | 
        
           |  |  | 766 |                 this.info.endTime = new Date().getTime();
 | 
        
           |  |  | 767 |                 this.info.positionChanged = this.hasPositionChanged(this.info);
 | 
        
           |  |  | 768 |                 this.info.dropped = true;
 | 
        
           |  |  | 769 |                 clickedElement.focus();
 | 
        
           | 1441 | ariadna | 770 |                 this.executeCallback(SortableList.EVENTS.elementDrop);
 | 
        
           | 1 | efrain | 771 |                 modal.hide();
 | 
        
           |  |  | 772 |             }, this);
 | 
        
           |  |  | 773 |             modal.getRoot().on('click', '[data-core_sortable_list-quickmove]', quickMoveHandler);
 | 
        
           |  |  | 774 |             modal.getRoot().on(ModalEvents.hidden, $.proxy(function() {
 | 
        
           |  |  | 775 |                 // Always destroy when hidden, it is generated dynamically each time.
 | 
        
           |  |  | 776 |                 modal.getRoot().off('click', '[data-core_sortable_list-quickmove]', quickMoveHandler);
 | 
        
           |  |  | 777 |                 modal.destroy();
 | 
        
           |  |  | 778 |                 this.finishDragging();
 | 
        
           |  |  | 779 |             }, this));
 | 
        
           |  |  | 780 |             modal.setLarge();
 | 
        
           |  |  | 781 |             modal.show();
 | 
        
           |  |  | 782 |             return modal;
 | 
        
           |  |  | 783 |         }, this)).catch(Notification.exception);
 | 
        
           |  |  | 784 |     };
 | 
        
           |  |  | 785 |   | 
        
           |  |  | 786 |     return SortableList;
 | 
        
           |  |  | 787 |   | 
        
           |  |  | 788 | });
 |