MDL-51803 core: sortable list singleton
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+219
-281
@@ -18,15 +18,20 @@
|
||||
*
|
||||
* Example of usage:
|
||||
*
|
||||
* define(['jquery', 'core/sortable_list'], function($, sortableList) {
|
||||
* sortableList.init({
|
||||
* listSelector: 'ul.my-awesome-list', // mandatory, CSS selector for the list (usually <ul> or <tbody>)
|
||||
* moveHandlerSelector: '.draghandle' // CSS selector of the crossarrow handle. Make sure that this
|
||||
* element can handle keypress and mouse click events for displaying accessible move popup.
|
||||
* });
|
||||
* $('ul.my-awesome-list > *').on('sortablelist-drop', function(evt, info) {
|
||||
* define(['jquery', 'core/sortable_list'], function($, SortableList) {
|
||||
* var list = new SortableList('ul.my-awesome-list', // source list (usually <ul> or <tbody>) - selector or element
|
||||
* {
|
||||
* moveHandlerSelector: '.draghandle' // CSS selector of the crossarrow handle. Make sure that this
|
||||
* // element can handle keypress and mouse click events for displaying accessible move popup.
|
||||
* });
|
||||
* $('ul.my-awesome-list > *').on(SortableList.EVENTS.DROP, function(evt, info) {
|
||||
* console.log(info);
|
||||
* });
|
||||
*
|
||||
* // Advanced usage. Overwrite methods getElementName, getDestinationName, moveDialogueTitle, for example:
|
||||
* list.getElementName = function(element) {
|
||||
* return $.Deferred().resolve(element.attr('data-name'));
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* More details: https://docs.moodle.org/dev/Sortable_list
|
||||
@@ -34,10 +39,10 @@
|
||||
* For the full list of possible parameters see var defaultParameters below.
|
||||
*
|
||||
* The following jQuery events are fired:
|
||||
* - sortablelist-dragstart : when user started dragging a list element
|
||||
* - sortablelist-drag : when user dragged a list element to a new position
|
||||
* - sortablelist-drop : when user dropped a list element
|
||||
* - sortablelist-dragend : when user finished dragging - either fired right after dropping or
|
||||
* - SortableList.EVENTS.DRAGSTART : when user started dragging a list element
|
||||
* - SortableList.EVENTS.DRAG : when user dragged a list element to a new position
|
||||
* - SortableList.EVENTS.DROP : when user dropped a list element
|
||||
* - SortableList.EVENTS.DROPEND : when user finished dragging - either fired right after dropping or
|
||||
* if "Esc" was pressed during dragging
|
||||
*
|
||||
* @module core/sortable_list
|
||||
@@ -59,25 +64,7 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
targetListSelector: null,
|
||||
moveHandlerSelector: null,
|
||||
isHorizontal: false,
|
||||
autoScroll: true,
|
||||
elementNameCallback: function(element) {
|
||||
return element.text();
|
||||
},
|
||||
destinationNameCallback: function(parentElement, afterElement) {
|
||||
if (!afterElement.length) {
|
||||
return str.get_string('movecontenttothetop', 'moodle');
|
||||
} else {
|
||||
return getElementName(afterElement)
|
||||
.then(function(name) {
|
||||
return str.get_string('movecontentafter', 'moodle', name);
|
||||
});
|
||||
}
|
||||
},
|
||||
moveDialogueTitleCallback: function(element) {
|
||||
return getElementName(element).then(function(name) {
|
||||
return str.get_string('movecontent', 'moodle', name);
|
||||
});
|
||||
}
|
||||
autoScroll: true
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -96,72 +83,78 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores parameters of the currently dragged item
|
||||
* Initialise sortable list.
|
||||
*
|
||||
* @private
|
||||
* @param {(String|jQuery|Element)} root JQuery/DOM element representing sortable list (i.e. <ul>, <tbody>) or CSS selector
|
||||
* @param {Object} config Parameters for the list. See defaultParameters above for examples.
|
||||
* @property {(String|jQuery|Element)} config.targetListSelector target lists, by default same as root
|
||||
* @property {String} config.moveHandlerSelector CSS selector for a drag handle. By default the whole item is a handle.
|
||||
* Without drag handle sorting is not accessible!
|
||||
* @property {String} config.targetListSelector CSS selector for target lists. By default the same as root
|
||||
* @property {(Boolean|Function)} config.isHorizontal Set to true if the list is horizontal
|
||||
* (can also be a callback with list as an argument)
|
||||
* @property {Boolean} config.autoScroll Engages autoscroll module for automatic vertical scrolling of the
|
||||
* whole page, by default true
|
||||
*/
|
||||
var SortableList = function(root, config) {
|
||||
|
||||
this.info = null;
|
||||
this.proxy = null;
|
||||
this.proxyDelta = null;
|
||||
this.dragCounter = 0;
|
||||
this.lastEvent = null;
|
||||
|
||||
this.config = $.extend({}, defaultParameters, config || {});
|
||||
this.config.listSelector = root;
|
||||
if (!this.config.targetListSelector) {
|
||||
this.config.targetListSelector = root;
|
||||
}
|
||||
if (typeof this.config.listSelector === 'object') {
|
||||
// The root is an element on the page. Register a listener for this element.
|
||||
$(this.config.listSelector).on('mousedown touchstart', $.proxy(this.dragStartHandler, this));
|
||||
} else {
|
||||
// The root is a CSS selector. Register a listener that picks up the element dynamically.
|
||||
$('body').on('mousedown touchstart', this.config.listSelector, $.proxy(this.dragStartHandler, this));
|
||||
}
|
||||
if (this.config.moveHandlerSelector !== null) {
|
||||
$('body').on('click keypress', this.config.moveHandlerSelector, $.proxy(this.clickHandler, this));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Events fired by this entity
|
||||
*
|
||||
* @public
|
||||
* @type {Object}
|
||||
*/
|
||||
var config = {};
|
||||
|
||||
/**
|
||||
* Stores information about currently dragged item
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
var info = null;
|
||||
|
||||
/**
|
||||
* Stores the proxy object
|
||||
*
|
||||
* @private
|
||||
* @type {jQuery}
|
||||
*/
|
||||
var proxy;
|
||||
|
||||
/**
|
||||
* Stores initial position of the proxy
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
var proxyDelta;
|
||||
|
||||
/**
|
||||
* Counter of drag events
|
||||
*
|
||||
* @private
|
||||
* @type {Number}
|
||||
*/
|
||||
var dragCounter = 0;
|
||||
SortableList.EVENTS = {
|
||||
DRAGSTART: 'sortablelist-dragstart',
|
||||
DRAG: 'sortablelist-drag',
|
||||
DROP: 'sortablelist-drop',
|
||||
DRAGEND: 'sortablelist-dragend'
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the temporary classes assigned during dragging
|
||||
* @private
|
||||
*/
|
||||
var resetDraggedClasses = function() {
|
||||
SortableList.prototype.resetDraggedClasses = function() {
|
||||
var classes = [
|
||||
config.isDraggedClass,
|
||||
config.currentPositionClass,
|
||||
config.overElementClass,
|
||||
config.targetListClass,
|
||||
config.sourceListClass
|
||||
CSS.isDraggedClass,
|
||||
CSS.currentPositionClass,
|
||||
CSS.overElementClass,
|
||||
CSS.targetListClass,
|
||||
];
|
||||
for (var i in classes) {
|
||||
$('.' + classes[i]).removeClass(classes[i]);
|
||||
}
|
||||
if (proxy) {
|
||||
proxy.remove();
|
||||
proxy = $();
|
||||
if (this.proxy) {
|
||||
this.proxy.remove();
|
||||
this.proxy = $();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* {Event} stores the last event that had pageX and pageY defined
|
||||
* @private
|
||||
*/
|
||||
var lastEvent;
|
||||
|
||||
/**
|
||||
* Calculates evt.pageX, evt.pageY, evt.clientX and evt.clientY
|
||||
*
|
||||
@@ -171,7 +164,7 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @private
|
||||
* @param {Event} evt
|
||||
*/
|
||||
var calculatePositionOnPage = function(evt) {
|
||||
SortableList.prototype.calculatePositionOnPage = function(evt) {
|
||||
|
||||
if (evt.originalEvent && evt.originalEvent.touches && evt.originalEvent.touches[0] !== undefined) {
|
||||
// This is a touchmove or touchstart event, get position from the first touch position.
|
||||
@@ -183,10 +176,10 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
if (evt.pageX === undefined) {
|
||||
// Information is not present in case of touchend or when event was emulated by autoScroll.
|
||||
// Take the absolute mouse position from the last event.
|
||||
evt.pageX = lastEvent.pageX;
|
||||
evt.pageY = lastEvent.pageY;
|
||||
evt.pageX = this.lastEvent.pageX;
|
||||
evt.pageY = this.lastEvent.pageY;
|
||||
} else {
|
||||
lastEvent = evt;
|
||||
this.lastEvent = evt;
|
||||
}
|
||||
|
||||
if (evt.clientX === undefined) {
|
||||
@@ -202,16 +195,15 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @private
|
||||
* @param {Event} evt
|
||||
*/
|
||||
var dragStartHandler = function(evt) {
|
||||
config = evt.data.config;
|
||||
if (info !== null) {
|
||||
if (info.type === 'click') {
|
||||
SortableList.prototype.dragStartHandler = function(evt) {
|
||||
if (this.info !== null) {
|
||||
if (this.info.type === 'click') {
|
||||
// Ignore double click.
|
||||
return;
|
||||
}
|
||||
// Mouse down or touch while already dragging, cancel previous dragging.
|
||||
moveElement(info.sourceList, info.sourceNextElement);
|
||||
finishDragging();
|
||||
this.moveElement(this.info.sourceList, this.info.sourceNextElement);
|
||||
this.finishDragging();
|
||||
}
|
||||
|
||||
if (evt.type === 'mousedown' && evt.which !== 1) {
|
||||
@@ -219,7 +211,7 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
calculatePositionOnPage(evt);
|
||||
this.calculatePositionOnPage(evt);
|
||||
var movedElement = $(evt.target).closest($(evt.currentTarget).children());
|
||||
if (!movedElement.length) {
|
||||
// Can't find the element user wants to drag. They clicked on the list but outside of any element of the list.
|
||||
@@ -227,8 +219,8 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
}
|
||||
|
||||
// Check that we grabbed the element by the handle.
|
||||
if (config.moveHandlerSelector !== null) {
|
||||
if (!$(evt.target).closest(config.moveHandlerSelector, movedElement).length) {
|
||||
if (this.config.moveHandlerSelector !== null) {
|
||||
if (!$(evt.target).closest(this.config.moveHandlerSelector, movedElement).length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -238,8 +230,8 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
|
||||
// Information about moved element with original location.
|
||||
// This object is passed to event observers.
|
||||
dragCounter++;
|
||||
info = {
|
||||
this.dragCounter++;
|
||||
this.info = {
|
||||
element: movedElement,
|
||||
sourceNextElement: movedElement.next(),
|
||||
sourceList: movedElement.parent(),
|
||||
@@ -252,50 +244,51 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
startTime: new Date().getTime()
|
||||
};
|
||||
|
||||
$(config.targetListSelector).addClass(config.targetListClass);
|
||||
$(this.config.targetListSelector).addClass(CSS.targetListClass);
|
||||
|
||||
var offset = movedElement.offset();
|
||||
movedElement.addClass(config.currentPositionClass);
|
||||
proxyDelta = {x: offset.left - evt.pageX, y: offset.top - evt.pageY};
|
||||
proxy = $();
|
||||
var thisDragCounter = dragCounter;
|
||||
setTimeout(function() {
|
||||
movedElement.addClass(CSS.currentPositionClass);
|
||||
this.proxyDelta = {x: offset.left - evt.pageX, y: offset.top - evt.pageY};
|
||||
this.proxy = $();
|
||||
var thisDragCounter = this.dragCounter;
|
||||
setTimeout($.proxy(function() {
|
||||
// This mousedown event may in fact be a beginning of a 'click' event. Use timeout before showing the
|
||||
// dragged object so we can catch click event. When timeout finishes make sure that click event
|
||||
// has not happened during this half a second.
|
||||
// Verify dragcounter to make sure the user did not manage to do two very fast drag actions one after another.
|
||||
if (info === null || info.type === 'click' || info.type === 'keypress' || dragCounter !== thisDragCounter) {
|
||||
if (this.info === null || this.info.type === 'click' || this.info.type === 'keypress'
|
||||
|| this.dragCounter !== thisDragCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a proxy - the copy of the dragged element that moves together with a mouse.
|
||||
createProxy();
|
||||
}, 500);
|
||||
this.createProxy();
|
||||
}, this), 500);
|
||||
|
||||
// Start drag.
|
||||
$('body').on('mousemove touchmove mouseup touchend', dragHandler);
|
||||
$('body').on('keypress', dragcancelHandler);
|
||||
$(window).on('mousemove touchmove mouseup touchend', $.proxy(this.dragHandler, this));
|
||||
$(window).on('keypress', $.proxy(this.dragcancelHandler, this));
|
||||
|
||||
// Start autoscrolling. Every time the page is scrolled emulate the mousemove event.
|
||||
if (config.autoScroll) {
|
||||
if (this.config.autoScroll) {
|
||||
autoScroll.start(function() {
|
||||
$('body').trigger('mousemove');
|
||||
$(window).trigger('mousemove');
|
||||
});
|
||||
}
|
||||
|
||||
executeCallback('dragstart');
|
||||
this.executeCallback(SortableList.EVENTS.DRAGSTART);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a "proxy" object - a copy of the element that is being moved that always follows the mouse
|
||||
* @private
|
||||
*/
|
||||
var createProxy = function() {
|
||||
proxy = info.element.clone();
|
||||
info.sourceList.append(proxy);
|
||||
proxy.removeAttr('id').removeClass(config.currentPositionClass)
|
||||
.addClass(config.isDraggedClass).css({position: 'fixed'});
|
||||
proxy.offset({top: proxyDelta.y + lastEvent.pageY, left: proxyDelta.x + lastEvent.pageX});
|
||||
SortableList.prototype.createProxy = function() {
|
||||
this.proxy = this.info.element.clone();
|
||||
this.info.sourceList.append(this.proxy);
|
||||
this.proxy.removeAttr('id').removeClass(CSS.currentPositionClass)
|
||||
.addClass(CSS.isDraggedClass).css({position: 'fixed'});
|
||||
this.proxy.offset({top: this.proxyDelta.y + this.lastEvent.pageY, left: this.proxyDelta.x + this.lastEvent.pageX});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -304,29 +297,28 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @private
|
||||
* @param {Event} evt
|
||||
*/
|
||||
var clickHandler = function(evt) {
|
||||
SortableList.prototype.clickHandler = function(evt) {
|
||||
if (evt.type === 'keypress' && evt.originalEvent.keyCode !== 13 && evt.originalEvent.keyCode !== 32) {
|
||||
return;
|
||||
}
|
||||
if (info !== null && info.type === 'click') {
|
||||
if (this.info !== null && this.info.type === 'click') {
|
||||
// Ignore double click.
|
||||
return;
|
||||
}
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
config = evt.data.config;
|
||||
|
||||
// Find the element that this draghandle belongs to.
|
||||
var clickedElement = $(evt.currentTarget),
|
||||
sourceList = clickedElement.closest(config.listSelector),
|
||||
sourceList = clickedElement.closest(this.config.listSelector),
|
||||
movedElement = clickedElement.closest(sourceList.children());
|
||||
if (!movedElement.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store information about moved element with original location.
|
||||
dragCounter++;
|
||||
info = {
|
||||
this.dragCounter++;
|
||||
this.info = {
|
||||
element: movedElement,
|
||||
sourceNextElement: movedElement.next(),
|
||||
sourceList: sourceList,
|
||||
@@ -337,8 +329,8 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
startTime: new Date().getTime()
|
||||
};
|
||||
|
||||
executeCallback('dragstart');
|
||||
displayMoveDialogue(clickedElement);
|
||||
this.executeCallback(SortableList.EVENTS.DRAGSTART);
|
||||
this.displayMoveDialogue(clickedElement);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -352,7 +344,7 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @param {jQuery} element
|
||||
* @returns {(Object|null)}
|
||||
*/
|
||||
var getPositionInNode = function(pageX, pageY, element) {
|
||||
SortableList.prototype.getPositionInNode = function(pageX, pageY, element) {
|
||||
if (!element.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -372,24 +364,14 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback for filter that checks that current element is not proxy
|
||||
*
|
||||
* @private
|
||||
* @return {boolean}
|
||||
*/
|
||||
var isNotProxy = function() {
|
||||
return !proxy || !proxy.length || this !== proxy[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if list is horizontal
|
||||
*
|
||||
* @param {jQuery} element
|
||||
* @return {Boolean}
|
||||
*/
|
||||
var isListHorizontal = function(element) {
|
||||
var isHorizontal = config.isHorizontal;
|
||||
SortableList.prototype.isListHorizontal = function(element) {
|
||||
var isHorizontal = this.config.isHorizontal;
|
||||
if (isHorizontal === true || isHorizontal === false) {
|
||||
return isHorizontal;
|
||||
}
|
||||
@@ -402,64 +384,69 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @private
|
||||
* @param {Event} evt
|
||||
*/
|
||||
var dragHandler = function(evt) {
|
||||
SortableList.prototype.dragHandler = function(evt) {
|
||||
|
||||
calculatePositionOnPage(evt);
|
||||
this.calculatePositionOnPage(evt);
|
||||
|
||||
// We can not use evt.target here because it will most likely be our proxy.
|
||||
// Move the proxy out of the way so we can find the element at the current mouse position.
|
||||
proxy.offset({top: -1000, left: -1000});
|
||||
this.proxy.offset({top: -1000, left: -1000});
|
||||
// Find the element at the current mouse position.
|
||||
var element = $(document.elementFromPoint(evt.clientX, evt.clientY));
|
||||
|
||||
// Find the list element and the list over the mouse position.
|
||||
var current = element.closest('.' + config.targetListClass + ' > :not(.' + config.isDraggedClass + ')'),
|
||||
currentList = element.closest('.' + config.targetListClass);
|
||||
var current = element.closest('.' + CSS.targetListClass + ' > :not(.' + CSS.isDraggedClass + ')'),
|
||||
currentList = element.closest('.' + CSS.targetListClass),
|
||||
proxy = this.proxy,
|
||||
isNotProxy = function() {
|
||||
return !proxy || !proxy.length || this !== proxy[0];
|
||||
};
|
||||
|
||||
// Add the specified class to the list element we are hovering.
|
||||
$('.' + config.overElementClass).removeClass(config.overElementClass);
|
||||
current.addClass(config.overElementClass);
|
||||
$('.' + CSS.overElementClass).removeClass(CSS.overElementClass);
|
||||
current.addClass(CSS.overElementClass);
|
||||
|
||||
// Move proxy to the current position.
|
||||
proxy.offset({top: proxyDelta.y + evt.pageY, left: proxyDelta.x + evt.pageX});
|
||||
this.proxy.offset({top: this.proxyDelta.y + evt.pageY, left: this.proxyDelta.x + evt.pageX});
|
||||
|
||||
if (currentList.length && !currentList.children().filter(isNotProxy).length) {
|
||||
// Mouse is over an empty list.
|
||||
moveElement(currentList, $());
|
||||
} else if (current.length === 1 && !info.element.find(current[0]).length) {
|
||||
this.moveElement(currentList, $());
|
||||
} else if (current.length === 1 && !this.info.element.find(current[0]).length) {
|
||||
// Mouse is over an element in a list - find whether we should move the current position
|
||||
// above or below this element.
|
||||
var coordinates = getPositionInNode(evt.pageX, evt.pageY, current);
|
||||
var coordinates = this.getPositionInNode(evt.pageX, evt.pageY, current);
|
||||
if (coordinates) {
|
||||
var parent = current.parent(),
|
||||
ratio = isListHorizontal(parent) ? coordinates.xRatio : coordinates.yRatio,
|
||||
subList = current.find('.' + config.targetListClass),
|
||||
ratio = this.isListHorizontal(parent) ? coordinates.xRatio : coordinates.yRatio,
|
||||
subList = current.find('.' + CSS.targetListClass),
|
||||
currentElement = this.info.element[0],
|
||||
isNotCurrent = function() {
|
||||
return this !== info.element[0];
|
||||
return this !== currentElement;
|
||||
},
|
||||
subListEmpty = !subList.children().filter(isNotProxy).filter(isNotCurrent).length;
|
||||
if (subList.length && subListEmpty && ratio > 0.2 && ratio < 0.8) {
|
||||
// This is an element that is a parent of an empty list and we are around the middle of this element.
|
||||
// Treat it as if we are over this empty list.
|
||||
moveElement(subList, $());
|
||||
this.moveElement(subList, $());
|
||||
} else if (ratio > 0.5) {
|
||||
// Insert after this element.
|
||||
moveElement(parent, current.next().filter(isNotProxy));
|
||||
this.moveElement(parent, current.next().filter(isNotProxy));
|
||||
} else {
|
||||
// Insert before this element.
|
||||
moveElement(parent, current);
|
||||
this.moveElement(parent, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evt.type === 'mouseup' || evt.type === 'touchend') {
|
||||
// Drop the moved element.
|
||||
info.endX = evt.pageX;
|
||||
info.endY = evt.pageY;
|
||||
info.endTime = new Date().getTime();
|
||||
info.dropped = true;
|
||||
executeCallback('drop');
|
||||
finishDragging();
|
||||
this.info.endX = evt.pageX;
|
||||
this.info.endY = evt.pageY;
|
||||
this.info.endTime = new Date().getTime();
|
||||
this.info.dropped = true;
|
||||
this.executeCallback(SortableList.EVENTS.DROP);
|
||||
this.finishDragging();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -470,15 +457,15 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @param {jQuery} parentElement
|
||||
* @param {jQuery} beforeElement
|
||||
*/
|
||||
var moveElement = function(parentElement, beforeElement) {
|
||||
var dragEl = info.element;
|
||||
SortableList.prototype.moveElement = function(parentElement, beforeElement) {
|
||||
var dragEl = this.info.element;
|
||||
if (beforeElement.length && beforeElement[0] === dragEl[0]) {
|
||||
// Insert before the current position of the dragged element - nothing to do.
|
||||
return;
|
||||
}
|
||||
if (parentElement[0] === info.targetList[0] &&
|
||||
beforeElement.length === info.targetNextElement.length &&
|
||||
beforeElement[0] === info.targetNextElement[0]) {
|
||||
if (parentElement[0] === this.info.targetList[0] &&
|
||||
beforeElement.length === this.info.targetNextElement.length &&
|
||||
beforeElement[0] === this.info.targetNextElement[0]) {
|
||||
// Insert in the same location as the current position - nothing to do.
|
||||
return;
|
||||
}
|
||||
@@ -486,34 +473,34 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
if (beforeElement.length) {
|
||||
// Move the dragged element before the specified element.
|
||||
parentElement[0].insertBefore(dragEl[0], beforeElement[0]);
|
||||
} else if (proxy && proxy.parent().length && proxy.parent()[0] === parentElement[0]) {
|
||||
} else if (this.proxy && this.proxy.parent().length && this.proxy.parent()[0] === parentElement[0]) {
|
||||
// We need to move to the end of the list but the last element in this list is a proxy.
|
||||
// Always leave the proxy in the end of the list.
|
||||
parentElement[0].insertBefore(dragEl[0], proxy[0]);
|
||||
parentElement[0].insertBefore(dragEl[0], this.proxy[0]);
|
||||
} else {
|
||||
// Insert in the end of a list (when proxy is in another list).
|
||||
parentElement[0].appendChild(dragEl[0]);
|
||||
}
|
||||
|
||||
// Save the current position of the dragged element in the list.
|
||||
info.targetList = parentElement;
|
||||
info.targetNextElement = beforeElement;
|
||||
executeCallback('drag');
|
||||
this.info.targetList = parentElement;
|
||||
this.info.targetNextElement = beforeElement;
|
||||
this.executeCallback(SortableList.EVENTS.DRAG);
|
||||
};
|
||||
|
||||
/**
|
||||
* Finish dragging (when dropped or cancelled).
|
||||
* @private
|
||||
*/
|
||||
var finishDragging = function() {
|
||||
resetDraggedClasses();
|
||||
if (config.autoScroll) {
|
||||
SortableList.prototype.finishDragging = function() {
|
||||
this.resetDraggedClasses();
|
||||
if (this.config.autoScroll) {
|
||||
autoScroll.stop();
|
||||
}
|
||||
$('body').off('mousemove touchmove mouseup touchend', dragHandler);
|
||||
$('body').off('keypress', dragcancelHandler);
|
||||
executeCallback('dragend');
|
||||
info = null;
|
||||
$(window).off('mousemove touchmove mouseup touchend', $.proxy(this.dragHandler, this));
|
||||
$(window).off('keypress', $.proxy(this.dragcancelHandler, this));
|
||||
this.executeCallback(SortableList.EVENTS.DRAGEND);
|
||||
this.info = null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -522,8 +509,8 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @private
|
||||
* @param {String} eventName
|
||||
*/
|
||||
var executeCallback = function(eventName) {
|
||||
info.element.trigger('sortablelist-' + eventName, info);
|
||||
SortableList.prototype.executeCallback = function(eventName) {
|
||||
this.info.element.trigger(eventName, this.info);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -532,41 +519,25 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @private
|
||||
* @param {Event} evt
|
||||
*/
|
||||
var dragcancelHandler = function(evt) {
|
||||
SortableList.prototype.dragcancelHandler = function(evt) {
|
||||
if (evt.type !== 'keypress' || evt.originalEvent.keyCode !== 27) {
|
||||
// Only cancel dragging when Esc was pressed.
|
||||
return;
|
||||
}
|
||||
// Dragging was cancelled. Return item to the original position.
|
||||
moveElement(info.sourceList, info.sourceNextElement);
|
||||
finishDragging();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method to convert a string to a promise
|
||||
*
|
||||
* @private
|
||||
* @param {(String|Promise)} value
|
||||
* @return {Promise}
|
||||
*/
|
||||
var convertToPromise = function(value) {
|
||||
var p = value;
|
||||
if (typeof value !== 'object' || !value.hasOwnProperty('then')) {
|
||||
p = $.Deferred();
|
||||
p.resolve(value);
|
||||
}
|
||||
return p;
|
||||
this.moveElement(this.info.sourceList, this.info.sourceNextElement);
|
||||
this.finishDragging();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the name of the current element to be used in the move dialogue
|
||||
*
|
||||
* @private
|
||||
* @public
|
||||
* @param {jQuery} element
|
||||
* @return {Promise}
|
||||
*/
|
||||
var getElementName = function(element) {
|
||||
return convertToPromise(config.elementNameCallback(element));
|
||||
SortableList.prototype.getElementName = function(element) {
|
||||
return $.Deferred().resolve(element.text());
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -574,24 +545,33 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
*
|
||||
* Note that we use "after" in the label for better UX
|
||||
*
|
||||
* @private
|
||||
* @public
|
||||
* @param {jQuery} parentElement
|
||||
* @param {jQuery} afterElement
|
||||
* @return {Promise}
|
||||
*/
|
||||
var getDestinationName = function(parentElement, afterElement) {
|
||||
return convertToPromise(config.destinationNameCallback(parentElement, afterElement));
|
||||
SortableList.prototype.getDestinationName = function(parentElement, afterElement) {
|
||||
if (!afterElement.length) {
|
||||
return str.get_string('movecontenttothetop', 'moodle');
|
||||
} else {
|
||||
return this.getElementName(afterElement)
|
||||
.then(function(name) {
|
||||
return str.get_string('movecontentafter', 'moodle', name);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the title for the move dialogue ("Move elementY")
|
||||
*
|
||||
* @private
|
||||
* @public
|
||||
* @param {jQuery} element
|
||||
* @return {Promise}
|
||||
*/
|
||||
var getMoveDialogueTitle = function(element) {
|
||||
return convertToPromise(config.moveDialogueTitleCallback(element));
|
||||
SortableList.prototype.getMoveDialogueTitle = function(element) {
|
||||
return this.getElementName(element).then(function(name) {
|
||||
return str.get_string('movecontent', 'moodle', name);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -600,28 +580,33 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @private
|
||||
* @return {Promise}
|
||||
*/
|
||||
var getDestinationsList = function() {
|
||||
SortableList.prototype.getDestinationsList = function() {
|
||||
var addedLists = [],
|
||||
targets = $(config.targetListSelector),
|
||||
destinations = $('<ul/>').addClass(config.keyboardDragClass),
|
||||
targets = $(this.config.targetListSelector),
|
||||
destinations = $('<ul/>').addClass(CSS.keyboardDragClass),
|
||||
result = $.when().then(function() {
|
||||
return destinations;
|
||||
}),
|
||||
createLink = function(parentElement, beforeElement, afterElement) {
|
||||
if (beforeElement.is(info.element) || afterElement.is(info.element)) {
|
||||
createLink = $.proxy(function(parentElement, beforeElement, afterElement) {
|
||||
if (beforeElement.is(this.info.element) || afterElement.is(this.info.element)) {
|
||||
// Can not move before or after itself.
|
||||
return;
|
||||
}
|
||||
if ($.contains(this.info.element[0], parentElement[0])) {
|
||||
// Can not move to its own child.
|
||||
return;
|
||||
}
|
||||
result = result
|
||||
.then(function() {
|
||||
return getDestinationName(parentElement, afterElement);
|
||||
})
|
||||
.then($.proxy(function() {
|
||||
return this.getDestinationName(parentElement, afterElement);
|
||||
}, this))
|
||||
.then(function(txt) {
|
||||
var li = $('<li/>').appendTo(destinations);
|
||||
var a = $('<a href="#"/>').attr('data-sortable-quickmove', 1).appendTo(li);
|
||||
var a = $('<a href="#"/>').attr('data-core_sortable_list-quickmove', 1).appendTo(li);
|
||||
a.data('parent-element', parentElement).data('before-element', beforeElement).text(txt);
|
||||
return destinations;
|
||||
});
|
||||
},
|
||||
}, this),
|
||||
addList = function() {
|
||||
// Destination lists may be nested. We want to add all move destinations in the same
|
||||
// order they appear on the screen for the user.
|
||||
@@ -648,82 +633,35 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
* @param {jQuery} clickedElement element to return focus to after the modal is closed
|
||||
* @private
|
||||
*/
|
||||
var displayMoveDialogue = function(clickedElement) {
|
||||
SortableList.prototype.displayMoveDialogue = function(clickedElement) {
|
||||
ModalFactory.create({
|
||||
type: ModalFactory.types.CANCEL,
|
||||
title: getMoveDialogueTitle(info.element),
|
||||
body: getDestinationsList()
|
||||
}).then(function(modal) {
|
||||
var quickMoveHandler = function(e) {
|
||||
title: this.getMoveDialogueTitle(this.info.element),
|
||||
body: this.getDestinationsList()
|
||||
}).then($.proxy(function(modal) {
|
||||
var quickMoveHandler = $.proxy(function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
moveElement($(e.currentTarget).data('parent-element'), $(e.currentTarget).data('before-element'));
|
||||
info.endTime = new Date().getTime();
|
||||
info.dropped = true;
|
||||
this.moveElement($(e.currentTarget).data('parent-element'), $(e.currentTarget).data('before-element'));
|
||||
this.info.endTime = new Date().getTime();
|
||||
this.info.dropped = true;
|
||||
clickedElement.focus();
|
||||
executeCallback('drop');
|
||||
this.executeCallback(SortableList.EVENTS.DROP);
|
||||
modal.hide();
|
||||
};
|
||||
modal.getRoot().on('click', '[data-sortable-quickmove]', quickMoveHandler);
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
}, this);
|
||||
modal.getRoot().on('click', '[data-core_sortable_list-quickmove]', quickMoveHandler);
|
||||
modal.getRoot().on(ModalEvents.hidden, $.proxy(function() {
|
||||
// Always destroy when hidden, it is generated dynamically each time.
|
||||
modal.getRoot().off('click', '[data-sortable-quickmove]', quickMoveHandler);
|
||||
modal.getRoot().off('click', '[data-core_sortable_list-quickmove]', quickMoveHandler);
|
||||
modal.destroy();
|
||||
finishDragging();
|
||||
});
|
||||
this.finishDragging();
|
||||
}, this));
|
||||
modal.setLarge();
|
||||
modal.show();
|
||||
return modal;
|
||||
}).catch(Notification.exception);
|
||||
}, this)).catch(Notification.exception);
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Initialise sortable list.
|
||||
*
|
||||
* @param {(String|jQuery|Element)} root JQuery/DOM element representing sortable list (i.e. <ul>, <tbody>) or CSS selector
|
||||
* @param {Object} config Parameters for the list. See defaultParameters above for examples.
|
||||
* @property {(String|jQuery|Element)} config.targetListSelector target lists, by default same as root
|
||||
* @property {String} config.moveHandlerSelector CSS selector for a drag handle. By default the whole item is a handle.
|
||||
* Without drag handle sorting is not accessible!
|
||||
* @property {(Boolean|Function)} config.isHorizontal Set to true if the list is horizontal
|
||||
* (can also be a callback with list as an argument)
|
||||
* @property {Boolean} config.autoScroll Engages autoscroll module for automatic vertical scrolling of the
|
||||
* whole page, by default true
|
||||
* @property {Function} config.elementNameCallback Should return a string or Promise. Used for move dialogue title and
|
||||
* destination name
|
||||
* @property {Function} config.destinationNameCallback Callback that returns a string or Promise with the label
|
||||
* for the move destination
|
||||
* @property {Function} config.moveDialogueTitleCallback Should return a string or Promise. Used to form move dialogue title
|
||||
* @property {String} config.keyboardDragClass Class of the list of destinations in the popup
|
||||
* (default 'dragdrop-keyboard-drag')
|
||||
* @property {String} config.isDraggedClass Class added to the element that is dragged
|
||||
* (default 'sortable-list-is-dragged')
|
||||
* @property {String} config.currentPositionClass Class added to the current position of a dragged element
|
||||
* (default 'sortable-list-current-position')
|
||||
* @property {String} config.sourceListClass Class added to the list where dragging was started from
|
||||
* (default 'sortable-list-source')
|
||||
* @property {String} config.targetListClass Class added to all lists where item can be dropped
|
||||
* (default 'sortable-list-target')
|
||||
* @property {String} config.overElementClass Class added to the list element when the dragged element is above it
|
||||
* (default 'sortable-list-over-element')
|
||||
*/
|
||||
init: function(root, config) {
|
||||
config = $.extend({}, defaultParameters, CSS, config || {});
|
||||
config.listSelector = root;
|
||||
if (!config.targetListSelector) {
|
||||
config.targetListSelector = root;
|
||||
}
|
||||
if (typeof config.listSelector === 'object') {
|
||||
// The root is an element on the page. Register a listener for this element.
|
||||
$(config.listSelector).on('mousedown touchstart', {config: config}, dragStartHandler);
|
||||
} else {
|
||||
// The root is a CSS selector. Register a listener that picks up the element dynamically.
|
||||
$('body').on('mousedown touchstart', config.listSelector, {config: config}, dragStartHandler);
|
||||
}
|
||||
if (config.moveHandlerSelector !== null) {
|
||||
$('body').on('click keypress', config.moveHandlerSelector, {config: config}, clickHandler);
|
||||
}
|
||||
}
|
||||
};
|
||||
return SortableList;
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user