From b5c181f117d11d0f2d7d712d9e656d564a18e44f Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Wed, 9 Feb 2022 15:23:31 +0700 Subject: [PATCH] MDL-72915 qtype_ddimageortext: Fix eventHandler for Unlimited items Sometimes, for the question that has a lot of input groups and unlimited draggable items, the 'clone' process takes longer than usual, so the questionManager.init() method will not add the eventHandler for the cloned drag. We should make sure to bind the eventHandler for the cloned too --- question/type/ddimageortext/amd/build/question.min.js | 2 +- question/type/ddimageortext/amd/build/question.min.js.map | 2 +- question/type/ddimageortext/amd/src/question.js | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/question/type/ddimageortext/amd/build/question.min.js b/question/type/ddimageortext/amd/build/question.min.js index 4372a85c737..048dd4c9a17 100644 --- a/question/type/ddimageortext/amd/build/question.min.js +++ b/question/type/ddimageortext/amd/build/question.min.js @@ -1,2 +1,2 @@ -define ("qtype_ddimageortext/question",["jquery","core/dragdrop","core/key_codes"],function(a,b,c){"use strict";function d(a,b,c){this.containerId=a;M.util.js_pending("qtype_ddimageortext-init-"+this.containerId);this.places=c;this.allImagesLoaded=!1;this.imageLoadingTimeoutId=null;this.isPrinting=!1;if(b){this.getRoot().addClass("qtype_ddimageortext-readonly")}var d=this;this.getNotYetLoadedImages().one("load",function(){d.waitForAllImagesToBeLoaded()});this.waitForAllImagesToBeLoaded()}d.prototype.waitForAllImagesToBeLoaded=function(){var a=this;if(this.allImagesLoaded){return}if(null!==this.imageLoadingTimeoutId){clearTimeout(this.imageLoadingTimeoutId)}if(0 div").each(function(c,d){b.resizeAllDragsAndDropsInGroup(b.getClassnameNumericSuffix(a(d),"dragitemgroup"))})};d.prototype.resizeAllDragsAndDropsInGroup=function(b){var c=this.getRoot(),d=c.find(".dragitemgroup"+b+" .draghome"),e=0,f=0;d.each(function(a,b){e=Math.max(e,Math.ceil(b.offsetWidth));f=Math.max(f,Math.ceil(b.offsetHeight))});e+=10;f+=10;d.each(function(b,c){var d=Math.round((e-c.offsetWidth)/2),g=Math.floor((f-c.offsetHeight)/2);a(c).css({"padding-left":d+"px","padding-right":e-c.offsetWidth-d+"px","padding-top":g+"px","padding-bottom":f-c.offsetHeight-g+"px"})});for(var g in this.places){if(!this.places.hasOwnProperty(g)){continue}var h=this.places[g],i=h.text;if(parseInt(h.group)!==b){continue}if(""===i){i=M.util.get_string("blank","qtype_ddimageortext")}c.find(".dropzones").append("
"+i+" 
");c.find(".dropzone.place"+g).width(e-2).height(f-2)}};d.prototype.cloneDrags=function(){var b=this;b.getRoot().find(".draghome").each(function(c,d){var e=a(d),f=e.clone();f.removeClass();f.addClass("draghome choice"+b.getChoice(e)+" group"+b.getGroup(e)+" dragplaceholder");e.before(f)})};d.prototype.cloneDragsForOneChoice=function(a){if(a.hasClass("infinite")){for(var b=this.noOfDropsInGroup(this.getGroup(a)),c=0;c=d.left&&a=d.top&&b=d.left&&a=d.top&&bb){b=e}});return b};d.prototype.isDragSameAsDrop=function(a,b){return this.getChoice(a)===this.getChoice(b)&&this.getGroup(a)===this.getGroup(b)};var f={eventHandlersInitialised:!1,dragEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function init(b,c,e){f.questions[b]=new d(b,c,e);if(!f.eventHandlersInitialised){f.setupEventHandlers();f.eventHandlersInitialised=!0}if(!f.dragEventHandlersInitialised.hasOwnProperty(b)){f.dragEventHandlersInitialised[b]=!0;var g=document.getElementById(b);if(g.classList.contains("ddimageortext")&&!g.classList.contains("qtype_ddimageortext-readonly")){f.addEventHandlersToDrag(a(g).find(".draghome"))}}},setupEventHandlers:function setupEventHandlers(){a("body").on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone",f.handleKeyPress).on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)",f.handleKeyPress).on("qtype_ddimageortext-dragmoved",f.handleDragMoved);a(window).on("resize",function(){f.handleWindowResize(!1)});window.addEventListener("beforeprint",function(){f.isPrinting=!0;f.handleWindowResize(f.isPrinting)});window.addEventListener("afterprint",function(){f.isPrinting=!1;f.handleWindowResize(f.isPrinting)});setTimeout(function(){f.fixLayoutIfThingsMoved()},100)},addEventHandlersToDrag:function addEventHandlersToDrag(a){a.unbind("mousedown touchstart");a.on("mousedown touchstart",f.handleDragStart)},handleDragStart:function handleDragStart(a){a.preventDefault();var b=f.getQuestionForEvent(a);if(b){b.handleDragStart(a)}},handleKeyPress:function handleKeyPress(a){if(f.isKeyboardNavigation){return}f.isKeyboardNavigation=!0;var b=f.getQuestionForEvent(a);if(b){b.handleKeyPress(a)}},handleWindowResize:function handleWindowResize(a){for(var b in f.questions){if(f.questions.hasOwnProperty(b)){f.questions[b].isPrinting=a;f.questions[b].handleResize()}}},fixLayoutIfThingsMoved:function fixLayoutIfThingsMoved(){this.handleWindowResize(f.isPrinting);setTimeout(function(){f.fixLayoutIfThingsMoved(f.isPrinting)},100)},handleDragMoved:function handleDragMoved(a,b,c,d){b.removeClass("beingdragged").css("z-index","");b.css("top",c.position().top).css("left",c.position().left);c.after(b);c.removeClass("active");if("undefined"!=typeof b.data("unplaced")&&!0===b.data("unplaced")){b.removeClass("placed").addClass("unplaced");b.removeAttr("tabindex");b.removeData("unplaced");b.css("top","").css("left","").css("transform","");if(b.hasClass("infinite")&&1 div").each(function(c,d){b.resizeAllDragsAndDropsInGroup(b.getClassnameNumericSuffix(a(d),"dragitemgroup"))})};d.prototype.resizeAllDragsAndDropsInGroup=function(b){var c=this.getRoot(),d=c.find(".dragitemgroup"+b+" .draghome"),e=0,f=0;d.each(function(a,b){e=Math.max(e,Math.ceil(b.offsetWidth));f=Math.max(f,Math.ceil(b.offsetHeight))});e+=10;f+=10;d.each(function(b,c){var d=Math.round((e-c.offsetWidth)/2),g=Math.floor((f-c.offsetHeight)/2);a(c).css({"padding-left":d+"px","padding-right":e-c.offsetWidth-d+"px","padding-top":g+"px","padding-bottom":f-c.offsetHeight-g+"px"})});for(var g in this.places){if(!this.places.hasOwnProperty(g)){continue}var h=this.places[g],i=h.text;if(parseInt(h.group)!==b){continue}if(""===i){i=M.util.get_string("blank","qtype_ddimageortext")}c.find(".dropzones").append("
"+i+" 
");c.find(".dropzone.place"+g).width(e-2).height(f-2)}};d.prototype.cloneDrags=function(){var b=this;b.getRoot().find(".draghome").each(function(c,d){var e=a(d),f=e.clone();f.removeClass();f.addClass("draghome choice"+b.getChoice(e)+" group"+b.getGroup(e)+" dragplaceholder");e.before(f)})};d.prototype.cloneDragsForOneChoice=function(a){if(a.hasClass("infinite")){for(var b=this.noOfDropsInGroup(this.getGroup(a)),c=0;c=d.left&&a=d.top&&b=d.left&&a=d.top&&bb){b=e}});return b};d.prototype.isDragSameAsDrop=function(a,b){return this.getChoice(a)===this.getChoice(b)&&this.getGroup(a)===this.getGroup(b)};var f={eventHandlersInitialised:!1,dragEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function init(b,c,e){f.questions[b]=new d(b,c,e);if(!f.eventHandlersInitialised){f.setupEventHandlers();f.eventHandlersInitialised=!0}if(!f.dragEventHandlersInitialised.hasOwnProperty(b)){f.dragEventHandlersInitialised[b]=!0;var g=document.getElementById(b);if(g.classList.contains("ddimageortext")&&!g.classList.contains("qtype_ddimageortext-readonly")){f.addEventHandlersToDrag(a(g).find(".draghome"))}}},setupEventHandlers:function setupEventHandlers(){a("body").on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone",f.handleKeyPress).on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)",f.handleKeyPress).on("qtype_ddimageortext-dragmoved",f.handleDragMoved);a(window).on("resize",function(){f.handleWindowResize(!1)});window.addEventListener("beforeprint",function(){f.isPrinting=!0;f.handleWindowResize(f.isPrinting)});window.addEventListener("afterprint",function(){f.isPrinting=!1;f.handleWindowResize(f.isPrinting)});setTimeout(function(){f.fixLayoutIfThingsMoved()},100)},addEventHandlersToDrag:function addEventHandlersToDrag(a){a.unbind("mousedown touchstart");a.on("mousedown touchstart",f.handleDragStart)},handleDragStart:function handleDragStart(a){a.preventDefault();var b=f.getQuestionForEvent(a);if(b){b.handleDragStart(a)}},handleKeyPress:function handleKeyPress(a){if(f.isKeyboardNavigation){return}f.isKeyboardNavigation=!0;var b=f.getQuestionForEvent(a);if(b){b.handleKeyPress(a)}},handleWindowResize:function handleWindowResize(a){for(var b in f.questions){if(f.questions.hasOwnProperty(b)){f.questions[b].isPrinting=a;f.questions[b].handleResize()}}},fixLayoutIfThingsMoved:function fixLayoutIfThingsMoved(){this.handleWindowResize(f.isPrinting);setTimeout(function(){f.fixLayoutIfThingsMoved(f.isPrinting)},100)},handleDragMoved:function handleDragMoved(a,b,c,d){b.removeClass("beingdragged").css("z-index","");b.css("top",c.position().top).css("left",c.position().left);c.after(b);c.removeClass("active");if("undefined"!=typeof b.data("unplaced")&&!0===b.data("unplaced")){b.removeClass("placed").addClass("unplaced");b.removeAttr("tabindex");b.removeData("unplaced");b.css("top","").css("left","").css("transform","");if(b.hasClass("infinite")&&1.\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/question\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) {\n\n \"use strict\";\n\n /**\n * Initialise one drag-drop onto image question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Array} places Information about the drop places.\n * @constructor\n */\n function DragDropOntoImageQuestion(containerId, readOnly, places) {\n this.containerId = containerId;\n M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);\n this.places = places;\n this.allImagesLoaded = false;\n this.imageLoadingTimeoutId = null;\n this.isPrinting = false;\n if (readOnly) {\n this.getRoot().addClass('qtype_ddimageortext-readonly');\n }\n\n var thisQ = this;\n this.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n this.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n var thisQ = this;\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n thisQ.waitForAllImagesToBeLoaded();\n }, 100);\n return;\n }\n\n // We now have all images. Carry on, but only after giving the layout a chance to settle down.\n this.allImagesLoaded = true;\n thisQ.setupQuestion();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {\n var thisQ = this;\n return this.getRoot().find('.ddarea img').not(function(i, imgNode) {\n return thisQ.imageIsLoaded(imgNode);\n });\n };\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Set up the question, once all images have been loaded.\n */\n DragDropOntoImageQuestion.prototype.setupQuestion = function() {\n this.resizeAllDragsAndDrops();\n this.cloneDrags();\n this.positionDragsAndDrops();\n M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);\n };\n\n /**\n * In each group, resize all the items to be the same size.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {\n var thisQ = this;\n this.getRoot().find('.draghomes > div').each(function(i, node) {\n thisQ.resizeAllDragsAndDropsInGroup(\n thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));\n });\n };\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {\n var root = this.getRoot(),\n dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n dragHomes.each(function(i, drag) {\n maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n dragHomes.each(function(i, drag) {\n var left = Math.round((maxWidth - drag.offsetWidth) / 2),\n top = Math.floor((maxHeight - drag.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drag).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'\n });\n });\n\n // Create the drops and make them the right size.\n for (var i in this.places) {\n if (!this.places.hasOwnProperty((i))) {\n continue;\n }\n var place = this.places[i],\n label = place.text;\n if (parseInt(place.group) !== group) {\n continue;\n }\n if (label === '') {\n label = M.util.get_string('blank', 'qtype_ddimageortext');\n }\n root.find('.dropzones').append('
' +\n '' + label + ' 
');\n root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);\n }\n };\n\n /**\n * Invisible 'drag homes' are output by the renderer. These have the same properties\n * as the drag items but are invisible. We clone these invisible elements to make the\n * actual drag items.\n */\n DragDropOntoImageQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n thisQ.getRoot().find('.draghome').each(function(index, dragHome) {\n var drag = $(dragHome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('draghome choice' +\n thisQ.getChoice(drag) + ' group' +\n thisQ.getGroup(drag) + ' dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Clone drag item for one choice.\n *\n * @param {jQuery} dragHome the drag home to clone.\n */\n DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {\n if (dragHome.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));\n for (var i = 0; i < noOfDrags; i++) {\n this.cloneDrag(dragHome);\n }\n } else {\n this.cloneDrag(dragHome);\n }\n };\n\n /**\n * Clone drag item.\n *\n * @param {jQuery} dragHome\n */\n DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {\n var drag = dragHome.clone();\n drag.removeClass('draghome')\n .addClass('drag unplaced moodle-has-zindex')\n .offset(dragHome.offset());\n this.getRoot().find('.dragitems').append(drag);\n };\n\n /**\n * Update the position of drags.\n */\n DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {\n var thisQ = this,\n root = this.getRoot(),\n bgRatio = this.bgRatio();\n\n // Move the drops into position.\n root.find('.ddarea .dropzone').each(function(i, dropNode) {\n var drop = $(dropNode),\n place = thisQ.places[thisQ.getPlace(drop)];\n // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation.\n drop.css('left', parseInt(place.xy[0]) * bgRatio)\n .css('top', parseInt(place.xy[1]) * bgRatio);\n drop.data('originX', parseInt(place.xy[0]))\n .data('originY', parseInt(place.xy[1]));\n thisQ.handleElementScale(drop, 'left top');\n });\n\n // First move all items back home.\n root.find('.draghome').not('.dragplaceholder').each(function(i, dragNode) {\n var drag = $(dragNode),\n currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');\n drag.addClass('unplaced')\n .removeClass('placed');\n drag.removeAttr('tabindex');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n });\n\n // Then place the ones that should be placed.\n root.find('input.placeinput').each(function(i, inputNode) {\n var input = $(inputNode),\n choice = input.val();\n if (choice.length === 0 || (choice.length > 0 && choice === '0')) {\n // No item in this place.\n return;\n }\n\n var place = thisQ.getPlace(input);\n // Get the unplaced drag.\n var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);\n // Get the clone of the drag.\n var hiddenDrag = thisQ.getDragClone(unplacedDrag);\n if (hiddenDrag.length) {\n if (unplacedDrag.hasClass('infinite')) {\n var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));\n var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = unplacedDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n } else {\n hiddenDrag.addClass('active');\n }\n } else {\n hiddenDrag.addClass('active');\n }\n }\n\n // Send the drag to drop.\n var drop = root.find('.dropzone.place' + place);\n thisQ.sendDragToDrop(unplacedDrag, drop);\n });\n };\n\n /**\n * Handles the start of dragging an item.\n *\n * @param {Event} e the touch start or mouse down event.\n */\n DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n drag = $(e.target).closest('.draghome'),\n currentIndex = this.calculateZIndex(),\n newIndex = currentIndex + 2;\n\n var info = dragDrop.prepare(e);\n if (!info.start || drag.hasClass('beingdragged')) {\n return;\n }\n\n drag.addClass('beingdragged').css('transform', '').css('z-index', newIndex);\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n this.setInputValue(currentPlace, 0);\n drag.removeClass('inplace' + currentPlace);\n var hiddenDrop = thisQ.getDrop(drag, currentPlace);\n if (hiddenDrop.length) {\n hiddenDrop.addClass('active');\n drag.offset(hiddenDrop.offset());\n }\n } else {\n var hiddenDrag = thisQ.getDragClone(drag);\n if (hiddenDrag.length) {\n if (drag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(thisQ.getGroup(drag));\n var cloneDrags = this.getInfiniteDragClones(drag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = drag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n drag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n }\n }\n\n dragDrop.start(e, drag, function(x, y, drag) {\n thisQ.dragMove(x, y, drag);\n }, function(x, y, drag) {\n thisQ.dragEnd(x, y, drag);\n });\n };\n\n /**\n * Called whenever the currently dragged items moves.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {\n var thisQ = this,\n highlighted = false;\n this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted) {\n highlighted = true;\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n this.getRoot().find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted && !thisQ.isDragSameAsDrop(drag, drop)) {\n highlighted = true;\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n };\n\n /**\n * Called when user drops a drag item.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {\n var thisQ = this,\n root = this.getRoot(),\n placed = false;\n\n // Looking for drag that was dropped on a dropzone.\n root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (!thisQ.isPointInDrop(pageX, pageY, drop)) {\n // Not this drop.\n return true;\n }\n\n // Now put this drag into the drop.\n drop.removeClass('valid-drag-over-drop');\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n\n if (!placed) {\n // Looking for drag that was dropped on a placed drag.\n root.find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) {\n var placedDrag = $(placedNode);\n if (!thisQ.isPointInDrop(pageX, pageY, placedDrag) || thisQ.isDragSameAsDrop(drag, placedDrag)) {\n // Not this placed drag.\n return true;\n }\n\n // Now put this drag into the drop.\n placedDrag.removeClass('valid-drag-over-drop');\n var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace');\n var drop = thisQ.getDrop(drag, currentPlace);\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n }\n\n if (!placed) {\n this.sendDragHome(drag);\n }\n };\n\n /**\n * Animate a drag item into a given place (or back home).\n *\n * @param {jQuery|null} drag the item to place. If null, clear the place.\n * @param {jQuery} drop the place to put it.\n */\n DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {\n // Is there already a drag in this drop? if so, evict it.\n var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));\n if (oldDrag.length !== 0) {\n oldDrag.addClass('beingdragged');\n oldDrag.offset(oldDrag.offset());\n var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');\n var hiddenDrop = this.getDrop(oldDrag, currentPlace);\n hiddenDrop.addClass('active');\n this.sendDragHome(oldDrag);\n }\n\n if (drag.length === 0) {\n this.setInputValue(this.getPlace(drop), 0);\n if (drop.data('isfocus')) {\n drop.focus();\n }\n } else {\n this.setInputValue(this.getPlace(drop), this.getChoice(drag));\n drag.removeClass('unplaced')\n .addClass('placed inplace' + this.getPlace(drop));\n drag.attr('tabindex', 0);\n this.animateTo(drag, drop);\n }\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n drag.data('unplaced', true);\n\n this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));\n };\n\n /**\n * Handles keyboard events on drops.\n *\n * Drops are focusable. Once focused, right/down/space switches to the next choice, and\n * left/up switches to the previous. Escape clear.\n *\n * @param {KeyboardEvent} e\n */\n DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {\n var drop = $(e.target).closest('.dropzone');\n if (drop.length === 0) {\n var placedDrag = $(e.target);\n var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');\n if (currentPlace !== null) {\n drop = this.getDrop(placedDrag, currentPlace);\n }\n }\n var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),\n nextDrag = $();\n\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.escape:\n questionManager.isKeyboardNavigation = false;\n break;\n\n default:\n questionManager.isKeyboardNavigation = false;\n return; // To avoid the preventDefault below.\n }\n\n if (nextDrag.length) {\n nextDrag.data('isfocus', true);\n nextDrag.addClass('beingdragged');\n var hiddenDrag = this.getDragClone(nextDrag);\n if (hiddenDrag.length) {\n if (nextDrag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));\n var cloneDrags = this.getInfiniteDragClones(nextDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = nextDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n nextDrag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n }\n } else {\n drop.data('isfocus', true);\n }\n\n e.preventDefault();\n this.sendDragToDrop(nextDrag, drop);\n };\n\n /**\n * Choose the next drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {\n var choice,\n numChoices = this.noOfChoicesInGroup(group);\n\n if (drag.length === 0) {\n choice = 1; // Was empty, so we want to select the first choice.\n } else {\n choice = this.getChoice(drag) + 1;\n }\n\n var next = this.getUnplacedChoice(group, choice);\n while (next.length === 0 && choice < numChoices) {\n choice++;\n next = this.getUnplacedChoice(group, choice);\n }\n\n return next;\n };\n\n /**\n * Choose the previous drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {\n var choice;\n\n if (drag.length === 0) {\n choice = this.noOfChoicesInGroup(group);\n } else {\n choice = this.getChoice(drag) - 1;\n }\n\n var previous = this.getUnplacedChoice(group, choice);\n while (previous.length === 0 && choice > 1) {\n choice--;\n previous = this.getUnplacedChoice(group, choice);\n }\n\n // Does this choice exist?\n return previous;\n };\n\n /**\n * Animate an object to the given destination.\n *\n * @param {jQuery} drag the element to be animated.\n * @param {jQuery} target element marking the place to move it to.\n */\n DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {\n var currentPos = drag.offset(),\n targetPos = target.offset(),\n thisQ = this;\n\n M.util.js_pending('qtype_ddimageortext-animate-' + thisQ.containerId);\n // Animate works in terms of CSS position, whereas locating an object\n // on the page works best with jQuery offset() function. So, to get\n // the right target position, we work out the required change in\n // offset() and then add that to the current CSS position.\n drag.animate(\n {\n left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,\n top: parseInt(drag.css('top')) + targetPos.top - currentPos.top\n },\n {\n duration: 'fast',\n done: function() {\n $('body').trigger('qtype_ddimageortext-dragmoved', [drag, target, thisQ]);\n M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId);\n }\n }\n );\n };\n\n /**\n * Detect if a point is inside a given DOM node.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drop the node to check (typically a drop).\n * @return {boolean} whether the point is inside the node.\n */\n DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {\n var position = drop.offset();\n if (drop.hasClass('draghome')) {\n return pageX >= position.left && pageX < position.left + drop.outerWidth()\n && pageY >= position.top && pageY < position.top + drop.outerHeight();\n }\n return pageX >= position.left && pageX < position.left + drop.width()\n && pageY >= position.top && pageY < position.top + drop.height();\n };\n\n /**\n * Set the value of the hidden input for a place, to record what is currently there.\n *\n * @param {int} place which place to set the input value for.\n * @param {int} choice the value to set.\n */\n DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {\n this.getRoot().find('input.placeinput.place' + place).val(choice);\n };\n\n /**\n * Get the outer div for this question.\n *\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DragDropOntoImageQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n /**\n * Get drag home for a given choice.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {\n if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {\n return this.getRoot().find('.dragitemgroup' + group +\n ' .draghome.infinite' +\n '.choice' + choice +\n '.group' + group);\n }\n return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);\n };\n\n /**\n * Get an unplaced choice for a particular group.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.\n */\n DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {\n return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);\n };\n\n /**\n * Get the drag that is currently in a given place.\n *\n * @param {int} place the place number.\n * @return {jQuery} the current drag (or an empty jQuery if none).\n */\n DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {\n return this.getRoot().find('.ddarea .draghome.inplace' + place);\n };\n\n /**\n * Return the number of blanks in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of drops.\n */\n DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {\n return this.getRoot().find('.dropzone.group' + group).length;\n };\n\n /**\n * Return the number of choices in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of choices.\n */\n DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {\n return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;\n };\n\n /**\n * Return the number at the end of the CSS class name with the given prefix.\n *\n * @param {jQuery} node\n * @param {String} prefix name prefix\n * @returns {Number|null} the suffix if found, else null.\n */\n DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = node.attr('class');\n if (classes !== '') {\n var classesArr = classes.split(' ');\n for (var index = 0; index < classesArr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesArr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesArr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Get the choice number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the choice number.\n */\n DragDropOntoImageQuestion.prototype.getChoice = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'choice');\n };\n\n /**\n * Given a DOM node that is significant to this question\n * (drag, drop, ...) get the group it belongs to.\n *\n * @param {jQuery} node a DOM node.\n * @returns {Number} the group it belongs to.\n */\n DragDropOntoImageQuestion.prototype.getGroup = function(node) {\n return this.getClassnameNumericSuffix(node, 'group');\n };\n\n /**\n * Get the place number of a drop, or its corresponding hidden input.\n *\n * @param {jQuery} node the DOM node.\n * @returns {Number} the place number.\n */\n DragDropOntoImageQuestion.prototype.getPlace = function(node) {\n return this.getClassnameNumericSuffix(node, 'place');\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropOntoImageQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.dragplaceholder');\n };\n\n /**\n * Get infinite drag clones for given drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} inHome in the home area or not.\n * @returns {jQuery} the drag's clones.\n */\n DragDropOntoImageQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {\n if (inHome) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n }\n return this.getRoot().find('.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n };\n\n /**\n * Get drop for a given drag and place.\n *\n * @param {jQuery} drag the drag.\n * @param {Integer} currentPlace the current place of drag.\n * @returns {jQuery} the drop's clone.\n */\n DragDropOntoImageQuestion.prototype.getDrop = function(drag, currentPlace) {\n return this.getRoot().find('.dropzone.group' + this.getGroup(drag) + '.place' + currentPlace);\n };\n\n /**\n * Handle when the window is resized.\n */\n DragDropOntoImageQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('.ddarea .dropzone').each(function(i, dropNode) {\n $(dropNode)\n .css('left', parseInt($(dropNode).data('originX')) * parseFloat(bgRatio))\n .css('top', parseInt($(dropNode).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(dropNode, 'left top');\n });\n\n this.getRoot().find('div.droparea .draghome').not('.beingdragged').each(function(key, drag) {\n $(drag)\n .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio))\n .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(drag, 'left top');\n });\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropOntoImageQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DragDropOntoImageQuestion.prototype.handleElementScale = function(element, type) {\n var bgRatio = parseFloat(this.bgRatio());\n if (this.isPrinting) {\n bgRatio = 1;\n }\n $(element).css({\n '-webkit-transform': 'scale(' + bgRatio + ')',\n '-moz-transform': 'scale(' + bgRatio + ')',\n '-ms-transform': 'scale(' + bgRatio + ')',\n '-o-transform': 'scale(' + bgRatio + ')',\n 'transform': 'scale(' + bgRatio + ')',\n 'transform-origin': type\n });\n };\n\n /**\n * Calculate z-index value.\n *\n * @returns {number} z-index value\n */\n DragDropOntoImageQuestion.prototype.calculateZIndex = function() {\n var zIndex = 0;\n this.getRoot().find('.ddarea .dropzone, div.droparea .draghome').each(function(i, dropNode) {\n dropNode = $(dropNode);\n // Note that webkit browsers won't return the z-index value from the CSS stylesheet\n // if the element doesn't have a position specified. Instead it'll return \"auto\".\n var itemZIndex = dropNode.css('z-index') ? parseInt(dropNode.css('z-index')) : 0;\n\n if (itemZIndex > zIndex) {\n zIndex = itemZIndex;\n }\n });\n\n return zIndex;\n };\n\n /**\n * Check that the drag is drop to it's clone.\n *\n * @param {jQuery} drag The drag.\n * @param {jQuery} drop The drop.\n * @returns {boolean}\n */\n DragDropOntoImageQuestion.prototype.isDragSameAsDrop = function(drag, drop) {\n return this.getChoice(drag) === this.getChoice(drop) && this.getGroup(drag) === this.getGroup(drop);\n };\n\n /**\n * Singleton object that handles all the DragDropOntoImageQuestions\n * on the page, and deals with event dispatching.\n * @type {Object}\n */\n var questionManager = {\n\n /**\n * {boolean} ensures that the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} ensures that the drag event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n dragEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation or not.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @method\n * @param {String} containerId the id of the div.que that contains this question.\n * @param {boolean} readOnly whether the question is read-only.\n * @param {Array} places data.\n */\n init: function(containerId, readOnly, places) {\n questionManager.questions[containerId] =\n new DragDropOntoImageQuestion(containerId, readOnly, places);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.dragEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('ddimageortext') &&\n !questionContainer.classList.contains('qtype_ddimageortext-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToDrag($(questionContainer).find('.draghome'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $('body')\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',\n questionManager.handleKeyPress)\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)',\n questionManager.handleKeyPress)\n .on('qtype_ddimageortext-dragmoved', questionManager.handleDragMoved);\n $(window).on('resize', function() {\n questionManager.handleWindowResize(false);\n });\n window.addEventListener('beforeprint', function() {\n questionManager.isPrinting = true;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n window.addEventListener('afterprint', function() {\n questionManager.isPrinting = false;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved();\n }, 100);\n },\n\n /**\n * Binding the drag/touch event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToDrag: function(element) {\n // Unbind all the mousedown and touchstart events to prevent double binding.\n element.unbind('mousedown touchstart');\n element.on('mousedown touchstart', questionManager.handleDragStart);\n },\n\n /**\n * Handle mouse down / touch start events on drags.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on drags.\n * @param {KeyboardEvent} e\n */\n handleKeyPress: function(e) {\n if (questionManager.isKeyboardNavigation) {\n return;\n }\n questionManager.isKeyboardNavigation = true;\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * Sometimes, despite our best efforts, things change in a way that cannot\n * be specifically caught (e.g. dock expanding or collapsing in Boost).\n * Therefore, we need to periodically check everything is in the right position.\n */\n fixLayoutIfThingsMoved: function() {\n this.handleWindowResize(questionManager.isPrinting);\n // We use setTimeout after finishing work, rather than setInterval,\n // in case positioning things is slow. We want 100 ms gap\n // between executions, not what setInterval does.\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);\n }, 100);\n },\n\n /**\n * Handle when drag moved.\n *\n * @param {Event} e the event.\n * @param {jQuery} drag the drag\n * @param {jQuery} target the target\n * @param {DragDropOntoImageQuestion} thisQ the question.\n */\n handleDragMoved: function(e, drag, target, thisQ) {\n drag.removeClass('beingdragged').css('z-index', '');\n drag.css('top', target.position().top).css('left', target.position().left);\n target.after(drag);\n target.removeClass('active');\n if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {\n drag.removeClass('placed').addClass('unplaced');\n drag.removeAttr('tabindex');\n drag.removeData('unplaced');\n drag.css('top', '')\n .css('left', '')\n .css('transform', '');\n if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {\n thisQ.getInfiniteDragClones(drag, true).first().remove();\n }\n } else {\n drag.data('originX', target.data('originX')).data('originY', target.data('originY'));\n thisQ.handleElementScale(drag, 'left top');\n }\n if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {\n drag.focus();\n drag.removeData('isfocus');\n }\n if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {\n target.removeData('isfocus');\n }\n if (questionManager.isKeyboardNavigation) {\n questionManager.isKeyboardNavigation = false;\n }\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');\n return questionManager.questions[containerId];\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/question\n */\n return {\n init: questionManager.init\n };\n});\n"],"file":"question.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/question.js"],"names":["define","$","dragDrop","keys","DragDropOntoImageQuestion","containerId","readOnly","places","M","util","js_pending","allImagesLoaded","imageLoadingTimeoutId","isPrinting","getRoot","addClass","thisQ","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","clearTimeout","length","setTimeout","setupQuestion","find","not","i","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","resizeAllDragsAndDrops","cloneDrags","positionDragsAndDrops","js_complete","each","node","resizeAllDragsAndDropsInGroup","getClassnameNumericSuffix","group","root","dragHomes","maxWidth","maxHeight","drag","Math","max","ceil","offsetWidth","offsetHeight","left","round","top","floor","css","hasOwnProperty","place","label","text","parseInt","get_string","append","width","height","index","dragHome","placeHolder","clone","removeClass","getChoice","getGroup","before","cloneDragsForOneChoice","hasClass","noOfDrags","noOfDropsInGroup","cloneDrag","offset","bgRatio","dropNode","drop","getPlace","xy","data","handleElementScale","dragNode","currentPlace","removeAttr","inputNode","input","choice","val","unplacedDrag","getUnplacedChoice","hiddenDrag","getDragClone","getInfiniteDragClones","after","questionManager","addEventHandlersToDrag","sendDragToDrop","handleDragStart","e","target","closest","currentIndex","calculateZIndex","info","prepare","start","setInputValue","hiddenDrop","getDrop","x","y","dragMove","dragEnd","pageX","pageY","highlighted","isPointInDrop","isDragSameAsDrop","placed","placedNode","placedDrag","sendDragHome","oldDrag","getCurrentDragInPlace","focus","attr","animateTo","getDragHome","handleKeyPress","currentDrag","nextDrag","keyCode","space","arrowRight","arrowDown","getNextDrag","arrowLeft","arrowUp","getPreviousDrag","escape","isKeyboardNavigation","preventDefault","numChoices","noOfChoicesInGroup","next","previous","currentPos","targetPos","animate","duration","done","trigger","position","outerWidth","outerHeight","document","getElementById","bgImage","is","slice","prefix","classes","classesArr","split","patt1","RegExp","test","match","exec","inHome","handleResize","parseFloat","key","bgImg","bgImgNaturalWidth","get","naturalWidth","bgImgClientWidth","element","type","zIndex","itemZIndex","eventHandlersInitialised","dragEventHandlersInitialised","questions","init","setupEventHandlers","questionContainer","classList","contains","on","handleDragMoved","window","handleWindowResize","addEventListener","fixLayoutIfThingsMoved","unbind","question","getQuestionForEvent","removeData","first","remove","currentTarget"],"mappings":"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","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 .\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/question\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) {\n\n \"use strict\";\n\n /**\n * Initialise one drag-drop onto image question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Array} places Information about the drop places.\n * @constructor\n */\n function DragDropOntoImageQuestion(containerId, readOnly, places) {\n this.containerId = containerId;\n M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);\n this.places = places;\n this.allImagesLoaded = false;\n this.imageLoadingTimeoutId = null;\n this.isPrinting = false;\n if (readOnly) {\n this.getRoot().addClass('qtype_ddimageortext-readonly');\n }\n\n var thisQ = this;\n this.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n this.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n var thisQ = this;\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n thisQ.waitForAllImagesToBeLoaded();\n }, 100);\n return;\n }\n\n // We now have all images. Carry on, but only after giving the layout a chance to settle down.\n this.allImagesLoaded = true;\n thisQ.setupQuestion();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {\n var thisQ = this;\n return this.getRoot().find('.ddarea img').not(function(i, imgNode) {\n return thisQ.imageIsLoaded(imgNode);\n });\n };\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Set up the question, once all images have been loaded.\n */\n DragDropOntoImageQuestion.prototype.setupQuestion = function() {\n this.resizeAllDragsAndDrops();\n this.cloneDrags();\n this.positionDragsAndDrops();\n M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);\n };\n\n /**\n * In each group, resize all the items to be the same size.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {\n var thisQ = this;\n this.getRoot().find('.draghomes > div').each(function(i, node) {\n thisQ.resizeAllDragsAndDropsInGroup(\n thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));\n });\n };\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {\n var root = this.getRoot(),\n dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n dragHomes.each(function(i, drag) {\n maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n dragHomes.each(function(i, drag) {\n var left = Math.round((maxWidth - drag.offsetWidth) / 2),\n top = Math.floor((maxHeight - drag.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drag).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'\n });\n });\n\n // Create the drops and make them the right size.\n for (var i in this.places) {\n if (!this.places.hasOwnProperty((i))) {\n continue;\n }\n var place = this.places[i],\n label = place.text;\n if (parseInt(place.group) !== group) {\n continue;\n }\n if (label === '') {\n label = M.util.get_string('blank', 'qtype_ddimageortext');\n }\n root.find('.dropzones').append('
' +\n '' + label + ' 
');\n root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);\n }\n };\n\n /**\n * Invisible 'drag homes' are output by the renderer. These have the same properties\n * as the drag items but are invisible. We clone these invisible elements to make the\n * actual drag items.\n */\n DragDropOntoImageQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n thisQ.getRoot().find('.draghome').each(function(index, dragHome) {\n var drag = $(dragHome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('draghome choice' +\n thisQ.getChoice(drag) + ' group' +\n thisQ.getGroup(drag) + ' dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Clone drag item for one choice.\n *\n * @param {jQuery} dragHome the drag home to clone.\n */\n DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {\n if (dragHome.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));\n for (var i = 0; i < noOfDrags; i++) {\n this.cloneDrag(dragHome);\n }\n } else {\n this.cloneDrag(dragHome);\n }\n };\n\n /**\n * Clone drag item.\n *\n * @param {jQuery} dragHome\n */\n DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {\n var drag = dragHome.clone();\n drag.removeClass('draghome')\n .addClass('drag unplaced moodle-has-zindex')\n .offset(dragHome.offset());\n this.getRoot().find('.dragitems').append(drag);\n };\n\n /**\n * Update the position of drags.\n */\n DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {\n var thisQ = this,\n root = this.getRoot(),\n bgRatio = this.bgRatio();\n\n // Move the drops into position.\n root.find('.ddarea .dropzone').each(function(i, dropNode) {\n var drop = $(dropNode),\n place = thisQ.places[thisQ.getPlace(drop)];\n // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation.\n drop.css('left', parseInt(place.xy[0]) * bgRatio)\n .css('top', parseInt(place.xy[1]) * bgRatio);\n drop.data('originX', parseInt(place.xy[0]))\n .data('originY', parseInt(place.xy[1]));\n thisQ.handleElementScale(drop, 'left top');\n });\n\n // First move all items back home.\n root.find('.draghome').not('.dragplaceholder').each(function(i, dragNode) {\n var drag = $(dragNode),\n currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');\n drag.addClass('unplaced')\n .removeClass('placed');\n drag.removeAttr('tabindex');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n });\n\n // Then place the ones that should be placed.\n root.find('input.placeinput').each(function(i, inputNode) {\n var input = $(inputNode),\n choice = input.val();\n if (choice.length === 0 || (choice.length > 0 && choice === '0')) {\n // No item in this place.\n return;\n }\n\n var place = thisQ.getPlace(input);\n // Get the unplaced drag.\n var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);\n // Get the clone of the drag.\n var hiddenDrag = thisQ.getDragClone(unplacedDrag);\n if (hiddenDrag.length) {\n if (unplacedDrag.hasClass('infinite')) {\n var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));\n var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = unplacedDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n // Sometimes, for the question that has a lot of input groups and unlimited draggable items,\n // this 'clone' process takes longer than usual, so the questionManager.init() method\n // will not add the eventHandler for this cloned drag.\n // We need to make sure to add the eventHandler for the cloned drag too.\n questionManager.addEventHandlersToDrag(cloneDrag);\n } else {\n hiddenDrag.addClass('active');\n }\n } else {\n hiddenDrag.addClass('active');\n }\n }\n\n // Send the drag to drop.\n var drop = root.find('.dropzone.place' + place);\n thisQ.sendDragToDrop(unplacedDrag, drop);\n });\n };\n\n /**\n * Handles the start of dragging an item.\n *\n * @param {Event} e the touch start or mouse down event.\n */\n DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n drag = $(e.target).closest('.draghome'),\n currentIndex = this.calculateZIndex(),\n newIndex = currentIndex + 2;\n\n var info = dragDrop.prepare(e);\n if (!info.start || drag.hasClass('beingdragged')) {\n return;\n }\n\n drag.addClass('beingdragged').css('transform', '').css('z-index', newIndex);\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n this.setInputValue(currentPlace, 0);\n drag.removeClass('inplace' + currentPlace);\n var hiddenDrop = thisQ.getDrop(drag, currentPlace);\n if (hiddenDrop.length) {\n hiddenDrop.addClass('active');\n drag.offset(hiddenDrop.offset());\n }\n } else {\n var hiddenDrag = thisQ.getDragClone(drag);\n if (hiddenDrag.length) {\n if (drag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(thisQ.getGroup(drag));\n var cloneDrags = this.getInfiniteDragClones(drag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = drag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n drag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n }\n }\n\n dragDrop.start(e, drag, function(x, y, drag) {\n thisQ.dragMove(x, y, drag);\n }, function(x, y, drag) {\n thisQ.dragEnd(x, y, drag);\n });\n };\n\n /**\n * Called whenever the currently dragged items moves.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {\n var thisQ = this,\n highlighted = false;\n this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted) {\n highlighted = true;\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n this.getRoot().find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted && !thisQ.isDragSameAsDrop(drag, drop)) {\n highlighted = true;\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n };\n\n /**\n * Called when user drops a drag item.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {\n var thisQ = this,\n root = this.getRoot(),\n placed = false;\n\n // Looking for drag that was dropped on a dropzone.\n root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (!thisQ.isPointInDrop(pageX, pageY, drop)) {\n // Not this drop.\n return true;\n }\n\n // Now put this drag into the drop.\n drop.removeClass('valid-drag-over-drop');\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n\n if (!placed) {\n // Looking for drag that was dropped on a placed drag.\n root.find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) {\n var placedDrag = $(placedNode);\n if (!thisQ.isPointInDrop(pageX, pageY, placedDrag) || thisQ.isDragSameAsDrop(drag, placedDrag)) {\n // Not this placed drag.\n return true;\n }\n\n // Now put this drag into the drop.\n placedDrag.removeClass('valid-drag-over-drop');\n var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace');\n var drop = thisQ.getDrop(drag, currentPlace);\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n }\n\n if (!placed) {\n this.sendDragHome(drag);\n }\n };\n\n /**\n * Animate a drag item into a given place (or back home).\n *\n * @param {jQuery|null} drag the item to place. If null, clear the place.\n * @param {jQuery} drop the place to put it.\n */\n DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {\n // Is there already a drag in this drop? if so, evict it.\n var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));\n if (oldDrag.length !== 0) {\n oldDrag.addClass('beingdragged');\n oldDrag.offset(oldDrag.offset());\n var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');\n var hiddenDrop = this.getDrop(oldDrag, currentPlace);\n hiddenDrop.addClass('active');\n this.sendDragHome(oldDrag);\n }\n\n if (drag.length === 0) {\n this.setInputValue(this.getPlace(drop), 0);\n if (drop.data('isfocus')) {\n drop.focus();\n }\n } else {\n this.setInputValue(this.getPlace(drop), this.getChoice(drag));\n drag.removeClass('unplaced')\n .addClass('placed inplace' + this.getPlace(drop));\n drag.attr('tabindex', 0);\n this.animateTo(drag, drop);\n }\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n drag.data('unplaced', true);\n\n this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));\n };\n\n /**\n * Handles keyboard events on drops.\n *\n * Drops are focusable. Once focused, right/down/space switches to the next choice, and\n * left/up switches to the previous. Escape clear.\n *\n * @param {KeyboardEvent} e\n */\n DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {\n var drop = $(e.target).closest('.dropzone');\n if (drop.length === 0) {\n var placedDrag = $(e.target);\n var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');\n if (currentPlace !== null) {\n drop = this.getDrop(placedDrag, currentPlace);\n }\n }\n var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),\n nextDrag = $();\n\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.escape:\n questionManager.isKeyboardNavigation = false;\n break;\n\n default:\n questionManager.isKeyboardNavigation = false;\n return; // To avoid the preventDefault below.\n }\n\n if (nextDrag.length) {\n nextDrag.data('isfocus', true);\n nextDrag.addClass('beingdragged');\n var hiddenDrag = this.getDragClone(nextDrag);\n if (hiddenDrag.length) {\n if (nextDrag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));\n var cloneDrags = this.getInfiniteDragClones(nextDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = nextDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n nextDrag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n }\n } else {\n drop.data('isfocus', true);\n }\n\n e.preventDefault();\n this.sendDragToDrop(nextDrag, drop);\n };\n\n /**\n * Choose the next drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {\n var choice,\n numChoices = this.noOfChoicesInGroup(group);\n\n if (drag.length === 0) {\n choice = 1; // Was empty, so we want to select the first choice.\n } else {\n choice = this.getChoice(drag) + 1;\n }\n\n var next = this.getUnplacedChoice(group, choice);\n while (next.length === 0 && choice < numChoices) {\n choice++;\n next = this.getUnplacedChoice(group, choice);\n }\n\n return next;\n };\n\n /**\n * Choose the previous drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {\n var choice;\n\n if (drag.length === 0) {\n choice = this.noOfChoicesInGroup(group);\n } else {\n choice = this.getChoice(drag) - 1;\n }\n\n var previous = this.getUnplacedChoice(group, choice);\n while (previous.length === 0 && choice > 1) {\n choice--;\n previous = this.getUnplacedChoice(group, choice);\n }\n\n // Does this choice exist?\n return previous;\n };\n\n /**\n * Animate an object to the given destination.\n *\n * @param {jQuery} drag the element to be animated.\n * @param {jQuery} target element marking the place to move it to.\n */\n DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {\n var currentPos = drag.offset(),\n targetPos = target.offset(),\n thisQ = this;\n\n M.util.js_pending('qtype_ddimageortext-animate-' + thisQ.containerId);\n // Animate works in terms of CSS position, whereas locating an object\n // on the page works best with jQuery offset() function. So, to get\n // the right target position, we work out the required change in\n // offset() and then add that to the current CSS position.\n drag.animate(\n {\n left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,\n top: parseInt(drag.css('top')) + targetPos.top - currentPos.top\n },\n {\n duration: 'fast',\n done: function() {\n $('body').trigger('qtype_ddimageortext-dragmoved', [drag, target, thisQ]);\n M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId);\n }\n }\n );\n };\n\n /**\n * Detect if a point is inside a given DOM node.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drop the node to check (typically a drop).\n * @return {boolean} whether the point is inside the node.\n */\n DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {\n var position = drop.offset();\n if (drop.hasClass('draghome')) {\n return pageX >= position.left && pageX < position.left + drop.outerWidth()\n && pageY >= position.top && pageY < position.top + drop.outerHeight();\n }\n return pageX >= position.left && pageX < position.left + drop.width()\n && pageY >= position.top && pageY < position.top + drop.height();\n };\n\n /**\n * Set the value of the hidden input for a place, to record what is currently there.\n *\n * @param {int} place which place to set the input value for.\n * @param {int} choice the value to set.\n */\n DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {\n this.getRoot().find('input.placeinput.place' + place).val(choice);\n };\n\n /**\n * Get the outer div for this question.\n *\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DragDropOntoImageQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n /**\n * Get drag home for a given choice.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {\n if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {\n return this.getRoot().find('.dragitemgroup' + group +\n ' .draghome.infinite' +\n '.choice' + choice +\n '.group' + group);\n }\n return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);\n };\n\n /**\n * Get an unplaced choice for a particular group.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.\n */\n DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {\n return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);\n };\n\n /**\n * Get the drag that is currently in a given place.\n *\n * @param {int} place the place number.\n * @return {jQuery} the current drag (or an empty jQuery if none).\n */\n DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {\n return this.getRoot().find('.ddarea .draghome.inplace' + place);\n };\n\n /**\n * Return the number of blanks in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of drops.\n */\n DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {\n return this.getRoot().find('.dropzone.group' + group).length;\n };\n\n /**\n * Return the number of choices in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of choices.\n */\n DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {\n return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;\n };\n\n /**\n * Return the number at the end of the CSS class name with the given prefix.\n *\n * @param {jQuery} node\n * @param {String} prefix name prefix\n * @returns {Number|null} the suffix if found, else null.\n */\n DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = node.attr('class');\n if (classes !== '') {\n var classesArr = classes.split(' ');\n for (var index = 0; index < classesArr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesArr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesArr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Get the choice number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the choice number.\n */\n DragDropOntoImageQuestion.prototype.getChoice = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'choice');\n };\n\n /**\n * Given a DOM node that is significant to this question\n * (drag, drop, ...) get the group it belongs to.\n *\n * @param {jQuery} node a DOM node.\n * @returns {Number} the group it belongs to.\n */\n DragDropOntoImageQuestion.prototype.getGroup = function(node) {\n return this.getClassnameNumericSuffix(node, 'group');\n };\n\n /**\n * Get the place number of a drop, or its corresponding hidden input.\n *\n * @param {jQuery} node the DOM node.\n * @returns {Number} the place number.\n */\n DragDropOntoImageQuestion.prototype.getPlace = function(node) {\n return this.getClassnameNumericSuffix(node, 'place');\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropOntoImageQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.dragplaceholder');\n };\n\n /**\n * Get infinite drag clones for given drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} inHome in the home area or not.\n * @returns {jQuery} the drag's clones.\n */\n DragDropOntoImageQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {\n if (inHome) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n }\n return this.getRoot().find('.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n };\n\n /**\n * Get drop for a given drag and place.\n *\n * @param {jQuery} drag the drag.\n * @param {Integer} currentPlace the current place of drag.\n * @returns {jQuery} the drop's clone.\n */\n DragDropOntoImageQuestion.prototype.getDrop = function(drag, currentPlace) {\n return this.getRoot().find('.dropzone.group' + this.getGroup(drag) + '.place' + currentPlace);\n };\n\n /**\n * Handle when the window is resized.\n */\n DragDropOntoImageQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('.ddarea .dropzone').each(function(i, dropNode) {\n $(dropNode)\n .css('left', parseInt($(dropNode).data('originX')) * parseFloat(bgRatio))\n .css('top', parseInt($(dropNode).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(dropNode, 'left top');\n });\n\n this.getRoot().find('div.droparea .draghome').not('.beingdragged').each(function(key, drag) {\n $(drag)\n .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio))\n .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(drag, 'left top');\n });\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropOntoImageQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DragDropOntoImageQuestion.prototype.handleElementScale = function(element, type) {\n var bgRatio = parseFloat(this.bgRatio());\n if (this.isPrinting) {\n bgRatio = 1;\n }\n $(element).css({\n '-webkit-transform': 'scale(' + bgRatio + ')',\n '-moz-transform': 'scale(' + bgRatio + ')',\n '-ms-transform': 'scale(' + bgRatio + ')',\n '-o-transform': 'scale(' + bgRatio + ')',\n 'transform': 'scale(' + bgRatio + ')',\n 'transform-origin': type\n });\n };\n\n /**\n * Calculate z-index value.\n *\n * @returns {number} z-index value\n */\n DragDropOntoImageQuestion.prototype.calculateZIndex = function() {\n var zIndex = 0;\n this.getRoot().find('.ddarea .dropzone, div.droparea .draghome').each(function(i, dropNode) {\n dropNode = $(dropNode);\n // Note that webkit browsers won't return the z-index value from the CSS stylesheet\n // if the element doesn't have a position specified. Instead it'll return \"auto\".\n var itemZIndex = dropNode.css('z-index') ? parseInt(dropNode.css('z-index')) : 0;\n\n if (itemZIndex > zIndex) {\n zIndex = itemZIndex;\n }\n });\n\n return zIndex;\n };\n\n /**\n * Check that the drag is drop to it's clone.\n *\n * @param {jQuery} drag The drag.\n * @param {jQuery} drop The drop.\n * @returns {boolean}\n */\n DragDropOntoImageQuestion.prototype.isDragSameAsDrop = function(drag, drop) {\n return this.getChoice(drag) === this.getChoice(drop) && this.getGroup(drag) === this.getGroup(drop);\n };\n\n /**\n * Singleton object that handles all the DragDropOntoImageQuestions\n * on the page, and deals with event dispatching.\n * @type {Object}\n */\n var questionManager = {\n\n /**\n * {boolean} ensures that the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} ensures that the drag event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n dragEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation or not.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @method\n * @param {String} containerId the id of the div.que that contains this question.\n * @param {boolean} readOnly whether the question is read-only.\n * @param {Array} places data.\n */\n init: function(containerId, readOnly, places) {\n questionManager.questions[containerId] =\n new DragDropOntoImageQuestion(containerId, readOnly, places);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.dragEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('ddimageortext') &&\n !questionContainer.classList.contains('qtype_ddimageortext-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToDrag($(questionContainer).find('.draghome'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $('body')\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',\n questionManager.handleKeyPress)\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)',\n questionManager.handleKeyPress)\n .on('qtype_ddimageortext-dragmoved', questionManager.handleDragMoved);\n $(window).on('resize', function() {\n questionManager.handleWindowResize(false);\n });\n window.addEventListener('beforeprint', function() {\n questionManager.isPrinting = true;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n window.addEventListener('afterprint', function() {\n questionManager.isPrinting = false;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved();\n }, 100);\n },\n\n /**\n * Binding the drag/touch event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToDrag: function(element) {\n // Unbind all the mousedown and touchstart events to prevent double binding.\n element.unbind('mousedown touchstart');\n element.on('mousedown touchstart', questionManager.handleDragStart);\n },\n\n /**\n * Handle mouse down / touch start events on drags.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on drags.\n * @param {KeyboardEvent} e\n */\n handleKeyPress: function(e) {\n if (questionManager.isKeyboardNavigation) {\n return;\n }\n questionManager.isKeyboardNavigation = true;\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * Sometimes, despite our best efforts, things change in a way that cannot\n * be specifically caught (e.g. dock expanding or collapsing in Boost).\n * Therefore, we need to periodically check everything is in the right position.\n */\n fixLayoutIfThingsMoved: function() {\n this.handleWindowResize(questionManager.isPrinting);\n // We use setTimeout after finishing work, rather than setInterval,\n // in case positioning things is slow. We want 100 ms gap\n // between executions, not what setInterval does.\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);\n }, 100);\n },\n\n /**\n * Handle when drag moved.\n *\n * @param {Event} e the event.\n * @param {jQuery} drag the drag\n * @param {jQuery} target the target\n * @param {DragDropOntoImageQuestion} thisQ the question.\n */\n handleDragMoved: function(e, drag, target, thisQ) {\n drag.removeClass('beingdragged').css('z-index', '');\n drag.css('top', target.position().top).css('left', target.position().left);\n target.after(drag);\n target.removeClass('active');\n if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {\n drag.removeClass('placed').addClass('unplaced');\n drag.removeAttr('tabindex');\n drag.removeData('unplaced');\n drag.css('top', '')\n .css('left', '')\n .css('transform', '');\n if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {\n thisQ.getInfiniteDragClones(drag, true).first().remove();\n }\n } else {\n drag.data('originX', target.data('originX')).data('originY', target.data('originY'));\n thisQ.handleElementScale(drag, 'left top');\n }\n if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {\n drag.focus();\n drag.removeData('isfocus');\n }\n if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {\n target.removeData('isfocus');\n }\n if (questionManager.isKeyboardNavigation) {\n questionManager.isKeyboardNavigation = false;\n }\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');\n return questionManager.questions[containerId];\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/question\n */\n return {\n init: questionManager.init\n };\n});\n"],"file":"question.min.js"} \ No newline at end of file diff --git a/question/type/ddimageortext/amd/src/question.js b/question/type/ddimageortext/amd/src/question.js index 3fbf110dd52..b95cd05d53e 100644 --- a/question/type/ddimageortext/amd/src/question.js +++ b/question/type/ddimageortext/amd/src/question.js @@ -284,6 +284,11 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys cloneDrag.removeClass('beingdragged'); cloneDrag.removeAttr('tabindex'); hiddenDrag.after(cloneDrag); + // Sometimes, for the question that has a lot of input groups and unlimited draggable items, + // this 'clone' process takes longer than usual, so the questionManager.init() method + // will not add the eventHandler for this cloned drag. + // We need to make sure to add the eventHandler for the cloned drag too. + questionManager.addEventHandlersToDrag(cloneDrag); } else { hiddenDrag.addClass('active'); }