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