AutorÃa | Ultima modificación | Ver Log |
// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./** To make a set of things draggable, create a new instance of this object passing the* necessary config, as explained in the comment on the constructor.** @package qtype_ordering\drag_reorder* @copyright 2018 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/'use strict';import $ from 'jquery';import drag from 'core/dragdrop';import Templates from 'core/templates';import Notification from 'core/notification';import {getString} from 'core/str';import {prefetchString} from 'core/prefetch';export default class DragReorder {// Class variables handling state.config = {reorderStart: undefined, reorderEnd: undefined}; // Config object with some basic definitions.dragStart = null; // Information about when and where the drag started.originalOrder = null; // Array of ids that's used to compare the state after the drag event finishes.// DOM Nodes and jQuery representations.orderList = null; // Order list (HTMLElement).itemDragging = null; // Item being moved by dragging (jQuery object).proxy = null; // Drag proxy (jQuery object)./*** Constructor.** To make a list draggable, create a new instance of this object, passing the necessary config.* For example:* {* // Selector for the list (or lists) to be reordered.* list: 'ul.my-list',** // Selector, relative to the list selector, for the items that can be moved.* item: '> li',** // While the proxy is being dragged, this class is added to the item being moved.* // You can probably use "osep-itemmoving" here.* itemMovingClass: "osep-itemmoving",** // This is a callback which, when called with the DOM node for an item,* // returns the string that uniquely identifies each item.* // Therefore, the result of the drag action will be represented by the array* // obtained by calling this method on each item in the list in order.* idGetter: function(item) { return node.id; },** // Function that will be called when a re-order starts (optional, can be not set).* // Useful if you need to save information about the initial state.* // This function should have two parameters. The first will be a* // jQuery object for the list that was reordered, the second will* // be the jQuery object for the item moved - which will not yet have been moved.* // Note, it is quite possible for reorderStart to be called with no* // subsequent call to reorderDone.* reorderStart: function($list, $item) { ... }** // Function that will be called when a drag has finished, and the list* // has been reordered. This function should have three parameters. The first will be* // a jQuery object for the list that was reordered, the second will be the jQuery* // object for the item moved, and the third will be the new order, which is* // an array of ids obtained by calling idGetter on each item in the list in order.* // This callback will only be called in the new order is actually different from the old order.* reorderDone: function($list, $item, newOrder) { ... }** // Function that is always called when a re-order ends (optional, can be not set)* // whether the order has changed. Useful if you need to undo changes made* // in reorderStart, since reorderDone is only called if the new order is different* // from the original order.* reorderEnd: function($list, $item) { ... }* }** There is a subtlety, If you have items in your list that do not have a drag handle,* they are considered to be placeholders in otherwise empty containers.** @param {Object} config As above.*/constructor(config) {// Bring in the config to our state.this.config = config;// Get the list we'll be working with this time.this.orderList = document.querySelector(this.config.list);this.startListeners();}/*** Start the listeners for the list.*/startListeners() {/*** Handle mousedown or touchstart events on the list.** @param {Event} e The event.*/const pointerHandle = e => {if (e.target.closest(this.config.item) && !e.target.closest(this.config.actionButton)) {this.itemDragging = $(e.target.closest(this.config.item));const details = drag.prepare(e);if (details.start) {this.startDrag(e, details);}}};// Set up the list listeners for moving list items around.this.orderList.addEventListener('mousedown', pointerHandle);this.orderList.addEventListener('touchstart', pointerHandle);this.orderList.addEventListener('click', this.itemMovedByClick.bind(this));}/*** Start dragging.** @param {Event} e The event which is either mousedown or touchstart.* @param {Object} details Object with start (boolean flag) and x, y (only if flag true) values*/startDrag(e, details) {this.dragStart = {time: new Date().getTime(),x: details.x,y: details.y};if (typeof this.config.reorderStart !== 'undefined') {this.config.reorderStart(this.itemDragging.closest(this.config.list), this.itemDragging);}this.originalOrder = this.getCurrentOrder();Templates.renderForPromise('qtype_ordering/proxyhtml', {itemHtml: this.itemDragging.html(),itemClassName: this.itemDragging.attr('class'),listClassName: this.orderList.classList.toString(),proxyStyles: [`width: ${this.itemDragging.outerWidth()}px;`,`height: ${this.itemDragging.outerHeight()}px;`,].join(' '),}).then(({html, js}) => {this.proxy = $(Templates.appendNodeContents(document.body, html, js)[0]);this.proxy.css(this.itemDragging.offset());this.itemDragging.addClass(this.config.itemMovingClass);this.updateProxy();// Start drag.drag.start(e, this.proxy, this.dragMove.bind(this), this.dragEnd.bind(this));}).catch(Notification.exception);}/*** Move the proxy to the current mouse position.*/dragMove() {let closestItem = null;let closestDistance = null;this.orderList.querySelectorAll(this.config.item).forEach(element => {const distance = this.distanceBetweenElements(element);if (closestItem === null || distance < closestDistance) {closestItem = $(element);closestDistance = distance;}});if (closestItem[0] === this.itemDragging[0]) {return;}// Set offset depending on if item is being dragged downwards/upwards.const offsetValue = this.midY(this.proxy) < this.midY(closestItem) ? 20 : -20;if (this.midY(this.proxy) + offsetValue < this.midY(closestItem)) {this.itemDragging.insertBefore(closestItem);} else {this.itemDragging.insertAfter(closestItem);}this.updateProxy();}/*** Update proxy's position.*/updateProxy() {const items = [...this.orderList.querySelectorAll(this.config.item)];for (let i = 0; i < items.length; ++i) {if (this.itemDragging[0] === items[i]) {this.proxy.find('li').attr('value', i + 1);break;}}}/*** End dragging.*/dragEnd() {if (typeof this.config.reorderEnd !== 'undefined') {this.config.reorderEnd(this.itemDragging.closest(this.config.list), this.itemDragging);}if (!this.arrayEquals(this.originalOrder, this.getCurrentOrder())) {// Order has changed, call the callback.this.config.reorderDone(this.itemDragging.closest(this.config.list), this.itemDragging, this.getCurrentOrder());getString('moved', 'qtype_ordering', {item: this.itemDragging.find('[data-itemcontent]').text().trim(),position: this.itemDragging.index() + 1,total: this.orderList.querySelectorAll(this.config.item).length}).then((str) => {this.config.announcementRegion.innerHTML = str;});}// Clean up after the drag is finished.this.proxy.remove();this.proxy = null;this.itemDragging.removeClass(this.config.itemMovingClass);this.itemDragging = null;this.dragStart = null;}/*** Handles the movement of an item by click.** @param {MouseEvent} e The pointer event.*/itemMovedByClick(e) {const actionButton = e.target.closest(this.config.actionButton);if (actionButton) {this.itemDragging = $(e.target.closest(this.config.item));// Store the current state of the list.this.originalOrder = this.getCurrentOrder();switch (actionButton.dataset.action) {case 'move-backward':e.preventDefault();e.stopPropagation();if (this.itemDragging.prev().length) {this.itemDragging.prev().insertAfter(this.itemDragging);}break;case 'move-forward':e.preventDefault();e.stopPropagation();if (this.itemDragging.next().length) {this.itemDragging.next().insertBefore(this.itemDragging);}break;}// After we have potentially moved the item, we need to check if the order has changed.if (!this.arrayEquals(this.originalOrder, this.getCurrentOrder())) {// Order has changed, call the callback.this.config.reorderDone(this.itemDragging.closest(this.config.list), this.itemDragging, this.getCurrentOrder());// When moving an item to the first or last position, the button that was clicked will be hidden.// In this case, we need to focus the other button.if (!this.itemDragging.prev().length) {// Focus the 'next' action button.this.itemDragging.find('[data-action="move-forward"]').focus();} else if (!this.itemDragging.next().length) {// Focus the 'previous' action button.this.itemDragging.find('[data-action="move-backward"]').focus();}getString('moved', 'qtype_ordering', {item: this.itemDragging.find('[data-itemcontent]').text().trim(),position: this.itemDragging.index() + 1,total: this.orderList.querySelectorAll(this.config.item).length}).then((str) => {this.config.announcementRegion.innerHTML = str;});}}}/*** Get the x-position of the middle of the DOM node represented by the given jQuery object.** @param {jQuery} node jQuery wrapping a DOM node.* @returns {number} Number the x-coordinate of the middle (left plus half outerWidth).*/midX(node) {return node.offset().left + node.outerWidth() / 2;}/*** Get the y-position of the middle of the DOM node represented by the given jQuery object.** @param {jQuery} node jQuery wrapped DOM node.* @returns {number} Number the y-coordinate of the middle (top plus half outerHeight).*/midY(node) {return node.offset().top + node.outerHeight() / 2;}/*** Calculate the distance between the centres of two elements.** @param {HTMLLIElement} element DOM node of a list item.* @return {number} number the distance in pixels.*/distanceBetweenElements(element) {const [e1, e2] = [$(element), $(this.proxy)];const [dx, dy] = [this.midX(e1) - this.midX(e2), this.midY(e1) - this.midY(e2)];return Math.sqrt(dx * dx + dy * dy);}/*** Get the current order of the list containing itemDragging.** @returns {Array} Array of strings, the id of each element in order.*/getCurrentOrder() {return this.itemDragging.closest(this.config.list).find(this.config.item).map((index, item) => {return this.config.idGetter(item);}).get();}/*** Compare two arrays which contain primitive types to see if they are equal.* @param {Array} a1 first array.* @param {Array} a2 second array.* @return {Boolean} boolean true if they both contain the same elements in the same order, else false.*/arrayEquals(a1, a2) {return a1.length === a2.length &&a1.every((v, i) => {return v === a2[i];});}/*** Initialise one ordering question.** @param {String} sortableid id of ul for this question.* @param {String} responseid id of hidden field for this question.*/static init(sortableid, responseid) {new DragReorder({actionButton: '[data-action]',announcementRegion: document.querySelector(`#${sortableid}-announcement`),list: 'ul#' + sortableid,item: 'li.sortableitem',itemMovingClass: "current-drop",idGetter: item => {return item.id;},reorderDone: (list, item, newOrder) => {$('input#' + responseid)[0].value = newOrder.join(',');}});prefetchString('qtype_ordering', 'moved');}}