From b7f08d7eccb480c30187619f1895f644135cadaa Mon Sep 17 00:00:00 2001 From: hieuvu Date: Fri, 2 Dec 2022 12:39:23 +0700 Subject: [PATCH 1/2] MDL-76106 qtype_ddmarker: Improve loading consistency. Wait for the image to load completely before running the js. --- .../type/ddmarker/amd/build/question.min.js | 2 +- .../ddmarker/amd/build/question.min.js.map | 2 +- question/type/ddmarker/amd/src/question.js | 65 ++++++++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/question/type/ddmarker/amd/build/question.min.js b/question/type/ddmarker/amd/build/question.min.js index fbdc11f8eb5..9a55053a2dd 100644 --- a/question/type/ddmarker/amd/build/question.min.js +++ b/question/type/ddmarker/amd/build/question.min.js @@ -5,6 +5,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_ddmarker/question",["jquery","core/dragdrop","qtype_ddmarker/shapes","core/key_codes","core_form/changechecker"],(function($,dragDrop,Shapes,keys,FormChangeChecker){function DragDropMarkersQuestion(containerId,readOnly,visibleDropZones){this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.shapes=[],this.shapeSVGs=[],this.isPrinting=!1,this.questionAnswer={},readOnly&&this.getRoot().addClass("qtype_ddmarker-readonly"),this.cloneDrags(),this.repositionDrags(),this.drawDropzones()}DragDropMarkersQuestion.prototype.drawDropzones=function(){if(this.visibleDropZones.length>0){var bgImage=this.bgImage();this.getRoot().find("div.dropzones").html('');for(var svg=this.getRoot().find("svg.dropzones"),nextColourIndex=0,dropZoneNo=0;dropZoneNo'+dropZone.markertext+"");var markerspan=this.getRoot().find("div.ddarea div.markertexts span.markertext"+dropZoneNo);if(markerspan.length){var handles=shape.getHandlePositions(),positionLeft=handles.moveHandle.x-markerspan.outerWidth()/2-4,positionTop=handles.moveHandle.y-markerspan.outerHeight()/2;markerspan.css("left",positionLeft).css("top",positionTop),markerspan.data("originX",markerspan.position().left/bgRatio).data("originY",markerspan.position().top/bgRatio),this.handleElementScale(markerspan,"center")}}var shapeSVG=shape.makeSvg(svg[0]);shapeSVG.setAttribute("class","dropzone "+colourClass),this.shapes[this.shapes.length]=shape,this.shapeSVGs[this.shapeSVGs.length]=shapeSVG}},DragDropMarkersQuestion.prototype.repositionDrags=function(){var root=this.getRoot(),thisQ=this;root.find("div.draghomes .marker").not(".dragplaceholder").each((function(key,item){$(item).addClass("unneeded")})),root.find("input.choices").each((function(key,input){var choiceNo=thisQ.getChoiceNoFromElement(input),coords=thisQ.getCoords(input);if(coords.length){var drag=thisQ.getRoot().find(".draghomes span.marker.choice"+choiceNo).not(".dragplaceholder");drag.remove();for(var i=0;i{result[inputNode.id]=inputNode.value})),result},DragDropMarkersQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropMarkersQuestion.prototype.getCoords=function(inputNode){var coords=[],val=$(inputNode).val();if(""!==val)for(var coordsStrings=val.split(";"),i=0;i=bgPosition.left&&point.x=bgPosition.top&&point.y2&&void 0!==arguments[2]&&arguments[2];var dropArea=this.dropArea(),bgRatio=this.bgRatio();drag.removeClass("beingdragged").removeClass("unneeded");var dragXY=this.convertToBgImgXY(new Shapes.Point(drag.data("pagex"),drag.data("pagey")));isScaling?(drag.data("originX",dragXY.x/bgRatio).data("originY",dragXY.y/bgRatio),drag.css("left",dragXY.x).css("top",dragXY.y)):(drag.data("originX",dragXY.x).data("originY",dragXY.y),drag.css("left",dragXY.x*bgRatio).css("top",dragXY.y*bgRatio)),initialLoad||drag.data("scaleRatio",bgRatio),dropArea.append(drag),this.handleElementScale(drag,"left top")},DragDropMarkersQuestion.prototype.cloneDragIfNeeded=function(drag){var inputNode=this.getInput(drag),noOfDrags=Number(this.getClassnameNumericSuffix(inputNode,"noofdrags")),displayedDragsInDropArea=this.getRoot().find("div.droparea .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).length,displayedDragsInDragHomes=this.getRoot().find("div.draghomes .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).not(".dragplaceholder").length;if((this.isInfiniteDrag(drag)||!this.isInfiniteDrag(drag)&&displayedDragsInDropArea1;)dragsInHome.first().remove(),displayedDrags--},DragDropMarkersQuestion.prototype.getInput=function(drag){var choiceNo=this.getChoiceNoFromElement(drag);return this.getRoot().find("input.choices.choice"+choiceNo)},DragDropMarkersQuestion.prototype.bgRatio=function(){var bgImg=this.bgImage(),bgImgNaturalWidth=bgImg.get(0).naturalWidth;return bgImg.width()/bgImgNaturalWidth},DragDropMarkersQuestion.prototype.handleElementScale=function(element,type){var bgRatio=parseFloat(this.bgRatio());this.isPrinting&&(bgRatio=1),$(element).css({"-webkit-transform":"scale("+bgRatio+")","-moz-transform":"scale("+bgRatio+")","-ms-transform":"scale("+bgRatio+")","-o-transform":"scale("+bgRatio+")",transform:"scale("+bgRatio+")","transform-origin":type})},DragDropMarkersQuestion.prototype.isInfiniteDrag=function(drag){return drag.hasClass("infinite")};var questionManager={eventHandlersInitialised:!1,markerEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,visibleDropZones){if(questionManager.questions[containerId]=new DragDropMarkersQuestion(containerId,readOnly,visibleDropZones),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.markerEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("ddmarker")&&!questionContainer.classList.contains("qtype_ddmarker-readonly")&&(questionManager.addEventHandlersToMarker($(questionContainer).find("div.draghomes .marker")),questionManager.addEventHandlersToMarker($(questionContainer).find("div.droparea .marker")))}},setupEventHandlers:function(){$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToMarker:function(element){element.on("mousedown touchstart",questionManager.handleDragStart).on("keydown keypress",questionManager.handleKeyPress).focusin((function(e){questionManager.handleKeyboardFocus(e,!0)})).focusout((function(e){questionManager.handleKeyboardFocus(e,!1)}))},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},handleKeyboardFocus:function(e,isNavigating){questionManager.isKeyboardNavigation=isNavigating},fixLayoutIfThingsMoved:function(){questionManager.isKeyboardNavigation||this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.ddmarker").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); +define("qtype_ddmarker/question",["jquery","core/dragdrop","qtype_ddmarker/shapes","core/key_codes","core_form/changechecker"],(function($,dragDrop,Shapes,keys,FormChangeChecker){function DragDropMarkersQuestion(containerId,readOnly,visibleDropZones){var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.shapes=[],this.shapeSVGs=[],this.isPrinting=!1,this.questionAnswer={},readOnly&&this.getRoot().addClass("qtype_ddmarker-readonly"),thisQ.allImagesLoaded=!1,thisQ.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),thisQ.waitForAllImagesToBeLoaded()}DragDropMarkersQuestion.prototype.drawDropzones=function(){if(this.visibleDropZones.length>0){var bgImage=this.bgImage();this.getRoot().find("div.dropzones").html('');for(var svg=this.getRoot().find("svg.dropzones"),nextColourIndex=0,dropZoneNo=0;dropZoneNo'+dropZone.markertext+"");var markerspan=this.getRoot().find("div.ddarea div.markertexts span.markertext"+dropZoneNo);if(markerspan.length){var handles=shape.getHandlePositions(),positionLeft=handles.moveHandle.x-markerspan.outerWidth()/2-4,positionTop=handles.moveHandle.y-markerspan.outerHeight()/2;markerspan.css("left",positionLeft).css("top",positionTop),markerspan.data("originX",markerspan.position().left/bgRatio).data("originY",markerspan.position().top/bgRatio),this.handleElementScale(markerspan,"center")}}var shapeSVG=shape.makeSvg(svg[0]);shapeSVG.setAttribute("class","dropzone "+colourClass),this.shapes[this.shapes.length]=shape,this.shapeSVGs[this.shapeSVGs.length]=shapeSVG}},DragDropMarkersQuestion.prototype.repositionDrags=function(){var root=this.getRoot(),thisQ=this;root.find("div.draghomes .marker").not(".dragplaceholder").each((function(key,item){$(item).addClass("unneeded")})),root.find("input.choices").each((function(key,input){var choiceNo=thisQ.getChoiceNoFromElement(input),coords=thisQ.getCoords(input);if(coords.length){var drag=thisQ.getRoot().find(".draghomes span.marker.choice"+choiceNo).not(".dragplaceholder");drag.remove();for(var i=0;i{result[inputNode.id]=inputNode.value})),result},DragDropMarkersQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropMarkersQuestion.prototype.getCoords=function(inputNode){var coords=[],val=$(inputNode).val();if(""!==val)for(var coordsStrings=val.split(";"),i=0;i=bgPosition.left&&point.x=bgPosition.top&&point.y2&&void 0!==arguments[2]&&arguments[2];var dropArea=this.dropArea(),bgRatio=this.bgRatio();drag.removeClass("beingdragged").removeClass("unneeded");var dragXY=this.convertToBgImgXY(new Shapes.Point(drag.data("pagex"),drag.data("pagey")));isScaling?(drag.data("originX",dragXY.x/bgRatio).data("originY",dragXY.y/bgRatio),drag.css("left",dragXY.x).css("top",dragXY.y)):(drag.data("originX",dragXY.x).data("originY",dragXY.y),drag.css("left",dragXY.x*bgRatio).css("top",dragXY.y*bgRatio)),initialLoad||drag.data("scaleRatio",bgRatio),dropArea.append(drag),this.handleElementScale(drag,"left top")},DragDropMarkersQuestion.prototype.cloneDragIfNeeded=function(drag){var inputNode=this.getInput(drag),noOfDrags=Number(this.getClassnameNumericSuffix(inputNode,"noofdrags")),displayedDragsInDropArea=this.getRoot().find("div.droparea .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).length,displayedDragsInDragHomes=this.getRoot().find("div.draghomes .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).not(".dragplaceholder").length;if((this.isInfiniteDrag(drag)||!this.isInfiniteDrag(drag)&&displayedDragsInDropArea1;)dragsInHome.first().remove(),displayedDrags--},DragDropMarkersQuestion.prototype.getInput=function(drag){var choiceNo=this.getChoiceNoFromElement(drag);return this.getRoot().find("input.choices.choice"+choiceNo)},DragDropMarkersQuestion.prototype.bgRatio=function(){var bgImg=this.bgImage(),bgImgNaturalWidth=bgImg.get(0).naturalWidth;return bgImg.width()/bgImgNaturalWidth},DragDropMarkersQuestion.prototype.handleElementScale=function(element,type){var bgRatio=parseFloat(this.bgRatio());this.isPrinting&&(bgRatio=1),$(element).css({"-webkit-transform":"scale("+bgRatio+")","-moz-transform":"scale("+bgRatio+")","-ms-transform":"scale("+bgRatio+")","-o-transform":"scale("+bgRatio+")",transform:"scale("+bgRatio+")","transform-origin":type})},DragDropMarkersQuestion.prototype.isInfiniteDrag=function(drag){return drag.hasClass("infinite")},DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded=function(){this.allImagesLoaded||(null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId),this.getNotYetLoadedImages().length>0?this.imageLoadingTimeoutId=setTimeout((function(){this.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,this.cloneDrags(),this.repositionDrags(),this.drawDropzones()))},DragDropMarkersQuestion.prototype.getNotYetLoadedImages=function(){return this.getRoot().find(".ddmarker img.dropbackground").not((function(i,imgNode){return this.imageIsLoaded(imgNode)}))},DragDropMarkersQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,markerEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,visibleDropZones){if(questionManager.questions[containerId]=new DragDropMarkersQuestion(containerId,readOnly,visibleDropZones),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.markerEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("ddmarker")&&!questionContainer.classList.contains("qtype_ddmarker-readonly")&&(questionManager.addEventHandlersToMarker($(questionContainer).find("div.draghomes .marker")),questionManager.addEventHandlersToMarker($(questionContainer).find("div.droparea .marker")))}},setupEventHandlers:function(){$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToMarker:function(element){element.on("mousedown touchstart",questionManager.handleDragStart).on("keydown keypress",questionManager.handleKeyPress).focusin((function(e){questionManager.handleKeyboardFocus(e,!0)})).focusout((function(e){questionManager.handleKeyboardFocus(e,!1)}))},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},handleKeyboardFocus:function(e,isNavigating){questionManager.isKeyboardNavigation=isNavigating},fixLayoutIfThingsMoved:function(){questionManager.isKeyboardNavigation||this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.ddmarker").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); //# sourceMappingURL=question.min.js.map \ No newline at end of file diff --git a/question/type/ddmarker/amd/build/question.min.js.map b/question/type/ddmarker/amd/build/question.min.js.map index 1afcca1c366..f03ff661aaf 100644 --- a/question/type/ddmarker/amd/build/question.min.js.map +++ b/question/type/ddmarker/amd/build/question.min.js.map @@ -1 +1 @@ -{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Question class for drag and drop marker question type, used to support the question and preview pages.\n *\n * @module qtype_ddmarker/question\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'qtype_ddmarker/shapes',\n 'core/key_codes',\n 'core_form/changechecker'\n], function(\n $,\n dragDrop,\n Shapes,\n keys,\n FormChangeChecker\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop markers 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 {Object[]} visibleDropZones the geometry of any drop-zones to show.\n * Objects have fields shape, coords and markertext.\n * @constructor\n */\n function DragDropMarkersQuestion(containerId, readOnly, visibleDropZones) {\n var thisQ = this;\n this.containerId = containerId;\n this.visibleDropZones = visibleDropZones;\n this.shapes = [];\n this.shapeSVGs = [];\n this.isPrinting = false;\n this.questionAnswer = {};\n if (readOnly) {\n this.getRoot().addClass('qtype_ddmarker-readonly');\n }\n thisQ.cloneDrags();\n thisQ.repositionDrags();\n thisQ.drawDropzones();\n }\n\n /**\n * Draws the svg shapes of any drop zones that should be visible for feedback purposes.\n */\n DragDropMarkersQuestion.prototype.drawDropzones = function() {\n if (this.visibleDropZones.length > 0) {\n var bgImage = this.bgImage();\n\n this.getRoot().find('div.dropzones').html('');\n var svg = this.getRoot().find('svg.dropzones');\n\n var nextColourIndex = 0;\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var colourClass = 'color' + nextColourIndex;\n nextColourIndex = (nextColourIndex + 1) % 8;\n this.addDropzone(svg, dropZoneNo, colourClass);\n }\n }\n };\n\n /**\n * Adds a dropzone shape with colour, coords and link provided to the array of shapes.\n *\n * @param {jQuery} svg the SVG image to which to add this drop zone.\n * @param {int} dropZoneNo which drop-zone to add.\n * @param {string} colourClass class name\n */\n DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {\n var dropZone = this.visibleDropZones[dropZoneNo],\n shape = Shapes.make(dropZone.shape, ''),\n existingmarkertext,\n bgRatio = this.bgRatio();\n if (!shape.parse(dropZone.coords, bgRatio)) {\n return;\n }\n\n existingmarkertext = this.getRoot().find('div.markertexts span.markertext' + dropZoneNo);\n if (existingmarkertext.length) {\n if (dropZone.markertext !== '') {\n existingmarkertext.html(dropZone.markertext);\n } else {\n existingmarkertext.remove();\n }\n } else if (dropZone.markertext !== '') {\n var classnames = 'markertext markertext' + dropZoneNo;\n this.getRoot().find('div.markertexts').append('' +\n dropZone.markertext + '');\n var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n if (markerspan.length) {\n var handles = shape.getHandlePositions();\n var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;\n var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);\n markerspan\n .css('left', positionLeft)\n .css('top', positionTop);\n markerspan\n .data('originX', markerspan.position().left / bgRatio)\n .data('originY', markerspan.position().top / bgRatio);\n this.handleElementScale(markerspan, 'center');\n }\n }\n\n var shapeSVG = shape.makeSvg(svg[0]);\n shapeSVG.setAttribute('class', 'dropzone ' + colourClass);\n\n this.shapes[this.shapes.length] = shape;\n this.shapeSVGs[this.shapeSVGs.length] = shapeSVG;\n };\n\n /**\n * Draws the drag items on the page (and drop zones if required).\n * The idea is to re-draw all the drags and drops whenever there is a change\n * like a widow resize or an item dropped in place.\n */\n DragDropMarkersQuestion.prototype.repositionDrags = function() {\n var root = this.getRoot(),\n thisQ = this;\n\n root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {\n $(item).addClass('unneeded');\n });\n\n root.find('input.choices').each(function(key, input) {\n var choiceNo = thisQ.getChoiceNoFromElement(input),\n coords = thisQ.getCoords(input);\n if (coords.length) {\n var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');\n drag.remove();\n for (var i = 0; i < coords.length; i++) {\n var dragInDrop = drag.clone();\n dragInDrop.data('pagex', coords[i].x).data('pagey', coords[i].y);\n // We always save the coordinates in the 1:1 ratio.\n // So we need to set the scale ratio to 1 for the initial load.\n dragInDrop.data('scaleRatio', 1);\n thisQ.sendDragToDrop(dragInDrop, false, true);\n }\n thisQ.getDragClone(drag).addClass('active');\n thisQ.cloneDragIfNeeded(drag);\n }\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropMarkersQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.choices').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropMarkersQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Determine what drag items need to be shown and\n * return coords of all drag items except any that are currently being dragged\n * based on contents of hidden inputs and whether drags are 'infinite' or how many\n * drags should be shown.\n *\n * @param {jQuery} inputNode\n * @returns {Point[]} coordinates of however many copies of the drag item should be shown.\n */\n DragDropMarkersQuestion.prototype.getCoords = function(inputNode) {\n var coords = [],\n val = $(inputNode).val();\n if (val !== '') {\n var coordsStrings = val.split(';');\n for (var i = 0; i < coordsStrings.length; i++) {\n coords[i] = this.convertToWindowXY(Shapes.Point.parse(coordsStrings[i]));\n }\n }\n return coords;\n };\n\n /**\n * Converts the relative x and y position coordinates into\n * absolute x and y position coordinates.\n *\n * @param {Point} point relative to the background image.\n * @returns {Point} point relative to the page.\n */\n DragDropMarkersQuestion.prototype.convertToWindowXY = function(point) {\n var bgImage = this.bgImage();\n // The +1 seems rather odd, but seems to give the best results in\n // the three main browsers at a range of zoom levels.\n // (Its due to the 1px border around the image, that shifts the\n // image pixels by 1 down and to the left.)\n return point.offset(bgImage.offset().left + 1, bgImage.offset().top + 1);\n };\n\n /**\n * Utility function converting window coordinates to relative to the\n * background image coordinates.\n *\n * @param {Point} point relative to the page.\n * @returns {Point} point relative to the background image.\n */\n DragDropMarkersQuestion.prototype.convertToBgImgXY = function(point) {\n var bgImage = this.bgImage();\n return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);\n };\n\n /**\n * Is the point within the background image?\n *\n * @param {Point} point relative to the BG image.\n * @return {boolean} true it they are.\n */\n DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {\n var bgImage = this.bgImage();\n var bgPosition = bgImage.offset();\n\n return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()\n && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();\n };\n\n /**\n * Get the outer div for this question.\n * @returns {jQuery} containing that div.\n */\n DragDropMarkersQuestion.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 DragDropMarkersQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n DragDropMarkersQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n dragged = $(e.target).closest('.marker');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragged.addClass('beingdragged').css('transform', '');\n\n var placed = !dragged.hasClass('unneeded');\n if (!placed) {\n var hiddenDrag = thisQ.getDragClone(dragged);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n dragged.offset(hiddenDrag.offset());\n }\n }\n\n dragDrop.start(e, dragged, function() {\n void (1);\n }, function(x, y, dragged) {\n thisQ.dragEnd(dragged);\n });\n };\n\n /**\n * Functionality at the end of a drag drop.\n * @param {jQuery} dragged the marker that was dragged.\n */\n DragDropMarkersQuestion.prototype.dragEnd = function(dragged) {\n var placed = false,\n choiceNo = this.getChoiceNoFromElement(dragged),\n bgRatio = this.bgRatio(),\n dragXY;\n\n dragged.data('pagex', dragged.offset().left).data('pagey', dragged.offset().top);\n dragXY = new Shapes.Point(dragged.data('pagex'), dragged.data('pagey'));\n if (this.coordsInBgImg(dragXY)) {\n this.sendDragToDrop(dragged, true);\n placed = true;\n\n // It seems that the dragdrop sometimes leaves the drag\n // one pixel out of position. Put it in exactly the right place.\n var bgImgXY = this.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);\n }\n\n if (!placed) {\n this.sendDragHome(dragged);\n this.removeDragIfNeeded(dragged);\n } else {\n this.cloneDragIfNeeded(dragged);\n }\n\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Save the coordinates for a dropped item in the form field.\n * @param {Number} choiceNo which copy of the choice this was.\n */\n DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) {\n var coords = [],\n items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),\n thiQ = this,\n bgRatio = this.bgRatio();\n\n if (items.length) {\n items.each(function() {\n var drag = $(this);\n if (!drag.hasClass('beingdragged')) {\n if (drag.data('scaleRatio') !== bgRatio) {\n // The scale ratio for the draggable item was changed. We need to update that.\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n }\n var dragXY = new Shapes.Point(drag.data('pagex'), drag.data('pagey'));\n if (thiQ.coordsInBgImg(dragXY)) {\n var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n coords[coords.length] = bgImgXY;\n }\n }\n });\n }\n\n this.getRoot().find('input.choice' + choiceNo).val(coords.join(';'));\n if (this.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n this.questionAnswer = this.getQuestionAnsweredValues();\n }\n };\n\n /**\n * Handle key down / press events on markers.\n * @param {KeyboardEvent} e\n */\n DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {\n var drag = $(e.target).closest('.marker'),\n point = new Shapes.Point(drag.offset().left, drag.offset().top),\n choiceNo = this.getChoiceNoFromElement(drag);\n\n switch (e.keyCode) {\n case keys.arrowLeft:\n case 65: // A.\n point.x -= 1;\n break;\n case keys.arrowRight:\n case 68: // D.\n point.x += 1;\n break;\n case keys.arrowDown:\n case 83: // S.\n point.y += 1;\n break;\n case keys.arrowUp:\n case 87: // W.\n point.y -= 1;\n break;\n case keys.space:\n case keys.escape:\n point = null;\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n if (point !== null) {\n point = this.constrainToBgImg(point);\n drag.offset({'left': point.x, 'top': point.y});\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());\n if (this.coordsInBgImg(new Shapes.Point(drag.offset().left, drag.offset().top))) {\n if (drag.hasClass('unneeded')) {\n this.sendDragToDrop(drag, true);\n var hiddenDrag = this.getDragClone(drag);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n }\n this.cloneDragIfNeeded(drag);\n }\n }\n } else {\n drag.css('left', '').css('top', '');\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n this.sendDragHome(drag);\n this.removeDragIfNeeded(drag);\n }\n drag.focus();\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Makes sure the dragged item always exists within the background image area.\n *\n * @param {Point} windowxy\n * @returns {Point} coordinates\n */\n DragDropMarkersQuestion.prototype.constrainToBgImg = function(windowxy) {\n var bgImg = this.bgImage(),\n bgImgXY = this.convertToBgImgXY(windowxy);\n bgImgXY.x = Math.max(0, bgImgXY.x);\n bgImgXY.y = Math.max(0, bgImgXY.y);\n bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);\n bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);\n return this.convertToWindowXY(bgImgXY);\n };\n\n /**\n * Returns the choice number for a node.\n *\n * @param {Element|jQuery} node\n * @returns {Number}\n */\n DragDropMarkersQuestion.prototype.getChoiceNoFromElement = function(node) {\n return Number(this.getClassnameNumericSuffix(node, 'choice'));\n };\n\n /**\n * Returns the numeric part of a class with the given prefix.\n *\n * @param {Element|jQuery} node\n * @param {String} prefix\n * @returns {Number|null}\n */\n DragDropMarkersQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = $(node).attr('class');\n if (classes !== undefined && 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 * Handle when the window is resized.\n */\n DragDropMarkersQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('div.droparea .marker').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 this.getRoot().find('div.droparea svg.dropzones')\n .width(this.bgImage().width())\n .height(this.bgImage().height());\n\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var dropZone = thisQ.visibleDropZones[dropZoneNo];\n var originCoords = dropZone.coords;\n var shape = thisQ.shapes[dropZoneNo];\n var shapeSVG = thisQ.shapeSVGs[dropZoneNo];\n shape.parse(originCoords, bgRatio);\n shape.updateSvg(shapeSVG);\n\n var handles = shape.getHandlePositions();\n var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n markerSpan\n .css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)\n .css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));\n thisQ.handleElementScale(markerSpan, 'center');\n }\n };\n\n /**\n * Clone the drag.\n */\n DragDropMarkersQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('marker');\n placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));\n placeHolder.addClass(thisQ.getDragNoClass(drag, false));\n placeHolder.addClass('dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Get the drag number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the drag number.\n */\n DragDropMarkersQuestion.prototype.getDragNo = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'dragno');\n };\n\n /**\n * Get the drag number prefix of a drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} includeSelector include the CSS selector prefix or not.\n * @return {String} Class name\n */\n DragDropMarkersQuestion.prototype.getDragNoClass = function(drag, includeSelector) {\n var className = 'dragno' + this.getDragNo(drag);\n if (this.isInfiniteDrag(drag)) {\n className = 'infinite';\n }\n\n if (includeSelector) {\n return '.' + className;\n }\n\n return className;\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 DragDropMarkersQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draghomes' + ' span.marker' +\n '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');\n };\n\n /**\n * Get the drop area element.\n * @returns {jQuery} droparea element.\n */\n DragDropMarkersQuestion.prototype.dropArea = function() {\n return this.getRoot().find('div.droparea');\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropMarkersQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('beingdragged')\n .addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n var placeHolder = this.getDragClone(drag);\n placeHolder.after(drag);\n placeHolder.removeClass('active');\n };\n\n /**\n * Animate a drag item into a given place.\n *\n * @param {jQuery} drag the item to place.\n * @param {boolean} isScaling Scaling or not.\n * @param {boolean} initialLoad Whether it is the initial load or not.\n */\n DragDropMarkersQuestion.prototype.sendDragToDrop = function(drag, isScaling, initialLoad = false) {\n var dropArea = this.dropArea(),\n bgRatio = this.bgRatio();\n drag.removeClass('beingdragged').removeClass('unneeded');\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n if (isScaling) {\n drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);\n drag.css('left', dragXY.x).css('top', dragXY.y);\n } else {\n drag.data('originX', dragXY.x).data('originY', dragXY.y);\n drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);\n }\n // We need to save the original scale ratio for each draggable item.\n if (!initialLoad) {\n // Only set the scale ratio for a current being-dragged item, not for the initial loading.\n drag.data('scaleRatio', bgRatio);\n }\n dropArea.append(drag);\n this.handleElementScale(drag, 'left top');\n };\n\n /**\n * Clone the drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.cloneDragIfNeeded = function(drag) {\n var inputNode = this.getInput(drag),\n noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),\n displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,\n displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;\n\n if ((this.isInfiniteDrag(drag) ||\n !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {\n var dragClone = drag.clone();\n dragClone.addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n this.getDragClone(drag)\n .removeClass('active')\n .after(dragClone);\n questionManager.addEventHandlersToMarker(dragClone);\n }\n };\n\n /**\n * Remove the clone drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {\n var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');\n var displayedDrags = dragsInHome.length;\n while (displayedDrags > 1) {\n dragsInHome.first().remove();\n displayedDrags--;\n }\n };\n\n /**\n * Get the input belong to drag.\n *\n * @param {jQuery} drag the item to place.\n * @returns {jQuery} input element.\n */\n DragDropMarkersQuestion.prototype.getInput = function(drag) {\n var choiceNo = this.getChoiceNoFromElement(drag);\n return this.getRoot().find('input.choices.choice' + choiceNo);\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropMarkersQuestion.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 DragDropMarkersQuestion.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 * Check if the given drag is in infinite mode or not.\n *\n * @param {jQuery} drag The drag item need to check.\n */\n DragDropMarkersQuestion.prototype.isInfiniteDrag = function(drag) {\n return drag.hasClass('infinite');\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\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 marker event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n markerEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation.\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 * @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 {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.\n */\n init: function(containerId, readOnly, visibleDropZones) {\n questionManager.questions[containerId] =\n new DragDropMarkersQuestion(containerId, readOnly, visibleDropZones);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.markerEventHandlersInitialised[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('ddmarker') &&\n !questionContainer.classList.contains('qtype_ddmarker-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker'));\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker'));\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 $(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 event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToMarker: function(element) {\n element\n .on('mousedown touchstart', questionManager.handleDragStart)\n .on('keydown keypress', questionManager.handleKeyPress)\n .focusin(function(e) {\n questionManager.handleKeyboardFocus(e, true);\n })\n .focusout(function(e) {\n questionManager.handleKeyboardFocus(e, false);\n });\n },\n\n /**\n * Handle mouse down / touch start events on markers.\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 markers.\n * @param {Event} e\n */\n handleKeyPress: function(e) {\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 * Handle focus lost events on markers.\n * @param {Event} e\n * @param {boolean} isNavigating\n */\n handleKeyboardFocus: function(e, isNavigating) {\n questionManager.isKeyboardNavigation = isNavigating;\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 if (!questionManager.isKeyboardNavigation) {\n this.handleWindowResize(questionManager.isPrinting);\n }\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 * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropMarkersQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddmarker').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddmarker/question\n */\n return {\n /**\n * Initialise one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {String} bgImgUrl the URL of the background image.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {String[]} visibleDropZones the geometry of any drop-zones to show.\n */\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","Shapes","keys","FormChangeChecker","DragDropMarkersQuestion","containerId","readOnly","visibleDropZones","shapes","shapeSVGs","isPrinting","questionAnswer","getRoot","addClass","this","cloneDrags","repositionDrags","drawDropzones","prototype","length","bgImage","find","html","outerWidth","outerHeight","svg","nextColourIndex","dropZoneNo","colourClass","addDropzone","existingmarkertext","dropZone","shape","make","bgRatio","parse","coords","markertext","remove","classnames","append","markerspan","handles","getHandlePositions","positionLeft","moveHandle","x","positionTop","y","css","data","position","left","top","handleElementScale","shapeSVG","makeSvg","setAttribute","root","thisQ","not","each","key","item","input","choiceNo","getChoiceNoFromElement","getCoords","drag","i","dragInDrop","clone","sendDragToDrop","getDragClone","cloneDragIfNeeded","getQuestionAnsweredValues","result","inputNode","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","val","coordsStrings","split","convertToWindowXY","Point","point","offset","convertToBgImgXY","coordsInBgImg","bgPosition","width","height","document","getElementById","handleDragStart","e","dragged","target","closest","prepare","start","hasClass","hiddenDrag","dragEnd","dragXY","placed","bgImgXY","sendDragHome","removeDragIfNeeded","saveCoordsForChoice","items","thiQ","join","questionManager","handleFormDirty","handleKeyPress","keyCode","arrowLeft","arrowRight","arrowDown","arrowUp","space","escape","preventDefault","constrainToBgImg","focus","windowxy","bgImg","Math","max","min","node","Number","getClassnameNumericSuffix","prefix","classes","attr","undefined","classesarr","index","RegExp","test","match","exec","handleResize","parseFloat","originCoords","updateSvg","markerSpan","draghome","placeHolder","removeClass","getDragNoClass","before","getDragNo","includeSelector","className","isInfiniteDrag","dropArea","after","isScaling","initialLoad","getInput","noOfDrags","displayedDragsInDropArea","displayedDragsInDragHomes","dragClone","addEventHandlersToMarker","dragsInHome","displayedDrags","first","bgImgNaturalWidth","get","naturalWidth","element","type","eventHandlersInitialised","markerEventHandlersInitialised","isKeyboardNavigation","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","classList","contains","window","on","handleWindowResize","addEventListener","setTimeout","fixLayoutIfThingsMoved","focusin","handleKeyboardFocus","focusout","question","getQuestionForEvent","isNavigating","currentTarget","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Question class for drag and drop marker question type, used to support the question and preview pages.\n *\n * @module qtype_ddmarker/question\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'qtype_ddmarker/shapes',\n 'core/key_codes',\n 'core_form/changechecker'\n], function(\n $,\n dragDrop,\n Shapes,\n keys,\n FormChangeChecker\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop markers 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 {Object[]} visibleDropZones the geometry of any drop-zones to show.\n * Objects have fields shape, coords and markertext.\n * @constructor\n */\n function DragDropMarkersQuestion(containerId, readOnly, visibleDropZones) {\n var thisQ = this;\n this.containerId = containerId;\n this.visibleDropZones = visibleDropZones;\n this.shapes = [];\n this.shapeSVGs = [];\n this.isPrinting = false;\n this.questionAnswer = {};\n if (readOnly) {\n this.getRoot().addClass('qtype_ddmarker-readonly');\n }\n thisQ.allImagesLoaded = false;\n thisQ.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n thisQ.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Draws the svg shapes of any drop zones that should be visible for feedback purposes.\n */\n DragDropMarkersQuestion.prototype.drawDropzones = function() {\n if (this.visibleDropZones.length > 0) {\n var bgImage = this.bgImage();\n\n this.getRoot().find('div.dropzones').html('');\n var svg = this.getRoot().find('svg.dropzones');\n\n var nextColourIndex = 0;\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var colourClass = 'color' + nextColourIndex;\n nextColourIndex = (nextColourIndex + 1) % 8;\n this.addDropzone(svg, dropZoneNo, colourClass);\n }\n }\n };\n\n /**\n * Adds a dropzone shape with colour, coords and link provided to the array of shapes.\n *\n * @param {jQuery} svg the SVG image to which to add this drop zone.\n * @param {int} dropZoneNo which drop-zone to add.\n * @param {string} colourClass class name\n */\n DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {\n var dropZone = this.visibleDropZones[dropZoneNo],\n shape = Shapes.make(dropZone.shape, ''),\n existingmarkertext,\n bgRatio = this.bgRatio();\n if (!shape.parse(dropZone.coords, bgRatio)) {\n return;\n }\n\n existingmarkertext = this.getRoot().find('div.markertexts span.markertext' + dropZoneNo);\n if (existingmarkertext.length) {\n if (dropZone.markertext !== '') {\n existingmarkertext.html(dropZone.markertext);\n } else {\n existingmarkertext.remove();\n }\n } else if (dropZone.markertext !== '') {\n var classnames = 'markertext markertext' + dropZoneNo;\n this.getRoot().find('div.markertexts').append('' +\n dropZone.markertext + '');\n var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n if (markerspan.length) {\n var handles = shape.getHandlePositions();\n var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;\n var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);\n markerspan\n .css('left', positionLeft)\n .css('top', positionTop);\n markerspan\n .data('originX', markerspan.position().left / bgRatio)\n .data('originY', markerspan.position().top / bgRatio);\n this.handleElementScale(markerspan, 'center');\n }\n }\n\n var shapeSVG = shape.makeSvg(svg[0]);\n shapeSVG.setAttribute('class', 'dropzone ' + colourClass);\n\n this.shapes[this.shapes.length] = shape;\n this.shapeSVGs[this.shapeSVGs.length] = shapeSVG;\n };\n\n /**\n * Draws the drag items on the page (and drop zones if required).\n * The idea is to re-draw all the drags and drops whenever there is a change\n * like a widow resize or an item dropped in place.\n */\n DragDropMarkersQuestion.prototype.repositionDrags = function() {\n var root = this.getRoot(),\n thisQ = this;\n\n root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {\n $(item).addClass('unneeded');\n });\n\n root.find('input.choices').each(function(key, input) {\n var choiceNo = thisQ.getChoiceNoFromElement(input),\n coords = thisQ.getCoords(input);\n if (coords.length) {\n var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');\n drag.remove();\n for (var i = 0; i < coords.length; i++) {\n var dragInDrop = drag.clone();\n dragInDrop.data('pagex', coords[i].x).data('pagey', coords[i].y);\n // We always save the coordinates in the 1:1 ratio.\n // So we need to set the scale ratio to 1 for the initial load.\n dragInDrop.data('scaleRatio', 1);\n thisQ.sendDragToDrop(dragInDrop, false, true);\n }\n thisQ.getDragClone(drag).addClass('active');\n thisQ.cloneDragIfNeeded(drag);\n }\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropMarkersQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.choices').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropMarkersQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Determine what drag items need to be shown and\n * return coords of all drag items except any that are currently being dragged\n * based on contents of hidden inputs and whether drags are 'infinite' or how many\n * drags should be shown.\n *\n * @param {jQuery} inputNode\n * @returns {Point[]} coordinates of however many copies of the drag item should be shown.\n */\n DragDropMarkersQuestion.prototype.getCoords = function(inputNode) {\n var coords = [],\n val = $(inputNode).val();\n if (val !== '') {\n var coordsStrings = val.split(';');\n for (var i = 0; i < coordsStrings.length; i++) {\n coords[i] = this.convertToWindowXY(Shapes.Point.parse(coordsStrings[i]));\n }\n }\n return coords;\n };\n\n /**\n * Converts the relative x and y position coordinates into\n * absolute x and y position coordinates.\n *\n * @param {Point} point relative to the background image.\n * @returns {Point} point relative to the page.\n */\n DragDropMarkersQuestion.prototype.convertToWindowXY = function(point) {\n var bgImage = this.bgImage();\n // The +1 seems rather odd, but seems to give the best results in\n // the three main browsers at a range of zoom levels.\n // (Its due to the 1px border around the image, that shifts the\n // image pixels by 1 down and to the left.)\n return point.offset(bgImage.offset().left + 1, bgImage.offset().top + 1);\n };\n\n /**\n * Utility function converting window coordinates to relative to the\n * background image coordinates.\n *\n * @param {Point} point relative to the page.\n * @returns {Point} point relative to the background image.\n */\n DragDropMarkersQuestion.prototype.convertToBgImgXY = function(point) {\n var bgImage = this.bgImage();\n return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);\n };\n\n /**\n * Is the point within the background image?\n *\n * @param {Point} point relative to the BG image.\n * @return {boolean} true it they are.\n */\n DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {\n var bgImage = this.bgImage();\n var bgPosition = bgImage.offset();\n\n return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()\n && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();\n };\n\n /**\n * Get the outer div for this question.\n * @returns {jQuery} containing that div.\n */\n DragDropMarkersQuestion.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 DragDropMarkersQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n DragDropMarkersQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n dragged = $(e.target).closest('.marker');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragged.addClass('beingdragged').css('transform', '');\n\n var placed = !dragged.hasClass('unneeded');\n if (!placed) {\n var hiddenDrag = thisQ.getDragClone(dragged);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n dragged.offset(hiddenDrag.offset());\n }\n }\n\n dragDrop.start(e, dragged, function() {\n void (1);\n }, function(x, y, dragged) {\n thisQ.dragEnd(dragged);\n });\n };\n\n /**\n * Functionality at the end of a drag drop.\n * @param {jQuery} dragged the marker that was dragged.\n */\n DragDropMarkersQuestion.prototype.dragEnd = function(dragged) {\n var placed = false,\n choiceNo = this.getChoiceNoFromElement(dragged),\n bgRatio = this.bgRatio(),\n dragXY;\n\n dragged.data('pagex', dragged.offset().left).data('pagey', dragged.offset().top);\n dragXY = new Shapes.Point(dragged.data('pagex'), dragged.data('pagey'));\n if (this.coordsInBgImg(dragXY)) {\n this.sendDragToDrop(dragged, true);\n placed = true;\n\n // It seems that the dragdrop sometimes leaves the drag\n // one pixel out of position. Put it in exactly the right place.\n var bgImgXY = this.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);\n }\n\n if (!placed) {\n this.sendDragHome(dragged);\n this.removeDragIfNeeded(dragged);\n } else {\n this.cloneDragIfNeeded(dragged);\n }\n\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Save the coordinates for a dropped item in the form field.\n * @param {Number} choiceNo which copy of the choice this was.\n */\n DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) {\n var coords = [],\n items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),\n thiQ = this,\n bgRatio = this.bgRatio();\n\n if (items.length) {\n items.each(function() {\n var drag = $(this);\n if (!drag.hasClass('beingdragged')) {\n if (drag.data('scaleRatio') !== bgRatio) {\n // The scale ratio for the draggable item was changed. We need to update that.\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n }\n var dragXY = new Shapes.Point(drag.data('pagex'), drag.data('pagey'));\n if (thiQ.coordsInBgImg(dragXY)) {\n var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n coords[coords.length] = bgImgXY;\n }\n }\n });\n }\n\n this.getRoot().find('input.choice' + choiceNo).val(coords.join(';'));\n if (this.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n this.questionAnswer = this.getQuestionAnsweredValues();\n }\n };\n\n /**\n * Handle key down / press events on markers.\n * @param {KeyboardEvent} e\n */\n DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {\n var drag = $(e.target).closest('.marker'),\n point = new Shapes.Point(drag.offset().left, drag.offset().top),\n choiceNo = this.getChoiceNoFromElement(drag);\n\n switch (e.keyCode) {\n case keys.arrowLeft:\n case 65: // A.\n point.x -= 1;\n break;\n case keys.arrowRight:\n case 68: // D.\n point.x += 1;\n break;\n case keys.arrowDown:\n case 83: // S.\n point.y += 1;\n break;\n case keys.arrowUp:\n case 87: // W.\n point.y -= 1;\n break;\n case keys.space:\n case keys.escape:\n point = null;\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n if (point !== null) {\n point = this.constrainToBgImg(point);\n drag.offset({'left': point.x, 'top': point.y});\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());\n if (this.coordsInBgImg(new Shapes.Point(drag.offset().left, drag.offset().top))) {\n if (drag.hasClass('unneeded')) {\n this.sendDragToDrop(drag, true);\n var hiddenDrag = this.getDragClone(drag);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n }\n this.cloneDragIfNeeded(drag);\n }\n }\n } else {\n drag.css('left', '').css('top', '');\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n this.sendDragHome(drag);\n this.removeDragIfNeeded(drag);\n }\n drag.focus();\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Makes sure the dragged item always exists within the background image area.\n *\n * @param {Point} windowxy\n * @returns {Point} coordinates\n */\n DragDropMarkersQuestion.prototype.constrainToBgImg = function(windowxy) {\n var bgImg = this.bgImage(),\n bgImgXY = this.convertToBgImgXY(windowxy);\n bgImgXY.x = Math.max(0, bgImgXY.x);\n bgImgXY.y = Math.max(0, bgImgXY.y);\n bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);\n bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);\n return this.convertToWindowXY(bgImgXY);\n };\n\n /**\n * Returns the choice number for a node.\n *\n * @param {Element|jQuery} node\n * @returns {Number}\n */\n DragDropMarkersQuestion.prototype.getChoiceNoFromElement = function(node) {\n return Number(this.getClassnameNumericSuffix(node, 'choice'));\n };\n\n /**\n * Returns the numeric part of a class with the given prefix.\n *\n * @param {Element|jQuery} node\n * @param {String} prefix\n * @returns {Number|null}\n */\n DragDropMarkersQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = $(node).attr('class');\n if (classes !== undefined && 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 * Handle when the window is resized.\n */\n DragDropMarkersQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('div.droparea .marker').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 this.getRoot().find('div.droparea svg.dropzones')\n .width(this.bgImage().width())\n .height(this.bgImage().height());\n\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var dropZone = thisQ.visibleDropZones[dropZoneNo];\n var originCoords = dropZone.coords;\n var shape = thisQ.shapes[dropZoneNo];\n var shapeSVG = thisQ.shapeSVGs[dropZoneNo];\n shape.parse(originCoords, bgRatio);\n shape.updateSvg(shapeSVG);\n\n var handles = shape.getHandlePositions();\n var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n markerSpan\n .css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)\n .css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));\n thisQ.handleElementScale(markerSpan, 'center');\n }\n };\n\n /**\n * Clone the drag.\n */\n DragDropMarkersQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('marker');\n placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));\n placeHolder.addClass(thisQ.getDragNoClass(drag, false));\n placeHolder.addClass('dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Get the drag number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the drag number.\n */\n DragDropMarkersQuestion.prototype.getDragNo = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'dragno');\n };\n\n /**\n * Get the drag number prefix of a drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} includeSelector include the CSS selector prefix or not.\n * @return {String} Class name\n */\n DragDropMarkersQuestion.prototype.getDragNoClass = function(drag, includeSelector) {\n var className = 'dragno' + this.getDragNo(drag);\n if (this.isInfiniteDrag(drag)) {\n className = 'infinite';\n }\n\n if (includeSelector) {\n return '.' + className;\n }\n\n return className;\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 DragDropMarkersQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draghomes' + ' span.marker' +\n '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');\n };\n\n /**\n * Get the drop area element.\n * @returns {jQuery} droparea element.\n */\n DragDropMarkersQuestion.prototype.dropArea = function() {\n return this.getRoot().find('div.droparea');\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropMarkersQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('beingdragged')\n .addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n var placeHolder = this.getDragClone(drag);\n placeHolder.after(drag);\n placeHolder.removeClass('active');\n };\n\n /**\n * Animate a drag item into a given place.\n *\n * @param {jQuery} drag the item to place.\n * @param {boolean} isScaling Scaling or not.\n * @param {boolean} initialLoad Whether it is the initial load or not.\n */\n DragDropMarkersQuestion.prototype.sendDragToDrop = function(drag, isScaling, initialLoad = false) {\n var dropArea = this.dropArea(),\n bgRatio = this.bgRatio();\n drag.removeClass('beingdragged').removeClass('unneeded');\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n if (isScaling) {\n drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);\n drag.css('left', dragXY.x).css('top', dragXY.y);\n } else {\n drag.data('originX', dragXY.x).data('originY', dragXY.y);\n drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);\n }\n // We need to save the original scale ratio for each draggable item.\n if (!initialLoad) {\n // Only set the scale ratio for a current being-dragged item, not for the initial loading.\n drag.data('scaleRatio', bgRatio);\n }\n dropArea.append(drag);\n this.handleElementScale(drag, 'left top');\n };\n\n /**\n * Clone the drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.cloneDragIfNeeded = function(drag) {\n var inputNode = this.getInput(drag),\n noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),\n displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,\n displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;\n\n if ((this.isInfiniteDrag(drag) ||\n !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {\n var dragClone = drag.clone();\n dragClone.addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n this.getDragClone(drag)\n .removeClass('active')\n .after(dragClone);\n questionManager.addEventHandlersToMarker(dragClone);\n }\n };\n\n /**\n * Remove the clone drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {\n var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');\n var displayedDrags = dragsInHome.length;\n while (displayedDrags > 1) {\n dragsInHome.first().remove();\n displayedDrags--;\n }\n };\n\n /**\n * Get the input belong to drag.\n *\n * @param {jQuery} drag the item to place.\n * @returns {jQuery} input element.\n */\n DragDropMarkersQuestion.prototype.getInput = function(drag) {\n var choiceNo = this.getChoiceNoFromElement(drag);\n return this.getRoot().find('input.choices.choice' + choiceNo);\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropMarkersQuestion.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 DragDropMarkersQuestion.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 * Check if the given drag is in infinite mode or not.\n *\n * @param {jQuery} drag The drag item need to check.\n */\n DragDropMarkersQuestion.prototype.isInfiniteDrag = function(drag) {\n return drag.hasClass('infinite');\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 DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded = function() {\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 this.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 this.cloneDrags();\n this.repositionDrags();\n this.drawDropzones();\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 DragDropMarkersQuestion.prototype.getNotYetLoadedImages = function() {\n return this.getRoot().find('.ddmarker img.dropbackground').not(function(i, imgNode) {\n return this.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 DragDropMarkersQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\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 marker event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n markerEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation.\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 * @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 {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.\n */\n init: function(containerId, readOnly, visibleDropZones) {\n questionManager.questions[containerId] =\n new DragDropMarkersQuestion(containerId, readOnly, visibleDropZones);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.markerEventHandlersInitialised[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('ddmarker') &&\n !questionContainer.classList.contains('qtype_ddmarker-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker'));\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker'));\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 $(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 event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToMarker: function(element) {\n element\n .on('mousedown touchstart', questionManager.handleDragStart)\n .on('keydown keypress', questionManager.handleKeyPress)\n .focusin(function(e) {\n questionManager.handleKeyboardFocus(e, true);\n })\n .focusout(function(e) {\n questionManager.handleKeyboardFocus(e, false);\n });\n },\n\n /**\n * Handle mouse down / touch start events on markers.\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 markers.\n * @param {Event} e\n */\n handleKeyPress: function(e) {\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 * Handle focus lost events on markers.\n * @param {Event} e\n * @param {boolean} isNavigating\n */\n handleKeyboardFocus: function(e, isNavigating) {\n questionManager.isKeyboardNavigation = isNavigating;\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 if (!questionManager.isKeyboardNavigation) {\n this.handleWindowResize(questionManager.isPrinting);\n }\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 * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropMarkersQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddmarker').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddmarker/question\n */\n return {\n /**\n * Initialise one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {String} bgImgUrl the URL of the background image.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {String[]} visibleDropZones the geometry of any drop-zones to show.\n */\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","Shapes","keys","FormChangeChecker","DragDropMarkersQuestion","containerId","readOnly","visibleDropZones","thisQ","this","shapes","shapeSVGs","isPrinting","questionAnswer","getRoot","addClass","allImagesLoaded","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","drawDropzones","length","bgImage","find","html","outerWidth","outerHeight","svg","nextColourIndex","dropZoneNo","colourClass","addDropzone","existingmarkertext","dropZone","shape","make","bgRatio","parse","coords","markertext","remove","classnames","append","markerspan","handles","getHandlePositions","positionLeft","moveHandle","x","positionTop","y","css","data","position","left","top","handleElementScale","shapeSVG","makeSvg","setAttribute","repositionDrags","root","not","each","key","item","input","choiceNo","getChoiceNoFromElement","getCoords","drag","i","dragInDrop","clone","sendDragToDrop","getDragClone","cloneDragIfNeeded","getQuestionAnsweredValues","result","inputNode","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","val","coordsStrings","split","convertToWindowXY","Point","point","offset","convertToBgImgXY","coordsInBgImg","bgPosition","width","height","document","getElementById","handleDragStart","e","dragged","target","closest","prepare","start","hasClass","hiddenDrag","dragEnd","dragXY","placed","bgImgXY","sendDragHome","removeDragIfNeeded","saveCoordsForChoice","items","thiQ","join","questionManager","handleFormDirty","handleKeyPress","keyCode","arrowLeft","arrowRight","arrowDown","arrowUp","space","escape","preventDefault","constrainToBgImg","focus","windowxy","bgImg","Math","max","min","node","Number","getClassnameNumericSuffix","prefix","classes","attr","undefined","classesarr","index","RegExp","test","match","exec","handleResize","parseFloat","originCoords","updateSvg","markerSpan","cloneDrags","draghome","placeHolder","removeClass","getDragNoClass","before","getDragNo","includeSelector","className","isInfiniteDrag","dropArea","after","isScaling","initialLoad","getInput","noOfDrags","displayedDragsInDropArea","displayedDragsInDragHomes","dragClone","addEventHandlersToMarker","dragsInHome","displayedDrags","first","bgImgNaturalWidth","get","naturalWidth","element","type","imageLoadingTimeoutId","clearTimeout","setTimeout","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","eventHandlersInitialised","markerEventHandlersInitialised","isKeyboardNavigation","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","classList","contains","window","on","handleWindowResize","addEventListener","fixLayoutIfThingsMoved","focusin","handleKeyboardFocus","focusout","question","getQuestionForEvent","isNavigating","currentTarget","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file diff --git a/question/type/ddmarker/amd/src/question.js b/question/type/ddmarker/amd/src/question.js index b75d8cf77f6..abaf464c2f8 100644 --- a/question/type/ddmarker/amd/src/question.js +++ b/question/type/ddmarker/amd/src/question.js @@ -57,9 +57,11 @@ define([ if (readOnly) { this.getRoot().addClass('qtype_ddmarker-readonly'); } - thisQ.cloneDrags(); - thisQ.repositionDrags(); - thisQ.drawDropzones(); + thisQ.allImagesLoaded = false; + thisQ.getNotYetLoadedImages().one('load', function() { + thisQ.waitForAllImagesToBeLoaded(); + }); + thisQ.waitForAllImagesToBeLoaded(); } /** @@ -737,6 +739,63 @@ define([ return drag.hasClass('infinite'); }; + /** + * Waits until all images are loaded before calling setupQuestion(). + * + * This function is called from the onLoad of each image, and also polls with + * a time-out, because image on-loads are allegedly unreliable. + */ + DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded = function() { + + // This method may get called multiple times (via image on-loads or timeouts. + // If we are already done, don't do it again. + if (this.allImagesLoaded) { + return; + } + + // Clear any current timeout, if set. + if (this.imageLoadingTimeoutId !== null) { + clearTimeout(this.imageLoadingTimeoutId); + } + + // If we have not yet loaded all images, set a timeout to + // call ourselves again, since apparently images on-load + // events are flakey. + if (this.getNotYetLoadedImages().length > 0) { + this.imageLoadingTimeoutId = setTimeout(function() { + this.waitForAllImagesToBeLoaded(); + }, 100); + return; + } + + // We now have all images. Carry on, but only after giving the layout a chance to settle down. + this.allImagesLoaded = true; + this.cloneDrags(); + this.repositionDrags(); + this.drawDropzones(); + }; + + /** + * Get any of the images in the drag-drop area that are not yet fully loaded. + * + * @returns {jQuery} those images. + */ + DragDropMarkersQuestion.prototype.getNotYetLoadedImages = function() { + return this.getRoot().find('.ddmarker img.dropbackground').not(function(i, imgNode) { + return this.imageIsLoaded(imgNode); + }); + }; + + /** + * Check if an image has loaded without errors. + * + * @param {HTMLImageElement} imgElement an image. + * @returns {boolean} true if this image has loaded without errors. + */ + DragDropMarkersQuestion.prototype.imageIsLoaded = function(imgElement) { + return imgElement.complete && imgElement.naturalHeight !== 0; + }; + /** * Singleton that tracks all the DragDropToTextQuestions on this page, and deals * with event dispatching. From a42c52bf4e48e99ca5401a18ff21ed86418b242d Mon Sep 17 00:00:00 2001 From: hieuvu Date: Fri, 2 Dec 2022 12:40:47 +0700 Subject: [PATCH 2/2] MDL-76106 qtype_ddmarker: fix the missing maker issue. We will not calculate old maker position again and using old data. This will make sure the position of makers is correct when the background image is smaller than dropzone. --- .../type/ddmarker/amd/build/question.min.js | 2 +- .../ddmarker/amd/build/question.min.js.map | 2 +- question/type/ddmarker/amd/src/question.js | 41 +++++++++------ .../ddmarker/tests/behat/previewquiz.feature | 50 +++++++++++++++++++ 4 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 question/type/ddmarker/tests/behat/previewquiz.feature diff --git a/question/type/ddmarker/amd/build/question.min.js b/question/type/ddmarker/amd/build/question.min.js index 9a55053a2dd..0d5612c6480 100644 --- a/question/type/ddmarker/amd/build/question.min.js +++ b/question/type/ddmarker/amd/build/question.min.js @@ -5,6 +5,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_ddmarker/question",["jquery","core/dragdrop","qtype_ddmarker/shapes","core/key_codes","core_form/changechecker"],(function($,dragDrop,Shapes,keys,FormChangeChecker){function DragDropMarkersQuestion(containerId,readOnly,visibleDropZones){var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.shapes=[],this.shapeSVGs=[],this.isPrinting=!1,this.questionAnswer={},readOnly&&this.getRoot().addClass("qtype_ddmarker-readonly"),thisQ.allImagesLoaded=!1,thisQ.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),thisQ.waitForAllImagesToBeLoaded()}DragDropMarkersQuestion.prototype.drawDropzones=function(){if(this.visibleDropZones.length>0){var bgImage=this.bgImage();this.getRoot().find("div.dropzones").html('');for(var svg=this.getRoot().find("svg.dropzones"),nextColourIndex=0,dropZoneNo=0;dropZoneNo'+dropZone.markertext+"");var markerspan=this.getRoot().find("div.ddarea div.markertexts span.markertext"+dropZoneNo);if(markerspan.length){var handles=shape.getHandlePositions(),positionLeft=handles.moveHandle.x-markerspan.outerWidth()/2-4,positionTop=handles.moveHandle.y-markerspan.outerHeight()/2;markerspan.css("left",positionLeft).css("top",positionTop),markerspan.data("originX",markerspan.position().left/bgRatio).data("originY",markerspan.position().top/bgRatio),this.handleElementScale(markerspan,"center")}}var shapeSVG=shape.makeSvg(svg[0]);shapeSVG.setAttribute("class","dropzone "+colourClass),this.shapes[this.shapes.length]=shape,this.shapeSVGs[this.shapeSVGs.length]=shapeSVG}},DragDropMarkersQuestion.prototype.repositionDrags=function(){var root=this.getRoot(),thisQ=this;root.find("div.draghomes .marker").not(".dragplaceholder").each((function(key,item){$(item).addClass("unneeded")})),root.find("input.choices").each((function(key,input){var choiceNo=thisQ.getChoiceNoFromElement(input),coords=thisQ.getCoords(input);if(coords.length){var drag=thisQ.getRoot().find(".draghomes span.marker.choice"+choiceNo).not(".dragplaceholder");drag.remove();for(var i=0;i{result[inputNode.id]=inputNode.value})),result},DragDropMarkersQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropMarkersQuestion.prototype.getCoords=function(inputNode){var coords=[],val=$(inputNode).val();if(""!==val)for(var coordsStrings=val.split(";"),i=0;i=bgPosition.left&&point.x=bgPosition.top&&point.y2&&void 0!==arguments[2]&&arguments[2];var dropArea=this.dropArea(),bgRatio=this.bgRatio();drag.removeClass("beingdragged").removeClass("unneeded");var dragXY=this.convertToBgImgXY(new Shapes.Point(drag.data("pagex"),drag.data("pagey")));isScaling?(drag.data("originX",dragXY.x/bgRatio).data("originY",dragXY.y/bgRatio),drag.css("left",dragXY.x).css("top",dragXY.y)):(drag.data("originX",dragXY.x).data("originY",dragXY.y),drag.css("left",dragXY.x*bgRatio).css("top",dragXY.y*bgRatio)),initialLoad||drag.data("scaleRatio",bgRatio),dropArea.append(drag),this.handleElementScale(drag,"left top")},DragDropMarkersQuestion.prototype.cloneDragIfNeeded=function(drag){var inputNode=this.getInput(drag),noOfDrags=Number(this.getClassnameNumericSuffix(inputNode,"noofdrags")),displayedDragsInDropArea=this.getRoot().find("div.droparea .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).length,displayedDragsInDragHomes=this.getRoot().find("div.draghomes .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).not(".dragplaceholder").length;if((this.isInfiniteDrag(drag)||!this.isInfiniteDrag(drag)&&displayedDragsInDropArea1;)dragsInHome.first().remove(),displayedDrags--},DragDropMarkersQuestion.prototype.getInput=function(drag){var choiceNo=this.getChoiceNoFromElement(drag);return this.getRoot().find("input.choices.choice"+choiceNo)},DragDropMarkersQuestion.prototype.bgRatio=function(){var bgImg=this.bgImage(),bgImgNaturalWidth=bgImg.get(0).naturalWidth;return bgImg.width()/bgImgNaturalWidth},DragDropMarkersQuestion.prototype.handleElementScale=function(element,type){var bgRatio=parseFloat(this.bgRatio());this.isPrinting&&(bgRatio=1),$(element).css({"-webkit-transform":"scale("+bgRatio+")","-moz-transform":"scale("+bgRatio+")","-ms-transform":"scale("+bgRatio+")","-o-transform":"scale("+bgRatio+")",transform:"scale("+bgRatio+")","transform-origin":type})},DragDropMarkersQuestion.prototype.isInfiniteDrag=function(drag){return drag.hasClass("infinite")},DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded=function(){this.allImagesLoaded||(null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId),this.getNotYetLoadedImages().length>0?this.imageLoadingTimeoutId=setTimeout((function(){this.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,this.cloneDrags(),this.repositionDrags(),this.drawDropzones()))},DragDropMarkersQuestion.prototype.getNotYetLoadedImages=function(){return this.getRoot().find(".ddmarker img.dropbackground").not((function(i,imgNode){return this.imageIsLoaded(imgNode)}))},DragDropMarkersQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,markerEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,visibleDropZones){if(questionManager.questions[containerId]=new DragDropMarkersQuestion(containerId,readOnly,visibleDropZones),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.markerEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("ddmarker")&&!questionContainer.classList.contains("qtype_ddmarker-readonly")&&(questionManager.addEventHandlersToMarker($(questionContainer).find("div.draghomes .marker")),questionManager.addEventHandlersToMarker($(questionContainer).find("div.droparea .marker")))}},setupEventHandlers:function(){$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToMarker:function(element){element.on("mousedown touchstart",questionManager.handleDragStart).on("keydown keypress",questionManager.handleKeyPress).focusin((function(e){questionManager.handleKeyboardFocus(e,!0)})).focusout((function(e){questionManager.handleKeyboardFocus(e,!1)}))},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},handleKeyboardFocus:function(e,isNavigating){questionManager.isKeyboardNavigation=isNavigating},fixLayoutIfThingsMoved:function(){questionManager.isKeyboardNavigation||this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.ddmarker").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); +define("qtype_ddmarker/question",["jquery","core/dragdrop","qtype_ddmarker/shapes","core/key_codes","core_form/changechecker"],(function($,dragDrop,Shapes,keys,FormChangeChecker){function DragDropMarkersQuestion(containerId,readOnly,visibleDropZones){var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.shapes=[],this.shapeSVGs=[],this.isPrinting=!1,this.questionAnswer={},readOnly&&this.getRoot().addClass("qtype_ddmarker-readonly"),thisQ.allImagesLoaded=!1,thisQ.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),thisQ.waitForAllImagesToBeLoaded()}DragDropMarkersQuestion.prototype.drawDropzones=function(){if(this.visibleDropZones.length>0){var bgImage=this.bgImage();this.getRoot().find("div.dropzones").html('');for(var svg=this.getRoot().find("svg.dropzones"),nextColourIndex=0,dropZoneNo=0;dropZoneNo'+dropZone.markertext+"");var markerspan=this.getRoot().find("div.ddarea div.markertexts span.markertext"+dropZoneNo);if(markerspan.length){var handles=shape.getHandlePositions(),positionLeft=handles.moveHandle.x-markerspan.outerWidth()/2-4,positionTop=handles.moveHandle.y-markerspan.outerHeight()/2;markerspan.css("left",positionLeft).css("top",positionTop),markerspan.data("originX",markerspan.position().left/bgRatio).data("originY",markerspan.position().top/bgRatio),this.handleElementScale(markerspan,"center")}}var shapeSVG=shape.makeSvg(svg[0]);shapeSVG.setAttribute("class","dropzone "+colourClass),this.shapes[this.shapes.length]=shape,this.shapeSVGs[this.shapeSVGs.length]=shapeSVG}},DragDropMarkersQuestion.prototype.repositionDrags=function(){var root=this.getRoot(),thisQ=this;root.find("div.draghomes .marker").not(".dragplaceholder").each((function(key,item){$(item).addClass("unneeded")})),root.find("input.choices").each((function(key,input){var choiceNo=thisQ.getChoiceNoFromElement(input),imageCoords=thisQ.getImageCoords(input);if(imageCoords.length){var drag=thisQ.getRoot().find(".draghomes span.marker.choice"+choiceNo).not(".dragplaceholder");drag.remove();for(var i=0;i{result[inputNode.id]=inputNode.value})),result},DragDropMarkersQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropMarkersQuestion.prototype.getImageCoords=function(inputNode){var imageCoords=[],val=$(inputNode).val();if(""!==val)for(var coordsStrings=val.split(";"),i=0;i=bgPosition.left&&point.x=bgPosition.top&&point.y2&&void 0!==arguments[2]&&arguments[2];var dropArea=this.dropArea(),bgRatio=this.bgRatio();drag.removeClass("beingdragged").removeClass("unneeded");var dragXY=this.convertToBgImgXY(new Shapes.Point(drag.data("pagex"),drag.data("pagey")));isScaling?(drag.data("originX",dragXY.x/bgRatio).data("originY",dragXY.y/bgRatio),drag.css("left",dragXY.x).css("top",dragXY.y)):(drag.data("originX",dragXY.x).data("originY",dragXY.y),drag.css("left",dragXY.x*bgRatio).css("top",dragXY.y*bgRatio)),initialLoad||drag.data("scaleRatio",bgRatio),dropArea.append(drag),this.handleElementScale(drag,"left top")},DragDropMarkersQuestion.prototype.cloneDragIfNeeded=function(drag){var inputNode=this.getInput(drag),noOfDrags=Number(this.getClassnameNumericSuffix(inputNode,"noofdrags")),displayedDragsInDropArea=this.getRoot().find("div.droparea .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).length,displayedDragsInDragHomes=this.getRoot().find("div.draghomes .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).not(".dragplaceholder").length;if((this.isInfiniteDrag(drag)||!this.isInfiniteDrag(drag)&&displayedDragsInDropArea1;)dragsInHome.first().remove(),displayedDrags--},DragDropMarkersQuestion.prototype.getInput=function(drag){var choiceNo=this.getChoiceNoFromElement(drag);return this.getRoot().find("input.choices.choice"+choiceNo)},DragDropMarkersQuestion.prototype.bgRatio=function(){var bgImg=this.bgImage(),bgImgNaturalWidth=bgImg.get(0).naturalWidth;return bgImg.width()/bgImgNaturalWidth},DragDropMarkersQuestion.prototype.handleElementScale=function(element,type){var bgRatio=parseFloat(this.bgRatio());this.isPrinting&&(bgRatio=1),$(element).css({"-webkit-transform":"scale("+bgRatio+")","-moz-transform":"scale("+bgRatio+")","-ms-transform":"scale("+bgRatio+")","-o-transform":"scale("+bgRatio+")",transform:"scale("+bgRatio+")","transform-origin":type})},DragDropMarkersQuestion.prototype.isInfiniteDrag=function(drag){return drag.hasClass("infinite")},DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded=function(){this.allImagesLoaded||(null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId),this.getNotYetLoadedImages().length>0?this.imageLoadingTimeoutId=setTimeout((function(){this.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,this.cloneDrags(),this.repositionDrags(),this.drawDropzones()))},DragDropMarkersQuestion.prototype.getNotYetLoadedImages=function(){return this.getRoot().find(".ddmarker img.dropbackground").not((function(i,imgNode){return this.imageIsLoaded(imgNode)}))},DragDropMarkersQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,markerEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,visibleDropZones){if(questionManager.questions[containerId]=new DragDropMarkersQuestion(containerId,readOnly,visibleDropZones),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.markerEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("ddmarker")&&!questionContainer.classList.contains("qtype_ddmarker-readonly")&&(questionManager.addEventHandlersToMarker($(questionContainer).find("div.draghomes .marker")),questionManager.addEventHandlersToMarker($(questionContainer).find("div.droparea .marker")))}},setupEventHandlers:function(){$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToMarker:function(element){element.on("mousedown touchstart",questionManager.handleDragStart).on("keydown keypress",questionManager.handleKeyPress).focusin((function(e){questionManager.handleKeyboardFocus(e,!0)})).focusout((function(e){questionManager.handleKeyboardFocus(e,!1)}))},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},handleKeyboardFocus:function(e,isNavigating){questionManager.isKeyboardNavigation=isNavigating},fixLayoutIfThingsMoved:function(){questionManager.isKeyboardNavigation||this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.ddmarker").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); //# sourceMappingURL=question.min.js.map \ No newline at end of file diff --git a/question/type/ddmarker/amd/build/question.min.js.map b/question/type/ddmarker/amd/build/question.min.js.map index f03ff661aaf..14a7916e0c6 100644 --- a/question/type/ddmarker/amd/build/question.min.js.map +++ b/question/type/ddmarker/amd/build/question.min.js.map @@ -1 +1 @@ -{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Question class for drag and drop marker question type, used to support the question and preview pages.\n *\n * @module qtype_ddmarker/question\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'qtype_ddmarker/shapes',\n 'core/key_codes',\n 'core_form/changechecker'\n], function(\n $,\n dragDrop,\n Shapes,\n keys,\n FormChangeChecker\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop markers 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 {Object[]} visibleDropZones the geometry of any drop-zones to show.\n * Objects have fields shape, coords and markertext.\n * @constructor\n */\n function DragDropMarkersQuestion(containerId, readOnly, visibleDropZones) {\n var thisQ = this;\n this.containerId = containerId;\n this.visibleDropZones = visibleDropZones;\n this.shapes = [];\n this.shapeSVGs = [];\n this.isPrinting = false;\n this.questionAnswer = {};\n if (readOnly) {\n this.getRoot().addClass('qtype_ddmarker-readonly');\n }\n thisQ.allImagesLoaded = false;\n thisQ.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n thisQ.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Draws the svg shapes of any drop zones that should be visible for feedback purposes.\n */\n DragDropMarkersQuestion.prototype.drawDropzones = function() {\n if (this.visibleDropZones.length > 0) {\n var bgImage = this.bgImage();\n\n this.getRoot().find('div.dropzones').html('');\n var svg = this.getRoot().find('svg.dropzones');\n\n var nextColourIndex = 0;\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var colourClass = 'color' + nextColourIndex;\n nextColourIndex = (nextColourIndex + 1) % 8;\n this.addDropzone(svg, dropZoneNo, colourClass);\n }\n }\n };\n\n /**\n * Adds a dropzone shape with colour, coords and link provided to the array of shapes.\n *\n * @param {jQuery} svg the SVG image to which to add this drop zone.\n * @param {int} dropZoneNo which drop-zone to add.\n * @param {string} colourClass class name\n */\n DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {\n var dropZone = this.visibleDropZones[dropZoneNo],\n shape = Shapes.make(dropZone.shape, ''),\n existingmarkertext,\n bgRatio = this.bgRatio();\n if (!shape.parse(dropZone.coords, bgRatio)) {\n return;\n }\n\n existingmarkertext = this.getRoot().find('div.markertexts span.markertext' + dropZoneNo);\n if (existingmarkertext.length) {\n if (dropZone.markertext !== '') {\n existingmarkertext.html(dropZone.markertext);\n } else {\n existingmarkertext.remove();\n }\n } else if (dropZone.markertext !== '') {\n var classnames = 'markertext markertext' + dropZoneNo;\n this.getRoot().find('div.markertexts').append('' +\n dropZone.markertext + '');\n var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n if (markerspan.length) {\n var handles = shape.getHandlePositions();\n var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;\n var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);\n markerspan\n .css('left', positionLeft)\n .css('top', positionTop);\n markerspan\n .data('originX', markerspan.position().left / bgRatio)\n .data('originY', markerspan.position().top / bgRatio);\n this.handleElementScale(markerspan, 'center');\n }\n }\n\n var shapeSVG = shape.makeSvg(svg[0]);\n shapeSVG.setAttribute('class', 'dropzone ' + colourClass);\n\n this.shapes[this.shapes.length] = shape;\n this.shapeSVGs[this.shapeSVGs.length] = shapeSVG;\n };\n\n /**\n * Draws the drag items on the page (and drop zones if required).\n * The idea is to re-draw all the drags and drops whenever there is a change\n * like a widow resize or an item dropped in place.\n */\n DragDropMarkersQuestion.prototype.repositionDrags = function() {\n var root = this.getRoot(),\n thisQ = this;\n\n root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {\n $(item).addClass('unneeded');\n });\n\n root.find('input.choices').each(function(key, input) {\n var choiceNo = thisQ.getChoiceNoFromElement(input),\n coords = thisQ.getCoords(input);\n if (coords.length) {\n var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');\n drag.remove();\n for (var i = 0; i < coords.length; i++) {\n var dragInDrop = drag.clone();\n dragInDrop.data('pagex', coords[i].x).data('pagey', coords[i].y);\n // We always save the coordinates in the 1:1 ratio.\n // So we need to set the scale ratio to 1 for the initial load.\n dragInDrop.data('scaleRatio', 1);\n thisQ.sendDragToDrop(dragInDrop, false, true);\n }\n thisQ.getDragClone(drag).addClass('active');\n thisQ.cloneDragIfNeeded(drag);\n }\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropMarkersQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.choices').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropMarkersQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Determine what drag items need to be shown and\n * return coords of all drag items except any that are currently being dragged\n * based on contents of hidden inputs and whether drags are 'infinite' or how many\n * drags should be shown.\n *\n * @param {jQuery} inputNode\n * @returns {Point[]} coordinates of however many copies of the drag item should be shown.\n */\n DragDropMarkersQuestion.prototype.getCoords = function(inputNode) {\n var coords = [],\n val = $(inputNode).val();\n if (val !== '') {\n var coordsStrings = val.split(';');\n for (var i = 0; i < coordsStrings.length; i++) {\n coords[i] = this.convertToWindowXY(Shapes.Point.parse(coordsStrings[i]));\n }\n }\n return coords;\n };\n\n /**\n * Converts the relative x and y position coordinates into\n * absolute x and y position coordinates.\n *\n * @param {Point} point relative to the background image.\n * @returns {Point} point relative to the page.\n */\n DragDropMarkersQuestion.prototype.convertToWindowXY = function(point) {\n var bgImage = this.bgImage();\n // The +1 seems rather odd, but seems to give the best results in\n // the three main browsers at a range of zoom levels.\n // (Its due to the 1px border around the image, that shifts the\n // image pixels by 1 down and to the left.)\n return point.offset(bgImage.offset().left + 1, bgImage.offset().top + 1);\n };\n\n /**\n * Utility function converting window coordinates to relative to the\n * background image coordinates.\n *\n * @param {Point} point relative to the page.\n * @returns {Point} point relative to the background image.\n */\n DragDropMarkersQuestion.prototype.convertToBgImgXY = function(point) {\n var bgImage = this.bgImage();\n return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);\n };\n\n /**\n * Is the point within the background image?\n *\n * @param {Point} point relative to the BG image.\n * @return {boolean} true it they are.\n */\n DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {\n var bgImage = this.bgImage();\n var bgPosition = bgImage.offset();\n\n return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()\n && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();\n };\n\n /**\n * Get the outer div for this question.\n * @returns {jQuery} containing that div.\n */\n DragDropMarkersQuestion.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 DragDropMarkersQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n DragDropMarkersQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n dragged = $(e.target).closest('.marker');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragged.addClass('beingdragged').css('transform', '');\n\n var placed = !dragged.hasClass('unneeded');\n if (!placed) {\n var hiddenDrag = thisQ.getDragClone(dragged);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n dragged.offset(hiddenDrag.offset());\n }\n }\n\n dragDrop.start(e, dragged, function() {\n void (1);\n }, function(x, y, dragged) {\n thisQ.dragEnd(dragged);\n });\n };\n\n /**\n * Functionality at the end of a drag drop.\n * @param {jQuery} dragged the marker that was dragged.\n */\n DragDropMarkersQuestion.prototype.dragEnd = function(dragged) {\n var placed = false,\n choiceNo = this.getChoiceNoFromElement(dragged),\n bgRatio = this.bgRatio(),\n dragXY;\n\n dragged.data('pagex', dragged.offset().left).data('pagey', dragged.offset().top);\n dragXY = new Shapes.Point(dragged.data('pagex'), dragged.data('pagey'));\n if (this.coordsInBgImg(dragXY)) {\n this.sendDragToDrop(dragged, true);\n placed = true;\n\n // It seems that the dragdrop sometimes leaves the drag\n // one pixel out of position. Put it in exactly the right place.\n var bgImgXY = this.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);\n }\n\n if (!placed) {\n this.sendDragHome(dragged);\n this.removeDragIfNeeded(dragged);\n } else {\n this.cloneDragIfNeeded(dragged);\n }\n\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Save the coordinates for a dropped item in the form field.\n * @param {Number} choiceNo which copy of the choice this was.\n */\n DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) {\n var coords = [],\n items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),\n thiQ = this,\n bgRatio = this.bgRatio();\n\n if (items.length) {\n items.each(function() {\n var drag = $(this);\n if (!drag.hasClass('beingdragged')) {\n if (drag.data('scaleRatio') !== bgRatio) {\n // The scale ratio for the draggable item was changed. We need to update that.\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n }\n var dragXY = new Shapes.Point(drag.data('pagex'), drag.data('pagey'));\n if (thiQ.coordsInBgImg(dragXY)) {\n var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n coords[coords.length] = bgImgXY;\n }\n }\n });\n }\n\n this.getRoot().find('input.choice' + choiceNo).val(coords.join(';'));\n if (this.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n this.questionAnswer = this.getQuestionAnsweredValues();\n }\n };\n\n /**\n * Handle key down / press events on markers.\n * @param {KeyboardEvent} e\n */\n DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {\n var drag = $(e.target).closest('.marker'),\n point = new Shapes.Point(drag.offset().left, drag.offset().top),\n choiceNo = this.getChoiceNoFromElement(drag);\n\n switch (e.keyCode) {\n case keys.arrowLeft:\n case 65: // A.\n point.x -= 1;\n break;\n case keys.arrowRight:\n case 68: // D.\n point.x += 1;\n break;\n case keys.arrowDown:\n case 83: // S.\n point.y += 1;\n break;\n case keys.arrowUp:\n case 87: // W.\n point.y -= 1;\n break;\n case keys.space:\n case keys.escape:\n point = null;\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n if (point !== null) {\n point = this.constrainToBgImg(point);\n drag.offset({'left': point.x, 'top': point.y});\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());\n if (this.coordsInBgImg(new Shapes.Point(drag.offset().left, drag.offset().top))) {\n if (drag.hasClass('unneeded')) {\n this.sendDragToDrop(drag, true);\n var hiddenDrag = this.getDragClone(drag);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n }\n this.cloneDragIfNeeded(drag);\n }\n }\n } else {\n drag.css('left', '').css('top', '');\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n this.sendDragHome(drag);\n this.removeDragIfNeeded(drag);\n }\n drag.focus();\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Makes sure the dragged item always exists within the background image area.\n *\n * @param {Point} windowxy\n * @returns {Point} coordinates\n */\n DragDropMarkersQuestion.prototype.constrainToBgImg = function(windowxy) {\n var bgImg = this.bgImage(),\n bgImgXY = this.convertToBgImgXY(windowxy);\n bgImgXY.x = Math.max(0, bgImgXY.x);\n bgImgXY.y = Math.max(0, bgImgXY.y);\n bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);\n bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);\n return this.convertToWindowXY(bgImgXY);\n };\n\n /**\n * Returns the choice number for a node.\n *\n * @param {Element|jQuery} node\n * @returns {Number}\n */\n DragDropMarkersQuestion.prototype.getChoiceNoFromElement = function(node) {\n return Number(this.getClassnameNumericSuffix(node, 'choice'));\n };\n\n /**\n * Returns the numeric part of a class with the given prefix.\n *\n * @param {Element|jQuery} node\n * @param {String} prefix\n * @returns {Number|null}\n */\n DragDropMarkersQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = $(node).attr('class');\n if (classes !== undefined && 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 * Handle when the window is resized.\n */\n DragDropMarkersQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('div.droparea .marker').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 this.getRoot().find('div.droparea svg.dropzones')\n .width(this.bgImage().width())\n .height(this.bgImage().height());\n\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var dropZone = thisQ.visibleDropZones[dropZoneNo];\n var originCoords = dropZone.coords;\n var shape = thisQ.shapes[dropZoneNo];\n var shapeSVG = thisQ.shapeSVGs[dropZoneNo];\n shape.parse(originCoords, bgRatio);\n shape.updateSvg(shapeSVG);\n\n var handles = shape.getHandlePositions();\n var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n markerSpan\n .css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)\n .css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));\n thisQ.handleElementScale(markerSpan, 'center');\n }\n };\n\n /**\n * Clone the drag.\n */\n DragDropMarkersQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('marker');\n placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));\n placeHolder.addClass(thisQ.getDragNoClass(drag, false));\n placeHolder.addClass('dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Get the drag number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the drag number.\n */\n DragDropMarkersQuestion.prototype.getDragNo = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'dragno');\n };\n\n /**\n * Get the drag number prefix of a drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} includeSelector include the CSS selector prefix or not.\n * @return {String} Class name\n */\n DragDropMarkersQuestion.prototype.getDragNoClass = function(drag, includeSelector) {\n var className = 'dragno' + this.getDragNo(drag);\n if (this.isInfiniteDrag(drag)) {\n className = 'infinite';\n }\n\n if (includeSelector) {\n return '.' + className;\n }\n\n return className;\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 DragDropMarkersQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draghomes' + ' span.marker' +\n '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');\n };\n\n /**\n * Get the drop area element.\n * @returns {jQuery} droparea element.\n */\n DragDropMarkersQuestion.prototype.dropArea = function() {\n return this.getRoot().find('div.droparea');\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropMarkersQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('beingdragged')\n .addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n var placeHolder = this.getDragClone(drag);\n placeHolder.after(drag);\n placeHolder.removeClass('active');\n };\n\n /**\n * Animate a drag item into a given place.\n *\n * @param {jQuery} drag the item to place.\n * @param {boolean} isScaling Scaling or not.\n * @param {boolean} initialLoad Whether it is the initial load or not.\n */\n DragDropMarkersQuestion.prototype.sendDragToDrop = function(drag, isScaling, initialLoad = false) {\n var dropArea = this.dropArea(),\n bgRatio = this.bgRatio();\n drag.removeClass('beingdragged').removeClass('unneeded');\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n if (isScaling) {\n drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);\n drag.css('left', dragXY.x).css('top', dragXY.y);\n } else {\n drag.data('originX', dragXY.x).data('originY', dragXY.y);\n drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);\n }\n // We need to save the original scale ratio for each draggable item.\n if (!initialLoad) {\n // Only set the scale ratio for a current being-dragged item, not for the initial loading.\n drag.data('scaleRatio', bgRatio);\n }\n dropArea.append(drag);\n this.handleElementScale(drag, 'left top');\n };\n\n /**\n * Clone the drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.cloneDragIfNeeded = function(drag) {\n var inputNode = this.getInput(drag),\n noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),\n displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,\n displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;\n\n if ((this.isInfiniteDrag(drag) ||\n !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {\n var dragClone = drag.clone();\n dragClone.addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n this.getDragClone(drag)\n .removeClass('active')\n .after(dragClone);\n questionManager.addEventHandlersToMarker(dragClone);\n }\n };\n\n /**\n * Remove the clone drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {\n var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');\n var displayedDrags = dragsInHome.length;\n while (displayedDrags > 1) {\n dragsInHome.first().remove();\n displayedDrags--;\n }\n };\n\n /**\n * Get the input belong to drag.\n *\n * @param {jQuery} drag the item to place.\n * @returns {jQuery} input element.\n */\n DragDropMarkersQuestion.prototype.getInput = function(drag) {\n var choiceNo = this.getChoiceNoFromElement(drag);\n return this.getRoot().find('input.choices.choice' + choiceNo);\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropMarkersQuestion.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 DragDropMarkersQuestion.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 * Check if the given drag is in infinite mode or not.\n *\n * @param {jQuery} drag The drag item need to check.\n */\n DragDropMarkersQuestion.prototype.isInfiniteDrag = function(drag) {\n return drag.hasClass('infinite');\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 DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded = function() {\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 this.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 this.cloneDrags();\n this.repositionDrags();\n this.drawDropzones();\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 DragDropMarkersQuestion.prototype.getNotYetLoadedImages = function() {\n return this.getRoot().find('.ddmarker img.dropbackground').not(function(i, imgNode) {\n return this.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 DragDropMarkersQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\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 marker event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n markerEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation.\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 * @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 {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.\n */\n init: function(containerId, readOnly, visibleDropZones) {\n questionManager.questions[containerId] =\n new DragDropMarkersQuestion(containerId, readOnly, visibleDropZones);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.markerEventHandlersInitialised[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('ddmarker') &&\n !questionContainer.classList.contains('qtype_ddmarker-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker'));\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker'));\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 $(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 event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToMarker: function(element) {\n element\n .on('mousedown touchstart', questionManager.handleDragStart)\n .on('keydown keypress', questionManager.handleKeyPress)\n .focusin(function(e) {\n questionManager.handleKeyboardFocus(e, true);\n })\n .focusout(function(e) {\n questionManager.handleKeyboardFocus(e, false);\n });\n },\n\n /**\n * Handle mouse down / touch start events on markers.\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 markers.\n * @param {Event} e\n */\n handleKeyPress: function(e) {\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 * Handle focus lost events on markers.\n * @param {Event} e\n * @param {boolean} isNavigating\n */\n handleKeyboardFocus: function(e, isNavigating) {\n questionManager.isKeyboardNavigation = isNavigating;\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 if (!questionManager.isKeyboardNavigation) {\n this.handleWindowResize(questionManager.isPrinting);\n }\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 * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropMarkersQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddmarker').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddmarker/question\n */\n return {\n /**\n * Initialise one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {String} bgImgUrl the URL of the background image.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {String[]} visibleDropZones the geometry of any drop-zones to show.\n */\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","Shapes","keys","FormChangeChecker","DragDropMarkersQuestion","containerId","readOnly","visibleDropZones","thisQ","this","shapes","shapeSVGs","isPrinting","questionAnswer","getRoot","addClass","allImagesLoaded","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","drawDropzones","length","bgImage","find","html","outerWidth","outerHeight","svg","nextColourIndex","dropZoneNo","colourClass","addDropzone","existingmarkertext","dropZone","shape","make","bgRatio","parse","coords","markertext","remove","classnames","append","markerspan","handles","getHandlePositions","positionLeft","moveHandle","x","positionTop","y","css","data","position","left","top","handleElementScale","shapeSVG","makeSvg","setAttribute","repositionDrags","root","not","each","key","item","input","choiceNo","getChoiceNoFromElement","getCoords","drag","i","dragInDrop","clone","sendDragToDrop","getDragClone","cloneDragIfNeeded","getQuestionAnsweredValues","result","inputNode","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","val","coordsStrings","split","convertToWindowXY","Point","point","offset","convertToBgImgXY","coordsInBgImg","bgPosition","width","height","document","getElementById","handleDragStart","e","dragged","target","closest","prepare","start","hasClass","hiddenDrag","dragEnd","dragXY","placed","bgImgXY","sendDragHome","removeDragIfNeeded","saveCoordsForChoice","items","thiQ","join","questionManager","handleFormDirty","handleKeyPress","keyCode","arrowLeft","arrowRight","arrowDown","arrowUp","space","escape","preventDefault","constrainToBgImg","focus","windowxy","bgImg","Math","max","min","node","Number","getClassnameNumericSuffix","prefix","classes","attr","undefined","classesarr","index","RegExp","test","match","exec","handleResize","parseFloat","originCoords","updateSvg","markerSpan","cloneDrags","draghome","placeHolder","removeClass","getDragNoClass","before","getDragNo","includeSelector","className","isInfiniteDrag","dropArea","after","isScaling","initialLoad","getInput","noOfDrags","displayedDragsInDropArea","displayedDragsInDragHomes","dragClone","addEventHandlersToMarker","dragsInHome","displayedDrags","first","bgImgNaturalWidth","get","naturalWidth","element","type","imageLoadingTimeoutId","clearTimeout","setTimeout","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","eventHandlersInitialised","markerEventHandlersInitialised","isKeyboardNavigation","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","classList","contains","window","on","handleWindowResize","addEventListener","fixLayoutIfThingsMoved","focusin","handleKeyboardFocus","focusout","question","getQuestionForEvent","isNavigating","currentTarget","responseForm","markFormAsDirty"],"mappings":";;;;;;;AAuBAA,iCAAO,CACH,SACA,gBACA,wBACA,iBACA,4BACD,SACCC,EACAC,SACAC,OACAC,KACAC,4BAcSC,wBAAwBC,YAAaC,SAAUC,sBAChDC,MAAQC,UACPJ,YAAcA,iBACdE,iBAAmBA,sBACnBG,OAAS,QACTC,UAAY,QACZC,YAAa,OACbC,eAAiB,GAClBP,eACKQ,UAAUC,SAAS,2BAE5BP,MAAMQ,iBAAkB,EACxBR,MAAMS,wBAAwBC,IAAI,QAAQ,WACtCV,MAAMW,gCAEVX,MAAMW,6BAMVf,wBAAwBgB,UAAUC,cAAgB,cAC1CZ,KAAKF,iBAAiBe,OAAS,EAAG,KAC9BC,QAAUd,KAAKc,eAEdT,UAAUU,KAAK,iBAAiBC,KAAK,oEAC1BF,QAAQG,aADkB,aAEzBH,QAAQI,cAAgB,oBACrCC,IAAMnB,KAAKK,UAAUU,KAAK,iBAE1BK,gBAAkB,EACbC,WAAa,EAAGA,WAAarB,KAAKF,iBAAiBe,OAAQQ,aAAc,KAC1EC,YAAc,QAAUF,gBAC5BA,iBAAmBA,gBAAkB,GAAK,OACrCG,YAAYJ,IAAKE,WAAYC,gBAY9C3B,wBAAwBgB,UAAUY,YAAc,SAASJ,IAAKE,WAAYC,iBAGlEE,mBAFAC,SAAWzB,KAAKF,iBAAiBuB,YACjCK,MAAQlC,OAAOmC,KAAKF,SAASC,MAAO,IAEpCE,QAAU5B,KAAK4B,aACdF,MAAMG,MAAMJ,SAASK,OAAQF,cAIlCJ,mBAAqBxB,KAAKK,UAAUU,KAAK,kCAAoCM,aACtDR,OACS,KAAxBY,SAASM,WACTP,mBAAmBR,KAAKS,SAASM,YAEjCP,mBAAmBQ,cAEpB,GAA4B,KAAxBP,SAASM,WAAmB,KAC/BE,WAAa,wBAA0BZ,gBACtChB,UAAUU,KAAK,mBAAmBmB,OAAO,gBAAkBD,WAAa,KACzER,SAASM,WAAa,eACtBI,WAAanC,KAAKK,UAAUU,KAAK,6CAA+CM,eAChFc,WAAWtB,OAAQ,KACfuB,QAAUV,MAAMW,qBAChBC,aAAeF,QAAQG,WAAWC,EAAKL,WAAWlB,aAAe,EAAK,EACtEwB,YAAcL,QAAQG,WAAWG,EAAKP,WAAWjB,cAAgB,EACrEiB,WACKQ,IAAI,OAAQL,cACZK,IAAI,MAAOF,aAChBN,WACKS,KAAK,UAAWT,WAAWU,WAAWC,KAAOlB,SAC7CgB,KAAK,UAAWT,WAAWU,WAAWE,IAAMnB,cAC5CoB,mBAAmBb,WAAY,eAIxCc,SAAWvB,MAAMwB,QAAQ/B,IAAI,IACjC8B,SAASE,aAAa,QAAS,YAAc7B,kBAExCrB,OAAOD,KAAKC,OAAOY,QAAUa,WAC7BxB,UAAUF,KAAKE,UAAUW,QAAUoC,WAQ5CtD,wBAAwBgB,UAAUyC,gBAAkB,eAC5CC,KAAOrD,KAAKK,UACZN,MAAQC,KAEZqD,KAAKtC,KAAK,yBAAyBuC,IAAI,oBAAoBC,MAAK,SAASC,IAAKC,MAC1EnE,EAAEmE,MAAMnD,SAAS,eAGrB+C,KAAKtC,KAAK,iBAAiBwC,MAAK,SAASC,IAAKE,WACtCC,SAAW5D,MAAM6D,uBAAuBF,OACxC5B,OAAS/B,MAAM8D,UAAUH,UACzB5B,OAAOjB,OAAQ,KACXiD,KAAO/D,MAAMM,UAAUU,KAAK,gCAA4C4C,UAAUL,IAAI,oBAC1FQ,KAAK9B,aACA,IAAI+B,EAAI,EAAGA,EAAIjC,OAAOjB,OAAQkD,IAAK,KAChCC,WAAaF,KAAKG,QACtBD,WAAWpB,KAAK,QAASd,OAAOiC,GAAGvB,GAAGI,KAAK,QAASd,OAAOiC,GAAGrB,GAG9DsB,WAAWpB,KAAK,aAAc,GAC9B7C,MAAMmE,eAAeF,YAAY,GAAO,GAE5CjE,MAAMoE,aAAaL,MAAMxD,SAAS,UAClCP,MAAMqE,kBAAkBN,UAKhC/D,MAAMK,eAAiBL,MAAMsE,6BAQjC1E,wBAAwBgB,UAAU0D,0BAA4B,eACtDC,OAAS,eACRjE,UAAUU,KAAK,iBAAiBwC,MAAK,CAACQ,EAAGQ,aAC1CD,OAAOC,UAAUC,IAAMD,UAAUE,SAG9BH,QAQX3E,wBAAwBgB,UAAU+D,qBAAuB,iBAC/CC,UAAY3E,KAAKI,eACjBwE,UAAY5E,KAAKqE,gCACnBQ,cAAe,SAGfC,KAAKC,UAAUH,aAAeE,KAAKC,UAAUJ,YAC7CE,cAAe,EACRA,eAGXG,OAAOvF,KAAKmF,WAAWK,SAAQzB,MACvBoB,UAAUpB,OAASmB,UAAUnB,OAC7BqB,cAAe,MAIhBA,eAYXlF,wBAAwBgB,UAAUkD,UAAY,SAASU,eAC/CzC,OAAS,GACToD,IAAM5F,EAAEiF,WAAWW,SACX,KAARA,YACIC,cAAgBD,IAAIE,MAAM,KACrBrB,EAAI,EAAGA,EAAIoB,cAActE,OAAQkD,IACtCjC,OAAOiC,GAAK/D,KAAKqF,kBAAkB7F,OAAO8F,MAAMzD,MAAMsD,cAAcpB,YAGrEjC,QAUXnC,wBAAwBgB,UAAU0E,kBAAoB,SAASE,WACvDzE,QAAUd,KAAKc,iBAKZyE,MAAMC,OAAO1E,QAAQ0E,SAAS1C,KAAO,EAAGhC,QAAQ0E,SAASzC,IAAM,IAU1EpD,wBAAwBgB,UAAU8E,iBAAmB,SAASF,WACtDzE,QAAUd,KAAKc,iBACZyE,MAAMC,QAAQ1E,QAAQ0E,SAAS1C,KAAO,GAAIhC,QAAQ0E,SAASzC,IAAM,IAS5EpD,wBAAwBgB,UAAU+E,cAAgB,SAASH,WACnDzE,QAAUd,KAAKc,UACf6E,WAAa7E,QAAQ0E,gBAElBD,MAAM/C,GAAKmD,WAAW7C,MAAQyC,MAAM/C,EAAImD,WAAW7C,KAAOhC,QAAQ8E,SAClEL,MAAM7C,GAAKiD,WAAW5C,KAAOwC,MAAM7C,EAAIiD,WAAW5C,IAAMjC,QAAQ+E,UAO3ElG,wBAAwBgB,UAAUN,QAAU,kBACjCf,EAAEwG,SAASC,eAAe/F,KAAKJ,eAO1CD,wBAAwBgB,UAAUG,QAAU,kBACjCd,KAAKK,UAAUU,KAAK,uBAG/BpB,wBAAwBgB,UAAUqF,gBAAkB,SAASC,OACrDlG,MAAQC,KACRkG,QAAU5G,EAAE2G,EAAEE,QAAQC,QAAQ,cAEvB7G,SAAS8G,QAAQJ,GAClBK,UAIVJ,QAAQ5F,SAAS,gBAAgBqC,IAAI,YAAa,MAEpCuD,QAAQK,SAAS,YAClB,KACLC,WAAazG,MAAMoE,aAAa+B,SAChCM,WAAW3F,SACX2F,WAAWlG,SAAS,UACpB4F,QAAQV,OAAOgB,WAAWhB,WAIlCjG,SAAS+G,MAAML,EAAGC,SAAS,eAExB,SAAS1D,EAAGE,EAAGwD,SACdnG,MAAM0G,QAAQP,cAQtBvG,wBAAwBgB,UAAU8F,QAAU,SAASP,aAI7CQ,OAHAC,QAAS,EACThD,SAAW3D,KAAK4D,uBAAuBsC,SACvCtE,QAAU5B,KAAK4B,aAGnBsE,QAAQtD,KAAK,QAASsD,QAAQV,SAAS1C,MAAMF,KAAK,QAASsD,QAAQV,SAASzC,KAC5E2D,OAAS,IAAIlH,OAAO8F,MAAMY,QAAQtD,KAAK,SAAUsD,QAAQtD,KAAK,UAC1D5C,KAAK0F,cAAcgB,QAAS,MACvBxC,eAAegC,SAAS,GAC7BS,QAAS,MAILC,QAAU5G,KAAKyF,iBAAiBiB,QACpCE,QAAU,IAAIpH,OAAO8F,MAAMsB,QAAQpE,EAAIZ,QAASgF,QAAQlE,EAAId,SAC5DsE,QAAQtD,KAAK,UAAWgE,QAAQpE,GAAGI,KAAK,UAAWgE,QAAQlE,GAG1DiE,YAIIvC,kBAAkB8B,eAHlBW,aAAaX,cACbY,mBAAmBZ,eAKvBa,oBAAoBpD,WAO7BhE,wBAAwBgB,UAAUoG,oBAAsB,SAASpD,cACzD7B,OAAS,GACTkF,MAAQhH,KAAKK,UAAUU,KAAK,kCAAoC4C,UAChEsD,KAAOjH,KACP4B,QAAU5B,KAAK4B,UAEfoF,MAAMnG,QACNmG,MAAMzD,MAAK,eACHO,KAAOxE,EAAEU,UACR8D,KAAKyC,SAAS,gBAAiB,CAC5BzC,KAAKlB,KAAK,gBAAkBhB,SAE5BkC,KAAKlB,KAAK,QAASkB,KAAK0B,SAAS1C,MAAMF,KAAK,QAASkB,KAAK0B,SAASzC,SAEnE2D,OAAS,IAAIlH,OAAO8F,MAAMxB,KAAKlB,KAAK,SAAUkB,KAAKlB,KAAK,aACxDqE,KAAKvB,cAAcgB,QAAS,KACxBE,QAAUK,KAAKxB,iBAAiBiB,QACpCE,QAAU,IAAIpH,OAAO8F,MAAMsB,QAAQpE,EAAIZ,QAASgF,QAAQlE,EAAId,SAC5DE,OAAOA,OAAOjB,QAAU+F,kBAMnCvG,UAAUU,KAAK,eAAiB4C,UAAUuB,IAAIpD,OAAOoF,KAAK,MAC3DlH,KAAK0E,yBAELyC,gBAAgBC,uBAEXhH,eAAiBJ,KAAKqE,8BAQnC1E,wBAAwBgB,UAAU0G,eAAiB,SAASpB,OACpDnC,KAAOxE,EAAE2G,EAAEE,QAAQC,QAAQ,WAC3Bb,MAAQ,IAAI/F,OAAO8F,MAAMxB,KAAK0B,SAAS1C,KAAMgB,KAAK0B,SAASzC,KAC3DY,SAAW3D,KAAK4D,uBAAuBE,aAEnCmC,EAAEqB,cACD7H,KAAK8H,eACL,GACDhC,MAAM/C,GAAK,aAEV/C,KAAK+H,gBACL,GACDjC,MAAM/C,GAAK,aAEV/C,KAAKgI,eACL,GACDlC,MAAM7C,GAAK,aAEVjD,KAAKiI,aACL,GACDnC,MAAM7C,GAAK,aAEVjD,KAAKkI,WACLlI,KAAKmI,OACNrC,MAAQ,6BAKhBU,EAAE4B,iBAEY,OAAVtC,MAAgB,CAChBA,MAAQvF,KAAK8H,iBAAiBvC,OAC9BzB,KAAK0B,OAAO,MAASD,MAAM/C,MAAU+C,MAAM7C,IAC3CoB,KAAKlB,KAAK,QAASkB,KAAK0B,SAAS1C,MAAMF,KAAK,QAASkB,KAAK0B,SAASzC,SAC/D2D,OAAS1G,KAAKyF,iBAAiB,IAAIjG,OAAO8F,MAAMxB,KAAKlB,KAAK,SAAUkB,KAAKlB,KAAK,cAClFkB,KAAKlB,KAAK,UAAW8D,OAAOlE,EAAIxC,KAAK4B,WAAWgB,KAAK,UAAW8D,OAAOhE,EAAI1C,KAAK4B,WAC5E5B,KAAK0F,cAAc,IAAIlG,OAAO8F,MAAMxB,KAAK0B,SAAS1C,KAAMgB,KAAK0B,SAASzC,OAClEe,KAAKyC,SAAS,YAAa,MACtBrC,eAAeJ,MAAM,OACtB0C,WAAaxG,KAAKmE,aAAaL,MAC/B0C,WAAW3F,QACX2F,WAAWlG,SAAS,eAEnB8D,kBAAkBN,YAI/BA,KAAKnB,IAAI,OAAQ,IAAIA,IAAI,MAAO,IAChCmB,KAAKlB,KAAK,QAASkB,KAAK0B,SAAS1C,MAAMF,KAAK,QAASkB,KAAK0B,SAASzC,UAC9D8D,aAAa/C,WACbgD,mBAAmBhD,MAE5BA,KAAKiE,aACAhB,oBAAoBpD,WAS7BhE,wBAAwBgB,UAAUmH,iBAAmB,SAASE,cACtDC,MAAQjI,KAAKc,UACb8F,QAAU5G,KAAKyF,iBAAiBuC,iBACpCpB,QAAQpE,EAAI0F,KAAKC,IAAI,EAAGvB,QAAQpE,GAChCoE,QAAQlE,EAAIwF,KAAKC,IAAI,EAAGvB,QAAQlE,GAChCkE,QAAQpE,EAAI0F,KAAKE,IAAIH,MAAMrC,QAASgB,QAAQpE,GAC5CoE,QAAQlE,EAAIwF,KAAKE,IAAIH,MAAMpC,SAAUe,QAAQlE,GACtC1C,KAAKqF,kBAAkBuB,UASlCjH,wBAAwBgB,UAAUiD,uBAAyB,SAASyE,aACzDC,OAAOtI,KAAKuI,0BAA0BF,KAAM,YAUvD1I,wBAAwBgB,UAAU4H,0BAA4B,SAASF,KAAMG,YACrEC,QAAUnJ,EAAE+I,MAAMK,KAAK,iBACXC,IAAZF,SAAqC,KAAZA,gBACrBG,WAAaH,QAAQrD,MAAM,KACtByD,MAAQ,EAAGA,MAAQD,WAAW/H,OAAQgI,QAAS,IACxC,IAAIC,OAAO,IAAMN,OAAS,aAC5BO,KAAKH,WAAWC,QAAS,KAE3BG,MADQ,IAAIF,OAAO,aACLG,KAAKL,WAAWC,eAC3BP,OAAOU,MAAM,YAIzB,MAMXrJ,wBAAwBgB,UAAUuI,aAAe,eACzCnJ,MAAQC,KACR4B,QAAU5B,KAAK4B,UACf5B,KAAKG,aACLyB,QAAU,QAGTvB,UAAUU,KAAK,wBAAwBuC,IAAI,iBAAiBC,MAAK,SAASC,IAAKM,MAChFxE,EAAEwE,MACGnB,IAAI,OAAQwG,WAAW7J,EAAEwE,MAAMlB,KAAK,YAAcuG,WAAWvH,UAC7De,IAAI,MAAOwG,WAAW7J,EAAEwE,MAAMlB,KAAK,YAAcuG,WAAWvH,UACjE7B,MAAMiD,mBAAmBc,KAAM,oBAG9BzD,UAAUU,KAAK,8BACf6E,MAAM5F,KAAKc,UAAU8E,SACrBC,OAAO7F,KAAKc,UAAU+E,cAEtB,IAAIxE,WAAa,EAAGA,WAAarB,KAAKF,iBAAiBe,OAAQQ,aAAc,KAE1E+H,aADWrJ,MAAMD,iBAAiBuB,YACVS,OACxBJ,MAAQ3B,MAAME,OAAOoB,YACrB4B,SAAWlD,MAAMG,UAAUmB,YAC/BK,MAAMG,MAAMuH,aAAcxH,SAC1BF,MAAM2H,UAAUpG,cAEZb,QAAUV,MAAMW,qBAChBiH,WAAatJ,KAAKK,UAAUU,KAAK,6CAA+CM,YACpFiI,WACK3G,IAAI,OAAQP,QAAQG,WAAWC,EAAK8G,WAAWrI,aAAe,EAAK,GACnE0B,IAAI,MAAOP,QAAQG,WAAWG,EAAK4G,WAAWpI,cAAgB,GACnEnB,MAAMiD,mBAAmBsG,WAAY,YAO7C3J,wBAAwBgB,UAAU4I,WAAa,eACvCxJ,MAAQC,UACPK,UAAUU,KAAK,6BAA6BwC,MAAK,SAASsF,MAAOW,cAC9D1F,KAAOxE,EAAEkK,UACTC,YAAc3F,KAAKG,QACvBwF,YAAYC,cACZD,YAAYnJ,SAAS,UACrBmJ,YAAYnJ,SAAS,SAAWP,MAAM6D,uBAAuBE,OAC7D2F,YAAYnJ,SAASP,MAAM4J,eAAe7F,MAAM,IAChD2F,YAAYnJ,SAAS,mBACrBwD,KAAK8F,OAAOH,iBAUpB9J,wBAAwBgB,UAAUkJ,UAAY,SAAS/F,aAC5C9D,KAAKuI,0BAA0BzE,KAAM,WAUhDnE,wBAAwBgB,UAAUgJ,eAAiB,SAAS7F,KAAMgG,qBAC1DC,UAAY,SAAW/J,KAAK6J,UAAU/F,aACtC9D,KAAKgK,eAAelG,QACpBiG,UAAY,YAGZD,gBACO,IAAMC,UAGVA,WASXpK,wBAAwBgB,UAAUwD,aAAe,SAASL,aAC/C9D,KAAKK,UAAUU,KAAK,gCACXf,KAAK4D,uBAAuBE,MAAQ9D,KAAK2J,eAAe7F,MAAM,GAAQ,qBAO1FnE,wBAAwBgB,UAAUsJ,SAAW,kBAClCjK,KAAKK,UAAUU,KAAK,iBAQ/BpB,wBAAwBgB,UAAUkG,aAAe,SAAS/C,MACtDA,KAAK4F,YAAY,gBACZpJ,SAAS,YACTqC,IAAI,MAAO,IACXA,IAAI,OAAQ,IACZA,IAAI,YAAa,QAClB8G,YAAczJ,KAAKmE,aAAaL,MACpC2F,YAAYS,MAAMpG,MAClB2F,YAAYC,YAAY,WAU5B/J,wBAAwBgB,UAAUuD,eAAiB,SAASJ,KAAMqG,eAAWC,wEACrEH,SAAWjK,KAAKiK,WAChBrI,QAAU5B,KAAK4B,UACnBkC,KAAK4F,YAAY,gBAAgBA,YAAY,gBACzChD,OAAS1G,KAAKyF,iBAAiB,IAAIjG,OAAO8F,MAAMxB,KAAKlB,KAAK,SAAUkB,KAAKlB,KAAK,WAC9EuH,WACArG,KAAKlB,KAAK,UAAW8D,OAAOlE,EAAIZ,SAASgB,KAAK,UAAW8D,OAAOhE,EAAId,SACpEkC,KAAKnB,IAAI,OAAQ+D,OAAOlE,GAAGG,IAAI,MAAO+D,OAAOhE,KAE7CoB,KAAKlB,KAAK,UAAW8D,OAAOlE,GAAGI,KAAK,UAAW8D,OAAOhE,GACtDoB,KAAKnB,IAAI,OAAQ+D,OAAOlE,EAAIZ,SAASe,IAAI,MAAO+D,OAAOhE,EAAId,UAG1DwI,aAEDtG,KAAKlB,KAAK,aAAchB,SAE5BqI,SAAS/H,OAAO4B,WACXd,mBAAmBc,KAAM,aAQlCnE,wBAAwBgB,UAAUyD,kBAAoB,SAASN,UACvDS,UAAYvE,KAAKqK,SAASvG,MAC1BwG,UAAYhC,OAAOtI,KAAKuI,0BAA0BhE,UAAW,cAC7DgG,yBAA2BvK,KAAKK,UAAUU,KAAK,8BAC3Cf,KAAK4D,uBAAuBE,MAAQ9D,KAAK2J,eAAe7F,MAAM,IAAOjD,OACzE2J,0BAA4BxK,KAAKK,UAAUU,KAAK,+BAC5Cf,KAAK4D,uBAAuBE,MAAQ9D,KAAK2J,eAAe7F,MAAM,IAAOR,IAAI,oBAAoBzC,WAEhGb,KAAKgK,eAAelG,QAChB9D,KAAKgK,eAAelG,OAASyG,yBAA2BD,YAA4C,IAA9BE,0BAAiC,KACxGC,UAAY3G,KAAKG,QACrBwG,UAAUnK,SAAS,YACdqC,IAAI,MAAO,IACXA,IAAI,OAAQ,IACZA,IAAI,YAAa,SACjBwB,aAAaL,MACb4F,YAAY,UACZQ,MAAMO,WACXtD,gBAAgBuD,yBAAyBD,aASjD9K,wBAAwBgB,UAAUmG,mBAAqB,SAAShD,cACxD6G,YAAc3K,KAAKK,UAAUU,KAAK,+BAClCf,KAAK4D,uBAAuBE,MAAQ9D,KAAK2J,eAAe7F,MAAM,IAAOR,IAAI,oBACzEsH,eAAiBD,YAAY9J,OAC1B+J,eAAiB,GACpBD,YAAYE,QAAQ7I,SACpB4I,kBAURjL,wBAAwBgB,UAAU0J,SAAW,SAASvG,UAC9CH,SAAW3D,KAAK4D,uBAAuBE,aACpC9D,KAAKK,UAAUU,KAAK,uBAAyB4C,WAQxDhE,wBAAwBgB,UAAUiB,QAAU,eACpCqG,MAAQjI,KAAKc,UACbgK,kBAAoB7C,MAAM8C,IAAI,GAAGC,oBACd/C,MAAMrC,QAEHkF,mBAS9BnL,wBAAwBgB,UAAUqC,mBAAqB,SAASiI,QAASC,UACjEtJ,QAAUuH,WAAWnJ,KAAK4B,WAC1B5B,KAAKG,aACLyB,QAAU,GAEdtC,EAAE2L,SAAStI,IAAI,qBACU,SAAWf,QAAU,qBACxB,SAAWA,QAAU,oBACtB,SAAWA,QAAU,mBACtB,SAAWA,QAAU,cACxB,SAAWA,QAAU,uBACdsJ,QAS5BvL,wBAAwBgB,UAAUqJ,eAAiB,SAASlG,aACjDA,KAAKyC,SAAS,aASzB5G,wBAAwBgB,UAAUD,2BAA6B,WAIvDV,KAAKO,kBAK0B,OAA/BP,KAAKmL,uBACLC,aAAapL,KAAKmL,uBAMlBnL,KAAKQ,wBAAwBK,OAAS,OACjCsK,sBAAwBE,YAAW,gBAC/B3K,+BACN,WAKFH,iBAAkB,OAClBgJ,kBACAnG,uBACAxC,mBAQTjB,wBAAwBgB,UAAUH,sBAAwB,kBAC/CR,KAAKK,UAAUU,KAAK,gCAAgCuC,KAAI,SAASS,EAAGuH,gBAChEtL,KAAKuL,cAAcD,aAUlC3L,wBAAwBgB,UAAU4K,cAAgB,SAASC,mBAChDA,WAAWC,UAAyC,IAA7BD,WAAWE,mBASzCvE,gBAAkB,CAKlBwE,0BAA0B,EAM1BC,+BAAgC,GAKhCzL,YAAY,EAKZ0L,sBAAsB,EAKtBC,UAAW,GASXC,KAAM,SAASnM,YAAaC,SAAUC,qBAClCqH,gBAAgB2E,UAAUlM,aACtB,IAAID,wBAAwBC,YAAaC,SAAUC,kBAClDqH,gBAAgBwE,2BACjBxE,gBAAgB6E,qBAChB7E,gBAAgBwE,0BAA2B,IAE1CxE,gBAAgByE,+BAA+BK,eAAerM,aAAc,CAC7EuH,gBAAgByE,+BAA+BhM,cAAe,MAE1DsM,kBAAoBpG,SAASC,eAAenG,aAC5CsM,kBAAkBC,UAAUC,SAAS,cACpCF,kBAAkBC,UAAUC,SAAS,6BAEtCjF,gBAAgBuD,yBAAyBpL,EAAE4M,mBAAmBnL,KAAK,0BACnEoG,gBAAgBuD,yBAAyBpL,EAAE4M,mBAAmBnL,KAAK,4BAQ/EiL,mBAAoB,WAChB1M,EAAE+M,QAAQC,GAAG,UAAU,WACnBnF,gBAAgBoF,oBAAmB,MAEvCF,OAAOG,iBAAiB,eAAe,WACnCrF,gBAAgBhH,YAAa,EAC7BgH,gBAAgBoF,mBAAmBpF,gBAAgBhH,eAEvDkM,OAAOG,iBAAiB,cAAc,WAClCrF,gBAAgBhH,YAAa,EAC7BgH,gBAAgBoF,mBAAmBpF,gBAAgBhH,eAEvDkL,YAAW,WACPlE,gBAAgBsF,2BACjB,MAQP/B,yBAA0B,SAASO,SAC/BA,QACKqB,GAAG,uBAAwBnF,gBAAgBnB,iBAC3CsG,GAAG,mBAAoBnF,gBAAgBE,gBACvCqF,SAAQ,SAASzG,GACdkB,gBAAgBwF,oBAAoB1G,GAAG,MAE1C2G,UAAS,SAAS3G,GACfkB,gBAAgBwF,oBAAoB1G,GAAG,OAQnDD,gBAAiB,SAASC,GACtBA,EAAE4B,qBACEgF,SAAW1F,gBAAgB2F,oBAAoB7G,GAC/C4G,UACAA,SAAS7G,gBAAgBC,IAQjCoB,eAAgB,SAASpB,OACjB4G,SAAW1F,gBAAgB2F,oBAAoB7G,GAC/C4G,UACAA,SAASxF,eAAepB,IAQhCsG,mBAAoB,SAASpM,gBACpB,IAAIP,eAAeuH,gBAAgB2E,UAChC3E,gBAAgB2E,UAAUG,eAAerM,eACzCuH,gBAAgB2E,UAAUlM,aAAaO,WAAaA,WACpDgH,gBAAgB2E,UAAUlM,aAAasJ,iBAUnDyD,oBAAqB,SAAS1G,EAAG8G,cAC7B5F,gBAAgB0E,qBAAuBkB,cAQ3CN,uBAAwB,WACftF,gBAAgB0E,2BACZU,mBAAmBpF,gBAAgBhH,YAK5CkL,YAAW,WACPlE,gBAAgBsF,uBAAuBtF,gBAAgBhH,cACxD,MAQP2M,oBAAqB,SAAS7G,OACtBrG,YAAcN,EAAE2G,EAAE+G,eAAe5G,QAAQ,iBAAiBsC,KAAK,aAC5DvB,gBAAgB2E,UAAUlM,cAMrCwH,gBAAiB,iBACP6F,aAAenH,SAASC,eAAe,gBAC7CrG,kBAAkBwN,gBAAgBD,sBAOnC,CASHlB,KAAM5E,gBAAgB4E"} \ No newline at end of file +{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Question class for drag and drop marker question type, used to support the question and preview pages.\n *\n * @module qtype_ddmarker/question\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'qtype_ddmarker/shapes',\n 'core/key_codes',\n 'core_form/changechecker'\n], function(\n $,\n dragDrop,\n Shapes,\n keys,\n FormChangeChecker\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop markers 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 {Object[]} visibleDropZones the geometry of any drop-zones to show.\n * Objects have fields shape, coords and markertext.\n * @constructor\n */\n function DragDropMarkersQuestion(containerId, readOnly, visibleDropZones) {\n var thisQ = this;\n this.containerId = containerId;\n this.visibleDropZones = visibleDropZones;\n this.shapes = [];\n this.shapeSVGs = [];\n this.isPrinting = false;\n this.questionAnswer = {};\n if (readOnly) {\n this.getRoot().addClass('qtype_ddmarker-readonly');\n }\n thisQ.allImagesLoaded = false;\n thisQ.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n thisQ.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Draws the svg shapes of any drop zones that should be visible for feedback purposes.\n */\n DragDropMarkersQuestion.prototype.drawDropzones = function() {\n if (this.visibleDropZones.length > 0) {\n var bgImage = this.bgImage();\n\n this.getRoot().find('div.dropzones').html('');\n var svg = this.getRoot().find('svg.dropzones');\n\n var nextColourIndex = 0;\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var colourClass = 'color' + nextColourIndex;\n nextColourIndex = (nextColourIndex + 1) % 8;\n this.addDropzone(svg, dropZoneNo, colourClass);\n }\n }\n };\n\n /**\n * Adds a dropzone shape with colour, coords and link provided to the array of shapes.\n *\n * @param {jQuery} svg the SVG image to which to add this drop zone.\n * @param {int} dropZoneNo which drop-zone to add.\n * @param {string} colourClass class name\n */\n DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {\n var dropZone = this.visibleDropZones[dropZoneNo],\n shape = Shapes.make(dropZone.shape, ''),\n existingmarkertext,\n bgRatio = this.bgRatio();\n if (!shape.parse(dropZone.coords, bgRatio)) {\n return;\n }\n\n existingmarkertext = this.getRoot().find('div.markertexts span.markertext' + dropZoneNo);\n if (existingmarkertext.length) {\n if (dropZone.markertext !== '') {\n existingmarkertext.html(dropZone.markertext);\n } else {\n existingmarkertext.remove();\n }\n } else if (dropZone.markertext !== '') {\n var classnames = 'markertext markertext' + dropZoneNo;\n this.getRoot().find('div.markertexts').append('' +\n dropZone.markertext + '');\n var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n if (markerspan.length) {\n var handles = shape.getHandlePositions();\n var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;\n var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);\n markerspan\n .css('left', positionLeft)\n .css('top', positionTop);\n markerspan\n .data('originX', markerspan.position().left / bgRatio)\n .data('originY', markerspan.position().top / bgRatio);\n this.handleElementScale(markerspan, 'center');\n }\n }\n\n var shapeSVG = shape.makeSvg(svg[0]);\n shapeSVG.setAttribute('class', 'dropzone ' + colourClass);\n\n this.shapes[this.shapes.length] = shape;\n this.shapeSVGs[this.shapeSVGs.length] = shapeSVG;\n };\n\n /**\n * Draws the drag items on the page (and drop zones if required).\n * The idea is to re-draw all the drags and drops whenever there is a change\n * like a widow resize or an item dropped in place.\n */\n DragDropMarkersQuestion.prototype.repositionDrags = function() {\n var root = this.getRoot(),\n thisQ = this;\n\n root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {\n $(item).addClass('unneeded');\n });\n\n root.find('input.choices').each(function(key, input) {\n var choiceNo = thisQ.getChoiceNoFromElement(input),\n imageCoords = thisQ.getImageCoords(input);\n if (imageCoords.length) {\n var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');\n drag.remove();\n for (var i = 0; i < imageCoords.length; i++) {\n var dragInDrop = drag.clone();\n // Convert image coords to screen coords.\n const screenCoords = thisQ.convertToWindowXY(imageCoords[i]);\n dragInDrop.data('pagex', screenCoords.x).data('pagey', screenCoords.y);\n // Save image coords to the drag item so we can use it later.\n dragInDrop.data('imageCoords', imageCoords[i]);\n // We always save the coordinates in the 1:1 ratio.\n // So we need to set the scale ratio to 1 for the initial load.\n dragInDrop.data('scaleRatio', 1);\n thisQ.sendDragToDrop(dragInDrop, false, true);\n }\n thisQ.getDragClone(drag).addClass('active');\n thisQ.cloneDragIfNeeded(drag);\n }\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropMarkersQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.choices').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropMarkersQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Determine what drag items need to be shown and\n * return coords of all drag items except any that are currently being dragged\n * based on contents of hidden inputs and whether drags are 'infinite' or how many\n * drags should be shown.\n *\n * @param {jQuery} inputNode\n * @returns {Point[]} image coordinates of however many copies of the drag item should be shown.\n */\n DragDropMarkersQuestion.prototype.getImageCoords = function(inputNode) {\n var imageCoords = [],\n val = $(inputNode).val();\n if (val !== '') {\n var coordsStrings = val.split(';');\n for (var i = 0; i < coordsStrings.length; i++) {\n imageCoords[i] = Shapes.Point.parse(coordsStrings[i]);\n }\n }\n return imageCoords;\n };\n\n /**\n * Converts the relative x and y position coordinates into\n * absolute x and y position coordinates.\n *\n * @param {Point} point relative to the background image.\n * @returns {Point} point relative to the page.\n */\n DragDropMarkersQuestion.prototype.convertToWindowXY = function(point) {\n var bgImage = this.bgImage();\n // The +1 seems rather odd, but seems to give the best results in\n // the three main browsers at a range of zoom levels.\n // (Its due to the 1px border around the image, that shifts the\n // image pixels by 1 down and to the left.)\n return point.offset(bgImage.offset().left + 1, bgImage.offset().top + 1);\n };\n\n /**\n * Utility function converting window coordinates to relative to the\n * background image coordinates.\n *\n * @param {Point} point relative to the page.\n * @returns {Point} point relative to the background image.\n */\n DragDropMarkersQuestion.prototype.convertToBgImgXY = function(point) {\n var bgImage = this.bgImage();\n return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);\n };\n\n /**\n * Is the point within the background image?\n *\n * @param {Point} point relative to the BG image.\n * @return {boolean} true it they are.\n */\n DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {\n var bgImage = this.bgImage();\n var bgPosition = bgImage.offset();\n\n return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()\n && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();\n };\n\n /**\n * Get the outer div for this question.\n * @returns {jQuery} containing that div.\n */\n DragDropMarkersQuestion.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 DragDropMarkersQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n DragDropMarkersQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n dragged = $(e.target).closest('.marker');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragged.addClass('beingdragged').css('transform', '');\n\n var placed = !dragged.hasClass('unneeded');\n if (!placed) {\n var hiddenDrag = thisQ.getDragClone(dragged);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n dragged.offset(hiddenDrag.offset());\n }\n }\n\n dragDrop.start(e, dragged, function() {\n void (1);\n }, function(x, y, dragged) {\n thisQ.dragEnd(dragged);\n });\n };\n\n /**\n * Functionality at the end of a drag drop.\n * @param {jQuery} dragged the marker that was dragged.\n */\n DragDropMarkersQuestion.prototype.dragEnd = function(dragged) {\n var placed = false,\n choiceNo = this.getChoiceNoFromElement(dragged),\n bgRatio = this.bgRatio(),\n dragXY;\n\n dragged.data('pagex', dragged.offset().left).data('pagey', dragged.offset().top);\n dragXY = new Shapes.Point(dragged.data('pagex'), dragged.data('pagey'));\n if (this.coordsInBgImg(dragXY)) {\n this.sendDragToDrop(dragged, true);\n placed = true;\n // Since we already move the drag item to new position.\n // Remove the image coords if this drag item have it.\n // We will get the new image coords for this drag item in saveCoordsForChoice.\n if (dragged.data('imageCoords')) {\n dragged.data('imageCoords', null);\n }\n // It seems that the dragdrop sometimes leaves the drag\n // one pixel out of position. Put it in exactly the right place.\n var bgImgXY = this.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);\n }\n\n if (!placed) {\n this.sendDragHome(dragged);\n this.removeDragIfNeeded(dragged);\n } else {\n this.cloneDragIfNeeded(dragged);\n }\n\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Save the coordinates for a dropped item in the form field.\n * @param {Number} choiceNo which copy of the choice this was.\n */\n DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) {\n let imageCoords = [];\n var items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),\n thiQ = this,\n bgRatio = this.bgRatio();\n\n if (items.length) {\n items.each(function() {\n var drag = $(this);\n if (!drag.hasClass('beingdragged') && !drag.data('imageCoords')) {\n if (drag.data('scaleRatio') !== bgRatio) {\n // The scale ratio for the draggable item was changed. We need to update that.\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n }\n var dragXY = new Shapes.Point(drag.data('pagex'), drag.data('pagey'));\n if (thiQ.coordsInBgImg(dragXY)) {\n var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n imageCoords[imageCoords.length] = bgImgXY;\n }\n } else if (drag.data('imageCoords')) {\n imageCoords[imageCoords.length] = drag.data('imageCoords');\n }\n });\n }\n\n this.getRoot().find('input.choice' + choiceNo).val(imageCoords.join(';'));\n if (this.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n this.questionAnswer = this.getQuestionAnsweredValues();\n }\n };\n\n /**\n * Handle key down / press events on markers.\n * @param {KeyboardEvent} e\n */\n DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {\n var drag = $(e.target).closest('.marker'),\n point = new Shapes.Point(drag.offset().left, drag.offset().top),\n choiceNo = this.getChoiceNoFromElement(drag);\n\n switch (e.keyCode) {\n case keys.arrowLeft:\n case 65: // A.\n point.x -= 1;\n break;\n case keys.arrowRight:\n case 68: // D.\n point.x += 1;\n break;\n case keys.arrowDown:\n case 83: // S.\n point.y += 1;\n break;\n case keys.arrowUp:\n case 87: // W.\n point.y -= 1;\n break;\n case keys.space:\n case keys.escape:\n point = null;\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n if (point !== null) {\n point = this.constrainToBgImg(point);\n drag.offset({'left': point.x, 'top': point.y});\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());\n if (this.coordsInBgImg(new Shapes.Point(drag.offset().left, drag.offset().top))) {\n if (drag.hasClass('unneeded')) {\n this.sendDragToDrop(drag, true);\n var hiddenDrag = this.getDragClone(drag);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n }\n this.cloneDragIfNeeded(drag);\n }\n }\n } else {\n drag.css('left', '').css('top', '');\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n this.sendDragHome(drag);\n this.removeDragIfNeeded(drag);\n }\n drag.focus();\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Makes sure the dragged item always exists within the background image area.\n *\n * @param {Point} windowxy\n * @returns {Point} coordinates\n */\n DragDropMarkersQuestion.prototype.constrainToBgImg = function(windowxy) {\n var bgImg = this.bgImage(),\n bgImgXY = this.convertToBgImgXY(windowxy);\n bgImgXY.x = Math.max(0, bgImgXY.x);\n bgImgXY.y = Math.max(0, bgImgXY.y);\n bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);\n bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);\n return this.convertToWindowXY(bgImgXY);\n };\n\n /**\n * Returns the choice number for a node.\n *\n * @param {Element|jQuery} node\n * @returns {Number}\n */\n DragDropMarkersQuestion.prototype.getChoiceNoFromElement = function(node) {\n return Number(this.getClassnameNumericSuffix(node, 'choice'));\n };\n\n /**\n * Returns the numeric part of a class with the given prefix.\n *\n * @param {Element|jQuery} node\n * @param {String} prefix\n * @returns {Number|null}\n */\n DragDropMarkersQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = $(node).attr('class');\n if (classes !== undefined && 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 * Handle when the window is resized.\n */\n DragDropMarkersQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('div.droparea .marker').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 this.getRoot().find('div.droparea svg.dropzones')\n .width(this.bgImage().width())\n .height(this.bgImage().height());\n\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var dropZone = thisQ.visibleDropZones[dropZoneNo];\n var originCoords = dropZone.coords;\n var shape = thisQ.shapes[dropZoneNo];\n var shapeSVG = thisQ.shapeSVGs[dropZoneNo];\n shape.parse(originCoords, bgRatio);\n shape.updateSvg(shapeSVG);\n\n var handles = shape.getHandlePositions();\n var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n markerSpan\n .css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)\n .css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));\n thisQ.handleElementScale(markerSpan, 'center');\n }\n };\n\n /**\n * Clone the drag.\n */\n DragDropMarkersQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('marker');\n placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));\n placeHolder.addClass(thisQ.getDragNoClass(drag, false));\n placeHolder.addClass('dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Get the drag number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the drag number.\n */\n DragDropMarkersQuestion.prototype.getDragNo = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'dragno');\n };\n\n /**\n * Get the drag number prefix of a drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} includeSelector include the CSS selector prefix or not.\n * @return {String} Class name\n */\n DragDropMarkersQuestion.prototype.getDragNoClass = function(drag, includeSelector) {\n var className = 'dragno' + this.getDragNo(drag);\n if (this.isInfiniteDrag(drag)) {\n className = 'infinite';\n }\n\n if (includeSelector) {\n return '.' + className;\n }\n\n return className;\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 DragDropMarkersQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draghomes' + ' span.marker' +\n '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');\n };\n\n /**\n * Get the drop area element.\n * @returns {jQuery} droparea element.\n */\n DragDropMarkersQuestion.prototype.dropArea = function() {\n return this.getRoot().find('div.droparea');\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropMarkersQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('beingdragged')\n .addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n var placeHolder = this.getDragClone(drag);\n placeHolder.after(drag);\n placeHolder.removeClass('active');\n };\n\n /**\n * Animate a drag item into a given place.\n *\n * @param {jQuery} drag the item to place.\n * @param {boolean} isScaling Scaling or not.\n * @param {boolean} initialLoad Whether it is the initial load or not.\n */\n DragDropMarkersQuestion.prototype.sendDragToDrop = function(drag, isScaling, initialLoad = false) {\n var dropArea = this.dropArea(),\n bgRatio = this.bgRatio();\n drag.removeClass('beingdragged').removeClass('unneeded');\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n if (isScaling) {\n drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);\n drag.css('left', dragXY.x).css('top', dragXY.y);\n } else {\n drag.data('originX', dragXY.x).data('originY', dragXY.y);\n drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);\n }\n // We need to save the original scale ratio for each draggable item.\n if (!initialLoad) {\n // Only set the scale ratio for a current being-dragged item, not for the initial loading.\n drag.data('scaleRatio', bgRatio);\n }\n dropArea.append(drag);\n this.handleElementScale(drag, 'left top');\n };\n\n /**\n * Clone the drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.cloneDragIfNeeded = function(drag) {\n var inputNode = this.getInput(drag),\n noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),\n displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,\n displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;\n\n if ((this.isInfiniteDrag(drag) ||\n !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {\n var dragClone = drag.clone();\n dragClone.addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n this.getDragClone(drag)\n .removeClass('active')\n .after(dragClone);\n questionManager.addEventHandlersToMarker(dragClone);\n }\n };\n\n /**\n * Remove the clone drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {\n var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');\n var displayedDrags = dragsInHome.length;\n while (displayedDrags > 1) {\n dragsInHome.first().remove();\n displayedDrags--;\n }\n };\n\n /**\n * Get the input belong to drag.\n *\n * @param {jQuery} drag the item to place.\n * @returns {jQuery} input element.\n */\n DragDropMarkersQuestion.prototype.getInput = function(drag) {\n var choiceNo = this.getChoiceNoFromElement(drag);\n return this.getRoot().find('input.choices.choice' + choiceNo);\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropMarkersQuestion.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 DragDropMarkersQuestion.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 * Check if the given drag is in infinite mode or not.\n *\n * @param {jQuery} drag The drag item need to check.\n */\n DragDropMarkersQuestion.prototype.isInfiniteDrag = function(drag) {\n return drag.hasClass('infinite');\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 DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded = function() {\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 this.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 this.cloneDrags();\n this.repositionDrags();\n this.drawDropzones();\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 DragDropMarkersQuestion.prototype.getNotYetLoadedImages = function() {\n return this.getRoot().find('.ddmarker img.dropbackground').not(function(i, imgNode) {\n return this.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 DragDropMarkersQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\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 marker event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n markerEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation.\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 * @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 {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.\n */\n init: function(containerId, readOnly, visibleDropZones) {\n questionManager.questions[containerId] =\n new DragDropMarkersQuestion(containerId, readOnly, visibleDropZones);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.markerEventHandlersInitialised[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('ddmarker') &&\n !questionContainer.classList.contains('qtype_ddmarker-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker'));\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker'));\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 $(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 event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToMarker: function(element) {\n element\n .on('mousedown touchstart', questionManager.handleDragStart)\n .on('keydown keypress', questionManager.handleKeyPress)\n .focusin(function(e) {\n questionManager.handleKeyboardFocus(e, true);\n })\n .focusout(function(e) {\n questionManager.handleKeyboardFocus(e, false);\n });\n },\n\n /**\n * Handle mouse down / touch start events on markers.\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 markers.\n * @param {Event} e\n */\n handleKeyPress: function(e) {\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 * Handle focus lost events on markers.\n * @param {Event} e\n * @param {boolean} isNavigating\n */\n handleKeyboardFocus: function(e, isNavigating) {\n questionManager.isKeyboardNavigation = isNavigating;\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 if (!questionManager.isKeyboardNavigation) {\n this.handleWindowResize(questionManager.isPrinting);\n }\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 * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropMarkersQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddmarker').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddmarker/question\n */\n return {\n /**\n * Initialise one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {String} bgImgUrl the URL of the background image.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {String[]} visibleDropZones the geometry of any drop-zones to show.\n */\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","Shapes","keys","FormChangeChecker","DragDropMarkersQuestion","containerId","readOnly","visibleDropZones","thisQ","this","shapes","shapeSVGs","isPrinting","questionAnswer","getRoot","addClass","allImagesLoaded","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","drawDropzones","length","bgImage","find","html","outerWidth","outerHeight","svg","nextColourIndex","dropZoneNo","colourClass","addDropzone","existingmarkertext","dropZone","shape","make","bgRatio","parse","coords","markertext","remove","classnames","append","markerspan","handles","getHandlePositions","positionLeft","moveHandle","x","positionTop","y","css","data","position","left","top","handleElementScale","shapeSVG","makeSvg","setAttribute","repositionDrags","root","not","each","key","item","input","choiceNo","getChoiceNoFromElement","imageCoords","getImageCoords","drag","i","dragInDrop","clone","screenCoords","convertToWindowXY","sendDragToDrop","getDragClone","cloneDragIfNeeded","getQuestionAnsweredValues","result","inputNode","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","val","coordsStrings","split","Point","point","offset","convertToBgImgXY","coordsInBgImg","bgPosition","width","height","document","getElementById","handleDragStart","e","dragged","target","closest","prepare","start","hasClass","hiddenDrag","dragEnd","dragXY","placed","bgImgXY","sendDragHome","removeDragIfNeeded","saveCoordsForChoice","items","thiQ","join","questionManager","handleFormDirty","handleKeyPress","keyCode","arrowLeft","arrowRight","arrowDown","arrowUp","space","escape","preventDefault","constrainToBgImg","focus","windowxy","bgImg","Math","max","min","node","Number","getClassnameNumericSuffix","prefix","classes","attr","undefined","classesarr","index","RegExp","test","match","exec","handleResize","parseFloat","originCoords","updateSvg","markerSpan","cloneDrags","draghome","placeHolder","removeClass","getDragNoClass","before","getDragNo","includeSelector","className","isInfiniteDrag","dropArea","after","isScaling","initialLoad","getInput","noOfDrags","displayedDragsInDropArea","displayedDragsInDragHomes","dragClone","addEventHandlersToMarker","dragsInHome","displayedDrags","first","bgImgNaturalWidth","get","naturalWidth","element","type","imageLoadingTimeoutId","clearTimeout","setTimeout","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","eventHandlersInitialised","markerEventHandlersInitialised","isKeyboardNavigation","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","classList","contains","window","on","handleWindowResize","addEventListener","fixLayoutIfThingsMoved","focusin","handleKeyboardFocus","focusout","question","getQuestionForEvent","isNavigating","currentTarget","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file diff --git a/question/type/ddmarker/amd/src/question.js b/question/type/ddmarker/amd/src/question.js index abaf464c2f8..6ff3d7df726 100644 --- a/question/type/ddmarker/amd/src/question.js +++ b/question/type/ddmarker/amd/src/question.js @@ -149,13 +149,17 @@ define([ root.find('input.choices').each(function(key, input) { var choiceNo = thisQ.getChoiceNoFromElement(input), - coords = thisQ.getCoords(input); - if (coords.length) { + imageCoords = thisQ.getImageCoords(input); + if (imageCoords.length) { var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder'); drag.remove(); - for (var i = 0; i < coords.length; i++) { + for (var i = 0; i < imageCoords.length; i++) { var dragInDrop = drag.clone(); - dragInDrop.data('pagex', coords[i].x).data('pagey', coords[i].y); + // Convert image coords to screen coords. + const screenCoords = thisQ.convertToWindowXY(imageCoords[i]); + dragInDrop.data('pagex', screenCoords.x).data('pagey', screenCoords.y); + // Save image coords to the drag item so we can use it later. + dragInDrop.data('imageCoords', imageCoords[i]); // We always save the coordinates in the 1:1 ratio. // So we need to set the scale ratio to 1 for the initial load. dragInDrop.data('scaleRatio', 1); @@ -216,18 +220,18 @@ define([ * drags should be shown. * * @param {jQuery} inputNode - * @returns {Point[]} coordinates of however many copies of the drag item should be shown. + * @returns {Point[]} image coordinates of however many copies of the drag item should be shown. */ - DragDropMarkersQuestion.prototype.getCoords = function(inputNode) { - var coords = [], + DragDropMarkersQuestion.prototype.getImageCoords = function(inputNode) { + var imageCoords = [], val = $(inputNode).val(); if (val !== '') { var coordsStrings = val.split(';'); for (var i = 0; i < coordsStrings.length; i++) { - coords[i] = this.convertToWindowXY(Shapes.Point.parse(coordsStrings[i])); + imageCoords[i] = Shapes.Point.parse(coordsStrings[i]); } } - return coords; + return imageCoords; }; /** @@ -330,7 +334,12 @@ define([ if (this.coordsInBgImg(dragXY)) { this.sendDragToDrop(dragged, true); placed = true; - + // Since we already move the drag item to new position. + // Remove the image coords if this drag item have it. + // We will get the new image coords for this drag item in saveCoordsForChoice. + if (dragged.data('imageCoords')) { + dragged.data('imageCoords', null); + } // It seems that the dragdrop sometimes leaves the drag // one pixel out of position. Put it in exactly the right place. var bgImgXY = this.convertToBgImgXY(dragXY); @@ -353,15 +362,15 @@ define([ * @param {Number} choiceNo which copy of the choice this was. */ DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) { - var coords = [], - items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo), + let imageCoords = []; + var items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo), thiQ = this, bgRatio = this.bgRatio(); if (items.length) { items.each(function() { var drag = $(this); - if (!drag.hasClass('beingdragged')) { + if (!drag.hasClass('beingdragged') && !drag.data('imageCoords')) { if (drag.data('scaleRatio') !== bgRatio) { // The scale ratio for the draggable item was changed. We need to update that. drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top); @@ -370,13 +379,15 @@ define([ if (thiQ.coordsInBgImg(dragXY)) { var bgImgXY = thiQ.convertToBgImgXY(dragXY); bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio); - coords[coords.length] = bgImgXY; + imageCoords[imageCoords.length] = bgImgXY; } + } else if (drag.data('imageCoords')) { + imageCoords[imageCoords.length] = drag.data('imageCoords'); } }); } - this.getRoot().find('input.choice' + choiceNo).val(coords.join(';')); + this.getRoot().find('input.choice' + choiceNo).val(imageCoords.join(';')); if (this.isQuestionInteracted()) { // The user has interacted with the draggable items. We need to mark the form as dirty. questionManager.handleFormDirty(); diff --git a/question/type/ddmarker/tests/behat/previewquiz.feature b/question/type/ddmarker/tests/behat/previewquiz.feature new file mode 100644 index 00000000000..14085c3b3b7 --- /dev/null +++ b/question/type/ddmarker/tests/behat/previewquiz.feature @@ -0,0 +1,50 @@ +@qtype @qtype_ddmarker +Feature: Preview a quiz with multiple maker question. + As a teacher + In order to check my drag-drop marker questions will work for students + I need to preview them in quiz with multiple questions. + + Background: + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | template | + | Test questions | ddmarker | Drag markers | mkmap | + | Test questions | ddmarker | Drag markers 2 | mkmap | + And the following "activities" exist: + | activity | name | course | idnumber | + | quiz | Test quiz | C1 | quiz1 | + And quiz "Test quiz" contains the following questions: + | Drag markers | 1 | + | Drag markers 2 | 2 | + + @javascript + Scenario: Preview a quiz with multiple markers question + Given I am on the "Test quiz" "mod_quiz > View" page logged in as "admin" + And I press "Preview quiz" + # Add change window size so we can drag-drop OU marker on 322,213 coordinates on firefox. + And I change viewport size to "large" + # Drag items and go back and forth between the question. + And I drag "OU" to "322,213" in the drag and drop markers question + And I drag "Railway station" to "144,84" in the drag and drop markers question + And I drag "Railway station" to "195,180" in the drag and drop markers question + And I press "Next page" + And I drag "OU" to "322,213" in the drag and drop markers question + And I drag "Railway station" to "144,84" in the drag and drop markers question + And I drag "Railway station" to "195,180" in the drag and drop markers question + And I press "Previous page" + And I drag "Railway station" to "267,302" in the drag and drop markers question + And I press "Next page" + And I drag "Railway station" to "267,302" in the drag and drop markers question + And I press "Previous page" + And I press "Next page" + And I press "Finish attempt ..." + And I press "Submit all and finish" + When I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue" + Then I should see "2.00/2.00" + And the state of "Please place the markers on the map of Milton Keynes and be aware that" question is shown as "Correct" + And I should see "Well done!"