Rev 1 | AutorÃa | Comparar con el anterior | 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/>./*** JavaScript to make drag-drop into text questions work.** Some vocabulary to help understand this code:** The question text contains 'drops' - blanks into which the 'drags', the missing* words, can be put.** The thing that can be moved into the drops are called 'drags'. There may be* multiple copies of the 'same' drag which does not really cause problems.* Each drag has a 'choice' number which is the value set on the drop's hidden* input when this drag is placed in a drop.** These may be in separate 'groups', distinguished by colour.* Things can only interact with other things in the same group.* The groups are numbered from 1.** The place where a given drag started from is called its 'home'.** @module qtype_ddwtos/ddwtos* @copyright 2018 The Open University* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later* @since 3.6*/define(['jquery','core/dragdrop','core/key_codes','core_form/changechecker','core_filters/events',], function($,dragDrop,keys,FormChangeChecker,filterEvent) {"use strict";/*** Object to handle one drag-drop into text question.** @param {String} containerId id of the outer div for this question.* @param {boolean} readOnly whether the question is being displayed read-only.* @constructor*/function DragDropToTextQuestion(containerId, readOnly) {const thisQ = this;this.containerId = containerId;this.questionAnswer = {};this.questionDragDropWidthHeight = [];if (readOnly) {this.getRoot().addClass('qtype_ddwtos-readonly');}this.resizeAllDragsAndDrops();this.cloneDrags();this.positionDrags();// Wait for all dynamic content loaded by filter to be completed.document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => {elements.detail.nodes.forEach((element) => {thisQ.changeAllDragsAndDropsToFilteredContent(element);});});}/*** In each group, resize all the items to be the same size.*/DragDropToTextQuestion.prototype.resizeAllDragsAndDrops = function() {var thisQ = this;this.getRoot().find('.answercontainer > div').each(function(i, node) {thisQ.resizeAllDragsAndDropsInGroup(thisQ.getClassnameNumericSuffix($(node), 'draggrouphomes'));});};/*** In a given group, set all the drags and drops to be the same size.** @param {int} group the group number.*/DragDropToTextQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {var thisQ = this,dragDropItems = this.getRoot().find('span.group' + group),maxWidth = 0,maxHeight = 0;// Find the maximum size of any drag in this groups.dragDropItems.each(function(i, drag) {maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));maxHeight = Math.max(maxHeight, Math.ceil(0 + drag.offsetHeight));});// The size we will want to set is a bit bigger than this.maxWidth += 8;maxHeight += 2;thisQ.questionDragDropWidthHeight[group] = {maxWidth: maxWidth, maxHeight: maxHeight};// Set each drag home to that size.dragDropItems.each(function(i, drag) {thisQ.setElementSize(drag, maxWidth, maxHeight);});};/*** Change all the drags and drops related to the item that has been changed by filter to correct size and content.** @param {object} filteredElement the element has been modified by filter.*/DragDropToTextQuestion.prototype.changeAllDragsAndDropsToFilteredContent = function(filteredElement) {let currentFilteredItem = $(filteredElement);const parentIsDD = currentFilteredItem.parent().closest('span').hasClass('placed') ||currentFilteredItem.parent().closest('span').hasClass('draghome');const isDD = currentFilteredItem.hasClass('placed') || currentFilteredItem.hasClass('draghome');// The filtered element or parent element should a drag or drop item.if (!parentIsDD && !isDD) {return;}if (parentIsDD) {currentFilteredItem = currentFilteredItem.parent().closest('span');}const thisQ = this;if (thisQ.getRoot().find(currentFilteredItem).length <= 0) {// If the DD item doesn't belong to this question// In case we have multiple questions in the same page.return;}const group = thisQ.getGroup(currentFilteredItem),choice = thisQ.getChoice(currentFilteredItem);let listOfModifiedDragDrop = [];// Get the list of drag and drop item within the same group and choice.this.getRoot().find('.group' + group + '.choice' + choice).each(function(i, node) {// Same modified item, skip it.if ($(node).get(0) === currentFilteredItem.get(0)) {return;}const originalClass = $(node).attr('class');const originalStyle = $(node).attr('style');// We want to keep all the handler and event for filtered item, so using clone is the only choice.const filteredDragDropClone = currentFilteredItem.clone();// Replace the class and style of the drag drop item we want to replace for the clone.filteredDragDropClone.attr('class', originalClass);filteredDragDropClone.attr('style', originalStyle);// Insert into DOM.$(node).before(filteredDragDropClone);// Add the item has been replaced to a list so we can remove it later.listOfModifiedDragDrop.push(node);});listOfModifiedDragDrop.forEach(function(node) {$(node).remove();});// Save the current height and width.const currentHeight = currentFilteredItem.height();const currentWidth = currentFilteredItem.width();// Set to auto so we can get the real height and width of the filtered item.currentFilteredItem.height('auto');currentFilteredItem.width('auto');// We need to set display block so we can get height and width.// Some browser can't get the offsetWidth/Height if they are an inline element like span tag.if (!filteredElement.offsetWidth || !filteredElement.offsetHeight) {filteredElement.classList.add('d-block');}if (thisQ.questionDragDropWidthHeight[group].maxWidth < Math.ceil(filteredElement.offsetWidth) ||thisQ.questionDragDropWidthHeight[group].maxHeight < Math.ceil(0 + filteredElement.offsetHeight)) {// Remove the d-block class before calculation.filteredElement.classList.remove('d-block');// Now resize all the items in the same group if we have new maximum width or height.thisQ.resizeAllDragsAndDropsInGroup(group);} else {// Return the original height and width in case the real height and width is not the maximum.currentFilteredItem.height(currentHeight);currentFilteredItem.width(currentWidth);}// Remove the d-block class after resize.filteredElement.classList.remove('d-block');};/*** Set a given DOM element to be a particular size.** @param {HTMLElement} element* @param {int} width* @param {int} height*/DragDropToTextQuestion.prototype.setElementSize = function(element, width, height) {$(element).width(width).height(height).css('lineHeight', height + 'px');};/*** Invisible 'drag homes' are output by the renderer. These have the same properties* as the drag items but are invisible. We clone these invisible elements to make the* actual drag items.*/DragDropToTextQuestion.prototype.cloneDrags = function() {var thisQ = this;thisQ.getRoot().find('span.draghome').each(function(index, draghome) {var drag = $(draghome);var placeHolder = drag.clone();placeHolder.removeClass();placeHolder.addClass('draghome choice' +thisQ.getChoice(drag) + ' group' +thisQ.getGroup(drag) + ' dragplaceholder');drag.before(placeHolder);});};/*** Update the position of drags.*/DragDropToTextQuestion.prototype.positionDrags = function() {var thisQ = this,root = this.getRoot();// First move all items back home.root.find('span.draghome').not('.dragplaceholder').each(function(i, dragNode) {var drag = $(dragNode),currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');drag.addClass('unplaced').removeClass('placed');drag.removeAttr('tabindex');if (currentPlace !== null) {drag.removeClass('inplace' + currentPlace);}});// Then place the once that should be placed.root.find('input.placeinput').each(function(i, inputNode) {var input = $(inputNode),choice = input.val(),place = thisQ.getPlace(input);// Record the last known position of the drop.var drop = root.find('.drop.place' + place),dropPosition = drop.offset();drop.data('prev-top', dropPosition.top).data('prev-left', dropPosition.left);if (choice === '0') {// No item in this place.return;}// Get the unplaced drag.var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);// Get the clone of the drag.var hiddenDrag = thisQ.getDragClone(unplacedDrag);if (hiddenDrag.length) {if (unplacedDrag.hasClass('infinite')) {var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);if (cloneDrags.length < noOfDrags) {var cloneDrag = unplacedDrag.clone();hiddenDrag.after(cloneDrag);questionManager.addEventHandlersToDrag(cloneDrag);} else {hiddenDrag.addClass('active');}} else {hiddenDrag.addClass('active');}}// Send the drag to drop.thisQ.sendDragToDrop(thisQ.getUnplacedChoice(thisQ.getGroup(input), choice), drop);});// Save the question answer.thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();};/*** Get the question answered values.** @return {Object} Contain key-value with key is the input id and value is the input value.*/DragDropToTextQuestion.prototype.getQuestionAnsweredValues = function() {let result = {};this.getRoot().find('input.placeinput').each((i, inputNode) => {result[inputNode.id] = inputNode.value;});return result;};/*** Check if the question is being interacted or not.** @return {boolean} Return true if the user has changed the question-answer.*/DragDropToTextQuestion.prototype.isQuestionInteracted = function() {const oldAnswer = this.questionAnswer;const newAnswer = this.getQuestionAnsweredValues();let isInteracted = false;// First, check both answers have the same structure or not.if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {isInteracted = true;return isInteracted;}// Check the values.Object.keys(newAnswer).forEach(key => {if (newAnswer[key] !== oldAnswer[key]) {isInteracted = true;}});return isInteracted;};/*** Handles the start of dragging an item.** @param {Event} e the touch start or mouse down event.*/DragDropToTextQuestion.prototype.handleDragStart = function(e) {var thisQ = this,drag = $(e.target).closest('.draghome');var info = dragDrop.prepare(e);if (!info.start || drag.hasClass('beingdragged')) {return;}drag.addClass('beingdragged');var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');if (currentPlace !== null) {this.setInputValue(currentPlace, 0);drag.removeClass('inplace' + currentPlace);var hiddenDrop = thisQ.getDrop(drag, currentPlace);if (hiddenDrop.length) {hiddenDrop.addClass('active');drag.offset(hiddenDrop.offset());}} else {var hiddenDrag = thisQ.getDragClone(drag);if (hiddenDrag.length) {if (drag.hasClass('infinite')) {var noOfDrags = this.noOfDropsInGroup(this.getGroup(drag));var cloneDrags = this.getInfiniteDragClones(drag, false);if (cloneDrags.length < noOfDrags) {var cloneDrag = drag.clone();cloneDrag.removeClass('beingdragged');hiddenDrag.after(cloneDrag);questionManager.addEventHandlersToDrag(cloneDrag);drag.offset(cloneDrag.offset());} else {hiddenDrag.addClass('active');drag.offset(hiddenDrag.offset());}} else {hiddenDrag.addClass('active');drag.offset(hiddenDrag.offset());}}}dragDrop.start(e, drag, function(x, y, drag) {thisQ.dragMove(x, y, drag);}, function(x, y, drag) {thisQ.dragEnd(x, y, drag);});};/*** Called whenever the currently dragged items moves.** @param {Number} pageX the x position.* @param {Number} pageY the y position.* @param {jQuery} drag the item being moved.*/DragDropToTextQuestion.prototype.dragMove = function(pageX, pageY, drag) {var thisQ = this;this.getRoot().find('span.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {var drop = $(dropNode);if (thisQ.isPointInDrop(pageX, pageY, drop)) {drop.addClass('valid-drag-over-drop');} else {drop.removeClass('valid-drag-over-drop');}});};/*** Called when user drops a drag item.** @param {Number} pageX the x position.* @param {Number} pageY the y position.* @param {jQuery} drag the item being moved.*/DragDropToTextQuestion.prototype.dragEnd = function(pageX, pageY, drag) {var thisQ = this,root = this.getRoot(),placed = false;root.find('span.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {if (placed) {return false;}const dropZone = $(dropNode);if (!thisQ.isPointInDrop(pageX, pageY, dropZone)) {// Not this drop zone.return true;}let drop = null;if (dropZone.hasClass('placed')) {// This is an placed drag item in a drop.dropZone.removeClass('valid-drag-over-drop');// Get the correct drop.drop = thisQ.getDrop(drag, thisQ.getClassnameNumericSuffix(dropZone, 'inplace'));} else {// Empty drop.drop = dropZone;}// Now put this drag into the drop.drop.removeClass('valid-drag-over-drop');thisQ.sendDragToDrop(drag, drop);placed = true;return false; // Stop the each() here.});if (!placed) {this.sendDragHome(drag);}};/*** Animate a drag item into a given place (or back home).** @param {jQuery|null} drag the item to place. If null, clear the place.* @param {jQuery} drop the place to put it.*/DragDropToTextQuestion.prototype.sendDragToDrop = function(drag, drop) {// Send drag home if there is no place in drop.if (this.getPlace(drop) === null) {this.sendDragHome(drag);return;}// Is there already a drag in this drop? if so, evict it.var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));if (oldDrag.length !== 0) {var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');// When infinite group and there is already a drag in a drop, reject the exact clone in the same drop.if (this.hasDropSameDrag(currentPlace, drop, oldDrag, drag)) {this.sendDragHome(drag);return;}var hiddenDrop = this.getDrop(oldDrag, currentPlace);hiddenDrop.addClass('active');oldDrag.addClass('beingdragged');oldDrag.offset(hiddenDrop.offset());this.sendDragHome(oldDrag);}if (drag.length === 0) {this.setInputValue(this.getPlace(drop), 0);if (drop.data('isfocus')) {drop.focus();}} else {// Prevent the drag item drop into two drop-zone.if (this.getClassnameNumericSuffix(drag, 'inplace')) {return;}this.setInputValue(this.getPlace(drop), this.getChoice(drag));drag.removeClass('unplaced').addClass('placed inplace' + this.getPlace(drop));drag.attr('tabindex', 0);this.animateTo(drag, drop);}};/*** When infinite group and there is already a drag in a drop, reject the exact clone in the same drop.** @param {int} currentPlace the position of the current drop.* @param {jQuery} drop the drop containing a drag.* @param {jQuery} oldDrag the drag already placed in drop.* @param {jQuery} drag the new drag which is exactly the same (clone) as oldDrag .* @returns {boolean}*/DragDropToTextQuestion.prototype.hasDropSameDrag = function(currentPlace, drop, oldDrag, drag) {if (drag.hasClass('infinite')) {return drop.hasClass('place' + currentPlace) &&this.getGroup(drag) === this.getGroup(drop) &&this.getChoice(drag) === this.getChoice(oldDrag) &&this.getGroup(drag) === this.getGroup(oldDrag);}return false;};/*** Animate a drag back to its home.** @param {jQuery} drag the item being moved.*/DragDropToTextQuestion.prototype.sendDragHome = function(drag) {var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');if (currentPlace !== null) {drag.removeClass('inplace' + currentPlace);}drag.data('unplaced', true);this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));};/*** Handles keyboard events on drops.** Drops are focusable. Once focused, right/down/space switches to the next choice, and* left/up switches to the previous. Escape clear.** @param {KeyboardEvent} e*/DragDropToTextQuestion.prototype.handleKeyPress = function(e) {var drop = $(e.target).closest('.drop');if (drop.length === 0) {var placedDrag = $(e.target);var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');if (currentPlace !== null) {drop = this.getDrop(placedDrag, currentPlace);}}var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),nextDrag = $();switch (e.keyCode) {case keys.space:case keys.arrowRight:case keys.arrowDown:nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);break;case keys.arrowLeft:case keys.arrowUp:nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);break;case keys.escape:break;default:questionManager.isKeyboardNavigation = false;return; // To avoid the preventDefault below.}if (nextDrag.length) {nextDrag.data('isfocus', true);nextDrag.addClass('beingdragged');var hiddenDrag = this.getDragClone(nextDrag);if (hiddenDrag.length) {if (nextDrag.hasClass('infinite')) {var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));var cloneDrags = this.getInfiniteDragClones(nextDrag, false);if (cloneDrags.length < noOfDrags) {var cloneDrag = nextDrag.clone();cloneDrag.removeClass('beingdragged');cloneDrag.removeAttr('tabindex');hiddenDrag.after(cloneDrag);questionManager.addEventHandlersToDrag(cloneDrag);nextDrag.offset(cloneDrag.offset());} else {hiddenDrag.addClass('active');nextDrag.offset(hiddenDrag.offset());}} else {hiddenDrag.addClass('active');nextDrag.offset(hiddenDrag.offset());}}} else {drop.data('isfocus', true);}e.preventDefault();this.sendDragToDrop(nextDrag, drop);};/*** Choose the next drag in a group.** @param {int} group which group.* @param {jQuery} drag current choice (empty jQuery if there isn't one).* @return {jQuery} the next drag in that group, or null if there wasn't one.*/DragDropToTextQuestion.prototype.getNextDrag = function(group, drag) {var choice,numChoices = this.noOfChoicesInGroup(group);if (drag.length === 0) {choice = 1; // Was empty, so we want to select the first choice.} else {choice = this.getChoice(drag) + 1;}var next = this.getUnplacedChoice(group, choice);while (next.length === 0 && choice < numChoices) {choice++;next = this.getUnplacedChoice(group, choice);}return next;};/*** Choose the previous drag in a group.** @param {int} group which group.* @param {jQuery} drag current choice (empty jQuery if there isn't one).* @return {jQuery} the next drag in that group, or null if there wasn't one.*/DragDropToTextQuestion.prototype.getPreviousDrag = function(group, drag) {var choice;if (drag.length === 0) {choice = this.noOfChoicesInGroup(group);} else {choice = this.getChoice(drag) - 1;}var previous = this.getUnplacedChoice(group, choice);while (previous.length === 0 && choice > 1) {choice--;previous = this.getUnplacedChoice(group, choice);}// Does this choice exist?return previous;};/*** Animate an object to the given destination.** @param {jQuery} drag the element to be animated.* @param {jQuery} target element marking the place to move it to.*/DragDropToTextQuestion.prototype.animateTo = function(drag, target) {var currentPos = drag.offset(),targetPos = target.offset(),thisQ = this;M.util.js_pending('qtype_ddwtos-animate-' + thisQ.containerId);// Animate works in terms of CSS position, whereas locating an object// on the page works best with jQuery offset() function. So, to get// the right target position, we work out the required change in// offset() and then add that to the current CSS position.drag.animate({left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,top: parseInt(drag.css('top')) + targetPos.top - currentPos.top},{duration: 'fast',done: function() {$('body').trigger('qtype_ddwtos-dragmoved', [drag, target, thisQ]);M.util.js_complete('qtype_ddwtos-animate-' + thisQ.containerId);}});};/*** Detect if a point is inside a given DOM node.** @param {Number} pageX the x position.* @param {Number} pageY the y position.* @param {jQuery} drop the node to check (typically a drop).* @return {boolean} whether the point is inside the node.*/DragDropToTextQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {var position = drop.offset();return pageX >= position.left && pageX < position.left + drop.width()&& pageY >= position.top && pageY < position.top + drop.height();};/*** Set the value of the hidden input for a place, to record what is currently there.** @param {int} place which place to set the input value for.* @param {int} choice the value to set.*/DragDropToTextQuestion.prototype.setInputValue = function(place, choice) {this.getRoot().find('input.placeinput.place' + place).val(choice);};/*** Get the outer div for this question.** @returns {jQuery} containing that div.*/DragDropToTextQuestion.prototype.getRoot = function() {return $(document.getElementById(this.containerId));};/*** Get drag home for a given choice.** @param {int} group the group.* @param {int} choice the choice number.* @returns {jQuery} containing that div.*/DragDropToTextQuestion.prototype.getDragHome = function(group, choice) {if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {return this.getRoot().find('.draggrouphomes' + group +' span.draghome.infinite' +'.choice' + choice +'.group' + group);}return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);};/*** Get an unplaced choice for a particular group.** @param {int} group the group.* @param {int} choice the choice number.* @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.*/DragDropToTextQuestion.prototype.getUnplacedChoice = function(group, choice) {return this.getRoot().find('.draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);};/*** Get the drag that is currently in a given place.** @param {int} place the place number.* @return {jQuery} the current drag (or an empty jQuery if none).*/DragDropToTextQuestion.prototype.getCurrentDragInPlace = function(place) {return this.getRoot().find('span.draghome.inplace' + place);};/*** Return the number of blanks in a given group.** @param {int} group the group number.* @returns {int} the number of drops.*/DragDropToTextQuestion.prototype.noOfDropsInGroup = function(group) {return this.getRoot().find('.drop.group' + group).length;};/*** Return the number of choices in a given group.** @param {int} group the group number.* @returns {int} the number of choices.*/DragDropToTextQuestion.prototype.noOfChoicesInGroup = function(group) {return this.getRoot().find('.draghome.group' + group).length;};/*** Return the number at the end of the CSS class name with the given prefix.** @param {jQuery} node* @param {String} prefix name prefix* @returns {Number|null} the suffix if found, else null.*/DragDropToTextQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {var classes = node.attr('class');if (classes !== undefined && classes !== '') {var classesArr = classes.split(' ');for (var index = 0; index < classesArr.length; index++) {var patt1 = new RegExp('^' + prefix + '([0-9])+$');if (patt1.test(classesArr[index])) {var patt2 = new RegExp('([0-9])+$');var match = patt2.exec(classesArr[index]);return Number(match[0]);}}}return null;};/*** Get the choice number of a drag.** @param {jQuery} drag the drag.* @returns {Number} the choice number.*/DragDropToTextQuestion.prototype.getChoice = function(drag) {return this.getClassnameNumericSuffix(drag, 'choice');};/*** Given a DOM node that is significant to this question* (drag, drop, ...) get the group it belongs to.** @param {jQuery} node a DOM node.* @returns {Number} the group it belongs to.*/DragDropToTextQuestion.prototype.getGroup = function(node) {return this.getClassnameNumericSuffix(node, 'group');};/*** Get the place number of a drop, or its corresponding hidden input.** @param {jQuery} node the DOM node.* @returns {Number} the place number.*/DragDropToTextQuestion.prototype.getPlace = function(node) {return this.getClassnameNumericSuffix(node, 'place');};/*** Get drag clone for a given drag.** @param {jQuery} drag the drag.* @returns {jQuery} the drag's clone.*/DragDropToTextQuestion.prototype.getDragClone = function(drag) {return this.getRoot().find('.draggrouphomes' +this.getGroup(drag) +' span.draghome' +'.choice' + this.getChoice(drag) +'.group' + this.getGroup(drag) +'.dragplaceholder');};/*** Get infinite drag clones for given drag.** @param {jQuery} drag the drag.* @param {Boolean} inHome in the home area or not.* @returns {jQuery} the drag's clones.*/DragDropToTextQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {if (inHome) {return this.getRoot().find('.draggrouphomes' +this.getGroup(drag) +' span.draghome' +'.choice' + this.getChoice(drag) +'.group' + this.getGroup(drag) +'.infinite').not('.dragplaceholder');}return this.getRoot().find('span.draghome' +'.choice' + this.getChoice(drag) +'.group' + this.getGroup(drag) +'.infinite').not('.dragplaceholder');};/*** Get drop for a given drag and place.** @param {jQuery} drag the drag.* @param {Integer} currentPlace the current place of drag.* @returns {jQuery} the drop's clone.*/DragDropToTextQuestion.prototype.getDrop = function(drag, currentPlace) {return this.getRoot().find('.drop.group' + this.getGroup(drag) + '.place' + currentPlace);};/*** Singleton that tracks all the DragDropToTextQuestions on this page, and deals* with event dispatching.** @type {Object}*/var questionManager = {/*** {boolean} used to ensure the event handlers are only initialised once per page.*/eventHandlersInitialised: false,/*** {Object} ensures that the drag event handlers are only initialised once per question,* indexed by containerId (id on the .que div).*/dragEventHandlersInitialised: {},/*** {boolean} is keyboard navigation or not.*/isKeyboardNavigation: false,/*** {DragDropToTextQuestion[]} all the questions on this page, indexed by containerId (id on the .que div).*/questions: {},/*** Initialise questions.** @param {String} containerId id of the outer div for this question.* @param {boolean} readOnly whether the question is being displayed read-only.*/init: function(containerId, readOnly) {questionManager.questions[containerId] = new DragDropToTextQuestion(containerId, readOnly);if (!questionManager.eventHandlersInitialised) {questionManager.setupEventHandlers();questionManager.eventHandlersInitialised = true;}if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) {questionManager.dragEventHandlersInitialised[containerId] = true;// We do not use the body event here to prevent the other event on Mobile device, such as scroll event.var questionContainer = document.getElementById(containerId);if (questionContainer.classList.contains('ddwtos') &&!questionContainer.classList.contains('qtype_ddwtos-readonly')) {// TODO: Convert all the jQuery selectors and events to native Javascript.questionManager.addEventHandlersToDrag($(questionContainer).find('span.draghome'));}}},/*** Set up the event handlers that make this question type work. (Done once per page.)*/setupEventHandlers: function() {$('body').on('keydown','.que.ddwtos:not(.qtype_ddwtos-readonly) span.drop',questionManager.handleKeyPress).on('keydown','.que.ddwtos:not(.qtype_ddwtos-readonly) span.draghome.placed:not(.beingdragged)',questionManager.handleKeyPress).on('qtype_ddwtos-dragmoved', questionManager.handleDragMoved);},/*** Binding the drag/touch event again for newly created element.** @param {jQuery} element Element to bind the event*/addEventHandlersToDrag: function(element) {// Unbind all the mousedown and touchstart events to prevent double binding.element.unbind('mousedown touchstart');element.on('mousedown touchstart', questionManager.handleDragStart);},/*** Handle mouse down / touch start on drags.* @param {Event} e the DOM event.*/handleDragStart: function(e) {e.preventDefault();var question = questionManager.getQuestionForEvent(e);if (question) {question.handleDragStart(e);}},/*** Handle key down / press on drops.* @param {KeyboardEvent} e*/handleKeyPress: function(e) {if (questionManager.isKeyboardNavigation) {return;}questionManager.isKeyboardNavigation = true;var question = questionManager.getQuestionForEvent(e);if (question) {question.handleKeyPress(e);}},/*** Given an event, work out which question it affects.** @param {Event} e the event.* @returns {DragDropToTextQuestion|undefined} The question, or undefined.*/getQuestionForEvent: function(e) {var containerId = $(e.currentTarget).closest('.que.ddwtos').attr('id');return questionManager.questions[containerId];},/*** Handle when drag moved.** @param {Event} e the event.* @param {jQuery} drag the drag* @param {jQuery} target the target* @param {DragDropToTextQuestion} thisQ the question.*/handleDragMoved: function(e, drag, target, thisQ) {drag.removeClass('beingdragged');drag.css('top', '').css('left', '');target.after(drag);target.removeClass('active');if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {drag.removeClass('placed').addClass('unplaced');drag.removeAttr('tabindex');drag.removeData('unplaced');if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {thisQ.getInfiniteDragClones(drag, true).first().remove();}}if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {drag.focus();drag.removeData('isfocus');}if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {target.removeData('isfocus');}if (questionManager.isKeyboardNavigation) {questionManager.isKeyboardNavigation = false;}if (thisQ.isQuestionInteracted()) {// The user has interacted with the draggable items. We need to mark the form as dirty.questionManager.handleFormDirty();// Save the new answered value.thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();}},/*** Handle when the form is dirty.*/handleFormDirty: function() {const responseForm = document.getElementById('responseform');FormChangeChecker.markFormAsDirty(responseForm);}};/*** @alias module:qtype_ddwtos/ddwtos*/return {/*** Initialise one drag-drop into text question.** @param {String} containerId id of the outer div for this question.* @param {boolean} readOnly whether the question is being displayed read-only.*/init: questionManager.init};});