Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

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