| 1 | efrain | 1 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 2 | //
 | 
        
           |  |  | 3 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 4 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 5 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 6 | // (at your option) any later version.
 | 
        
           |  |  | 7 | //
 | 
        
           |  |  | 8 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 11 | // GNU General Public License for more details.
 | 
        
           |  |  | 12 | //
 | 
        
           |  |  | 13 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 14 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 15 |   | 
        
           |  |  | 16 | /*
 | 
        
           |  |  | 17 |  * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.
 | 
        
           |  |  | 18 |  *
 | 
        
           |  |  | 19 |  * @module     qtype_ddimageortext/question
 | 
        
           |  |  | 20 |  * @copyright  2018 The Open University
 | 
        
           |  |  | 21 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 22 |  */
 | 
        
           |  |  | 23 | define([
 | 
        
           |  |  | 24 |     'jquery',
 | 
        
           |  |  | 25 |     'core/dragdrop',
 | 
        
           |  |  | 26 |     'core/key_codes',
 | 
        
           |  |  | 27 |     'core_form/changechecker'
 | 
        
           |  |  | 28 | ], function(
 | 
        
           |  |  | 29 |     $,
 | 
        
           |  |  | 30 |     dragDrop,
 | 
        
           |  |  | 31 |     keys,
 | 
        
           |  |  | 32 |     FormChangeChecker
 | 
        
           |  |  | 33 | ) {
 | 
        
           |  |  | 34 |   | 
        
           |  |  | 35 |     "use strict";
 | 
        
           |  |  | 36 |   | 
        
           |  |  | 37 |     /**
 | 
        
           |  |  | 38 |      * Initialise one drag-drop onto image question.
 | 
        
           |  |  | 39 |      *
 | 
        
           |  |  | 40 |      * @param {String} containerId id of the outer div for this question.
 | 
        
           |  |  | 41 |      * @param {boolean} readOnly whether the question is being displayed read-only.
 | 
        
           |  |  | 42 |      * @param {Array} places Information about the drop places.
 | 
        
           |  |  | 43 |      * @constructor
 | 
        
           |  |  | 44 |      */
 | 
        
           |  |  | 45 |     function DragDropOntoImageQuestion(containerId, readOnly, places) {
 | 
        
           |  |  | 46 |         this.containerId = containerId;
 | 
        
           |  |  | 47 |         this.questionAnswer = {};
 | 
        
           |  |  | 48 |         M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);
 | 
        
           |  |  | 49 |         this.places = places;
 | 
        
           |  |  | 50 |         this.allImagesLoaded = false;
 | 
        
           |  |  | 51 |         this.imageLoadingTimeoutId = null;
 | 
        
           |  |  | 52 |         this.isPrinting = false;
 | 
        
           |  |  | 53 |         if (readOnly) {
 | 
        
           |  |  | 54 |             this.getRoot().addClass('qtype_ddimageortext-readonly');
 | 
        
           |  |  | 55 |         }
 | 
        
           |  |  | 56 |   | 
        
           |  |  | 57 |         var thisQ = this;
 | 
        
           |  |  | 58 |         this.getNotYetLoadedImages().one('load', function() {
 | 
        
           |  |  | 59 |             thisQ.waitForAllImagesToBeLoaded();
 | 
        
           |  |  | 60 |         });
 | 
        
           |  |  | 61 |         this.waitForAllImagesToBeLoaded();
 | 
        
           |  |  | 62 |     }
 | 
        
           |  |  | 63 |   | 
        
           |  |  | 64 |     /**
 | 
        
           |  |  | 65 |      * Waits until all images are loaded before calling setupQuestion().
 | 
        
           |  |  | 66 |      *
 | 
        
           |  |  | 67 |      * This function is called from the onLoad of each image, and also polls with
 | 
        
           |  |  | 68 |      * a time-out, because image on-loads are allegedly unreliable.
 | 
        
           |  |  | 69 |      */
 | 
        
           |  |  | 70 |     DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {
 | 
        
           |  |  | 71 |         var thisQ = this;
 | 
        
           |  |  | 72 |   | 
        
           |  |  | 73 |         // This method may get called multiple times (via image on-loads or timeouts.
 | 
        
           |  |  | 74 |         // If we are already done, don't do it again.
 | 
        
           |  |  | 75 |         if (this.allImagesLoaded) {
 | 
        
           |  |  | 76 |             return;
 | 
        
           |  |  | 77 |         }
 | 
        
           |  |  | 78 |   | 
        
           |  |  | 79 |         // Clear any current timeout, if set.
 | 
        
           |  |  | 80 |         if (this.imageLoadingTimeoutId !== null) {
 | 
        
           |  |  | 81 |             clearTimeout(this.imageLoadingTimeoutId);
 | 
        
           |  |  | 82 |         }
 | 
        
           |  |  | 83 |   | 
        
           |  |  | 84 |         // If we have not yet loaded all images, set a timeout to
 | 
        
           |  |  | 85 |         // call ourselves again, since apparently images on-load
 | 
        
           |  |  | 86 |         // events are flakey.
 | 
        
           |  |  | 87 |         if (this.getNotYetLoadedImages().length > 0) {
 | 
        
           |  |  | 88 |             this.imageLoadingTimeoutId = setTimeout(function() {
 | 
        
           |  |  | 89 |                 thisQ.waitForAllImagesToBeLoaded();
 | 
        
           |  |  | 90 |             }, 100);
 | 
        
           |  |  | 91 |             return;
 | 
        
           |  |  | 92 |         }
 | 
        
           |  |  | 93 |   | 
        
           |  |  | 94 |         // We now have all images. Carry on, but only after giving the layout a chance to settle down.
 | 
        
           |  |  | 95 |         this.allImagesLoaded = true;
 | 
        
           |  |  | 96 |         thisQ.setupQuestion();
 | 
        
           |  |  | 97 |     };
 | 
        
           |  |  | 98 |   | 
        
           |  |  | 99 |     /**
 | 
        
           |  |  | 100 |      * Get any of the images in the drag-drop area that are not yet fully loaded.
 | 
        
           |  |  | 101 |      *
 | 
        
           |  |  | 102 |      * @returns {jQuery} those images.
 | 
        
           |  |  | 103 |      */
 | 
        
           |  |  | 104 |     DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {
 | 
        
           |  |  | 105 |         var thisQ = this;
 | 
        
           |  |  | 106 |         return this.getRoot().find('.ddarea img').not(function(i, imgNode) {
 | 
        
           |  |  | 107 |             return thisQ.imageIsLoaded(imgNode);
 | 
        
           |  |  | 108 |         });
 | 
        
           |  |  | 109 |     };
 | 
        
           |  |  | 110 |   | 
        
           |  |  | 111 |     /**
 | 
        
           |  |  | 112 |      * Check if an image has loaded without errors.
 | 
        
           |  |  | 113 |      *
 | 
        
           |  |  | 114 |      * @param {HTMLImageElement} imgElement an image.
 | 
        
           |  |  | 115 |      * @returns {boolean} true if this image has loaded without errors.
 | 
        
           |  |  | 116 |      */
 | 
        
           |  |  | 117 |     DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {
 | 
        
           |  |  | 118 |         return imgElement.complete && imgElement.naturalHeight !== 0;
 | 
        
           |  |  | 119 |     };
 | 
        
           |  |  | 120 |   | 
        
           |  |  | 121 |     /**
 | 
        
           |  |  | 122 |      * Set up the question, once all images have been loaded.
 | 
        
           |  |  | 123 |      */
 | 
        
           |  |  | 124 |     DragDropOntoImageQuestion.prototype.setupQuestion = function() {
 | 
        
           |  |  | 125 |         this.resizeAllDragsAndDrops();
 | 
        
           |  |  | 126 |         this.cloneDrags();
 | 
        
           |  |  | 127 |         this.positionDragsAndDrops();
 | 
        
           |  |  | 128 |         M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);
 | 
        
           |  |  | 129 |     };
 | 
        
           |  |  | 130 |   | 
        
           |  |  | 131 |     /**
 | 
        
           |  |  | 132 |      * In each group, resize all the items to be the same size.
 | 
        
           |  |  | 133 |      */
 | 
        
           |  |  | 134 |     DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {
 | 
        
           |  |  | 135 |         var thisQ = this;
 | 
        
           |  |  | 136 |         this.getRoot().find('.draghomes > div').each(function(i, node) {
 | 
        
           |  |  | 137 |             thisQ.resizeAllDragsAndDropsInGroup(
 | 
        
           |  |  | 138 |                     thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));
 | 
        
           |  |  | 139 |         });
 | 
        
           |  |  | 140 |     };
 | 
        
           |  |  | 141 |   | 
        
           |  |  | 142 |     /**
 | 
        
           |  |  | 143 |      * In a given group, set all the drags and drops to be the same size.
 | 
        
           |  |  | 144 |      *
 | 
        
           |  |  | 145 |      * @param {int} group the group number.
 | 
        
           |  |  | 146 |      */
 | 
        
           |  |  | 147 |     DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {
 | 
        
           |  |  | 148 |         var root = this.getRoot(),
 | 
        
           |  |  | 149 |             dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),
 | 
        
           |  |  | 150 |             maxWidth = 0,
 | 
        
           |  |  | 151 |             maxHeight = 0;
 | 
        
           |  |  | 152 |   | 
        
           |  |  | 153 |         // Find the maximum size of any drag in this groups.
 | 
        
           |  |  | 154 |         dragHomes.each(function(i, drag) {
 | 
        
           |  |  | 155 |             maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));
 | 
        
           |  |  | 156 |             maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));
 | 
        
           |  |  | 157 |         });
 | 
        
           |  |  | 158 |   | 
        
           |  |  | 159 |         // The size we will want to set is a bit bigger than this.
 | 
        
           |  |  | 160 |         maxWidth += 10;
 | 
        
           |  |  | 161 |         maxHeight += 10;
 | 
        
           |  |  | 162 |   | 
        
           |  |  | 163 |         // Set each drag home to that size.
 | 
        
           |  |  | 164 |         dragHomes.each(function(i, drag) {
 | 
        
           |  |  | 165 |             var left = Math.round((maxWidth - drag.offsetWidth) / 2),
 | 
        
           |  |  | 166 |                 top = Math.floor((maxHeight - drag.offsetHeight) / 2);
 | 
        
           |  |  | 167 |             // Set top and left padding so the item is centred.
 | 
        
           |  |  | 168 |             $(drag).css({
 | 
        
           |  |  | 169 |                 'padding-left': left + 'px',
 | 
        
           |  |  | 170 |                 'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',
 | 
        
           |  |  | 171 |                 'padding-top': top + 'px',
 | 
        
           |  |  | 172 |                 'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'
 | 
        
           |  |  | 173 |             });
 | 
        
           |  |  | 174 |         });
 | 
        
           |  |  | 175 |   | 
        
           |  |  | 176 |         // Create the drops and make them the right size.
 | 
        
           |  |  | 177 |         for (var i in this.places) {
 | 
        
           |  |  | 178 |             if (!this.places.hasOwnProperty((i))) {
 | 
        
           |  |  | 179 |                 continue;
 | 
        
           |  |  | 180 |             }
 | 
        
           |  |  | 181 |             var place = this.places[i],
 | 
        
           |  |  | 182 |                 label = place.text;
 | 
        
           |  |  | 183 |             if (parseInt(place.group) !== group) {
 | 
        
           |  |  | 184 |                 continue;
 | 
        
           |  |  | 185 |             }
 | 
        
           |  |  | 186 |             if (label === '') {
 | 
        
           |  |  | 187 |                 label = M.util.get_string('blank', 'qtype_ddimageortext');
 | 
        
           |  |  | 188 |             }
 | 
        
           |  |  | 189 |             root.find('.dropzones').append('<div class="dropzone active group' + place.group +
 | 
        
           |  |  | 190 |                             ' place' + i + '" tabindex="0">' +
 | 
        
           |  |  | 191 |                     '<span class="accesshide">' + label + '</span> </div>');
 | 
        
           |  |  | 192 |             root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);
 | 
        
           |  |  | 193 |         }
 | 
        
           |  |  | 194 |     };
 | 
        
           |  |  | 195 |   | 
        
           |  |  | 196 |     /**
 | 
        
           |  |  | 197 |      * Invisible 'drag homes' are output by the renderer. These have the same properties
 | 
        
           |  |  | 198 |      * as the drag items but are invisible. We clone these invisible elements to make the
 | 
        
           |  |  | 199 |      * actual drag items.
 | 
        
           |  |  | 200 |      */
 | 
        
           |  |  | 201 |     DragDropOntoImageQuestion.prototype.cloneDrags = function() {
 | 
        
           |  |  | 202 |         var thisQ = this;
 | 
        
           |  |  | 203 |         thisQ.getRoot().find('.draghome').each(function(index, dragHome) {
 | 
        
           |  |  | 204 |             var drag = $(dragHome);
 | 
        
           |  |  | 205 |             var placeHolder = drag.clone();
 | 
        
           |  |  | 206 |             placeHolder.removeClass();
 | 
        
           |  |  | 207 |             placeHolder.addClass('draghome choice' +
 | 
        
           |  |  | 208 |                 thisQ.getChoice(drag) + ' group' +
 | 
        
           |  |  | 209 |                 thisQ.getGroup(drag) + ' dragplaceholder');
 | 
        
           |  |  | 210 |             drag.before(placeHolder);
 | 
        
           |  |  | 211 |         });
 | 
        
           |  |  | 212 |     };
 | 
        
           |  |  | 213 |   | 
        
           |  |  | 214 |     /**
 | 
        
           |  |  | 215 |      * Clone drag item for one choice.
 | 
        
           |  |  | 216 |      *
 | 
        
           |  |  | 217 |      * @param {jQuery} dragHome the drag home to clone.
 | 
        
           |  |  | 218 |      */
 | 
        
           |  |  | 219 |     DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {
 | 
        
           |  |  | 220 |         if (dragHome.hasClass('infinite')) {
 | 
        
           |  |  | 221 |             var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));
 | 
        
           |  |  | 222 |             for (var i = 0; i < noOfDrags; i++) {
 | 
        
           |  |  | 223 |                 this.cloneDrag(dragHome);
 | 
        
           |  |  | 224 |             }
 | 
        
           |  |  | 225 |         } else {
 | 
        
           |  |  | 226 |             this.cloneDrag(dragHome);
 | 
        
           |  |  | 227 |         }
 | 
        
           |  |  | 228 |     };
 | 
        
           |  |  | 229 |   | 
        
           |  |  | 230 |     /**
 | 
        
           |  |  | 231 |      * Clone drag item.
 | 
        
           |  |  | 232 |      *
 | 
        
           |  |  | 233 |      * @param {jQuery} dragHome
 | 
        
           |  |  | 234 |      */
 | 
        
           |  |  | 235 |     DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {
 | 
        
           |  |  | 236 |         var drag = dragHome.clone();
 | 
        
           |  |  | 237 |         drag.removeClass('draghome')
 | 
        
           |  |  | 238 |             .addClass('drag unplaced moodle-has-zindex')
 | 
        
           |  |  | 239 |             .offset(dragHome.offset());
 | 
        
           |  |  | 240 |         this.getRoot().find('.dragitems').append(drag);
 | 
        
           |  |  | 241 |     };
 | 
        
           |  |  | 242 |   | 
        
           |  |  | 243 |     /**
 | 
        
           |  |  | 244 |      * Update the position of drags.
 | 
        
           |  |  | 245 |      */
 | 
        
           |  |  | 246 |     DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {
 | 
        
           |  |  | 247 |         var thisQ = this,
 | 
        
           |  |  | 248 |             root = this.getRoot(),
 | 
        
           |  |  | 249 |             bgRatio = this.bgRatio();
 | 
        
           |  |  | 250 |   | 
        
           |  |  | 251 |         // Move the drops into position.
 | 
        
           |  |  | 252 |         root.find('.ddarea .dropzone').each(function(i, dropNode) {
 | 
        
           |  |  | 253 |             var drop = $(dropNode),
 | 
        
           |  |  | 254 |                 place = thisQ.places[thisQ.getPlace(drop)];
 | 
        
           |  |  | 255 |             // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation.
 | 
        
           |  |  | 256 |             drop.css('left', parseInt(place.xy[0]) * bgRatio)
 | 
        
           |  |  | 257 |                 .css('top', parseInt(place.xy[1]) * bgRatio);
 | 
        
           |  |  | 258 |             drop.data('originX', parseInt(place.xy[0]))
 | 
        
           |  |  | 259 |                 .data('originY', parseInt(place.xy[1]));
 | 
        
           |  |  | 260 |             thisQ.handleElementScale(drop, 'left top');
 | 
        
           |  |  | 261 |         });
 | 
        
           |  |  | 262 |   | 
        
           |  |  | 263 |         // First move all items back home.
 | 
        
           |  |  | 264 |         root.find('.draghome').not('.dragplaceholder').each(function(i, dragNode) {
 | 
        
           |  |  | 265 |             var drag = $(dragNode),
 | 
        
           |  |  | 266 |                 currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');
 | 
        
           |  |  | 267 |             drag.addClass('unplaced')
 | 
        
           |  |  | 268 |                 .removeClass('placed');
 | 
        
           |  |  | 269 |             drag.removeAttr('tabindex');
 | 
        
           |  |  | 270 |             if (currentPlace !== null) {
 | 
        
           |  |  | 271 |                 drag.removeClass('inplace' + currentPlace);
 | 
        
           |  |  | 272 |             }
 | 
        
           |  |  | 273 |         });
 | 
        
           |  |  | 274 |   | 
        
           |  |  | 275 |         // Then place the ones that should be placed.
 | 
        
           |  |  | 276 |         root.find('input.placeinput').each(function(i, inputNode) {
 | 
        
           |  |  | 277 |             var input = $(inputNode),
 | 
        
           |  |  | 278 |                 choice = input.val();
 | 
        
           |  |  | 279 |             if (choice.length === 0 || (choice.length > 0 && choice === '0')) {
 | 
        
           |  |  | 280 |                 // No item in this place.
 | 
        
           |  |  | 281 |                 return;
 | 
        
           |  |  | 282 |             }
 | 
        
           |  |  | 283 |   | 
        
           |  |  | 284 |             var place = thisQ.getPlace(input);
 | 
        
           |  |  | 285 |             // Get the unplaced drag.
 | 
        
           |  |  | 286 |             var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);
 | 
        
           |  |  | 287 |             // Get the clone of the drag.
 | 
        
           |  |  | 288 |             var hiddenDrag = thisQ.getDragClone(unplacedDrag);
 | 
        
           |  |  | 289 |             if (hiddenDrag.length) {
 | 
        
           |  |  | 290 |                 if (unplacedDrag.hasClass('infinite')) {
 | 
        
           |  |  | 291 |                     var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));
 | 
        
           |  |  | 292 |                     var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);
 | 
        
           |  |  | 293 |                     if (cloneDrags.length < noOfDrags) {
 | 
        
           |  |  | 294 |                         var cloneDrag = unplacedDrag.clone();
 | 
        
           |  |  | 295 |                         cloneDrag.removeClass('beingdragged');
 | 
        
           |  |  | 296 |                         cloneDrag.removeAttr('tabindex');
 | 
        
           |  |  | 297 |                         hiddenDrag.after(cloneDrag);
 | 
        
           |  |  | 298 |                         // Sometimes, for the question that has a lot of input groups and unlimited draggable items,
 | 
        
           |  |  | 299 |                         // this 'clone' process takes longer than usual, so the questionManager.init() method
 | 
        
           |  |  | 300 |                         // will not add the eventHandler for this cloned drag.
 | 
        
           |  |  | 301 |                         // We need to make sure to add the eventHandler for the cloned drag too.
 | 
        
           |  |  | 302 |                         questionManager.addEventHandlersToDrag(cloneDrag);
 | 
        
           |  |  | 303 |                     } else {
 | 
        
           |  |  | 304 |                         hiddenDrag.addClass('active');
 | 
        
           |  |  | 305 |                     }
 | 
        
           |  |  | 306 |                 } else {
 | 
        
           |  |  | 307 |                     hiddenDrag.addClass('active');
 | 
        
           |  |  | 308 |                 }
 | 
        
           |  |  | 309 |             }
 | 
        
           |  |  | 310 |   | 
        
           |  |  | 311 |             // Send the drag to drop.
 | 
        
           |  |  | 312 |             var drop = root.find('.dropzone.place' + place);
 | 
        
           |  |  | 313 |             thisQ.sendDragToDrop(unplacedDrag, drop);
 | 
        
           |  |  | 314 |         });
 | 
        
           |  |  | 315 |   | 
        
           |  |  | 316 |         // Save the question answer.
 | 
        
           |  |  | 317 |         thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();
 | 
        
           |  |  | 318 |     };
 | 
        
           |  |  | 319 |   | 
        
           |  |  | 320 |     /**
 | 
        
           |  |  | 321 |      * Get the question answered values.
 | 
        
           |  |  | 322 |      *
 | 
        
           |  |  | 323 |      * @return {Object} Contain key-value with key is the input id and value is the input value.
 | 
        
           |  |  | 324 |      */
 | 
        
           |  |  | 325 |     DragDropOntoImageQuestion.prototype.getQuestionAnsweredValues = function() {
 | 
        
           |  |  | 326 |         let result = {};
 | 
        
           |  |  | 327 |         this.getRoot().find('input.placeinput').each((i, inputNode) => {
 | 
        
           |  |  | 328 |             result[inputNode.id] = inputNode.value;
 | 
        
           |  |  | 329 |         });
 | 
        
           |  |  | 330 |   | 
        
           |  |  | 331 |         return result;
 | 
        
           |  |  | 332 |     };
 | 
        
           |  |  | 333 |   | 
        
           |  |  | 334 |     /**
 | 
        
           |  |  | 335 |      * Check if the question is being interacted or not.
 | 
        
           |  |  | 336 |      *
 | 
        
           |  |  | 337 |      * @return {boolean} Return true if the user has changed the question-answer.
 | 
        
           |  |  | 338 |      */
 | 
        
           |  |  | 339 |     DragDropOntoImageQuestion.prototype.isQuestionInteracted = function() {
 | 
        
           |  |  | 340 |         const oldAnswer = this.questionAnswer;
 | 
        
           |  |  | 341 |         const newAnswer = this.getQuestionAnsweredValues();
 | 
        
           |  |  | 342 |         let isInteracted = false;
 | 
        
           |  |  | 343 |   | 
        
           |  |  | 344 |         // First, check both answers have the same structure or not.
 | 
        
           |  |  | 345 |         if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {
 | 
        
           |  |  | 346 |             isInteracted = true;
 | 
        
           |  |  | 347 |             return isInteracted;
 | 
        
           |  |  | 348 |         }
 | 
        
           |  |  | 349 |         // Check the values.
 | 
        
           |  |  | 350 |         Object.keys(newAnswer).forEach(key => {
 | 
        
           |  |  | 351 |             if (newAnswer[key] !== oldAnswer[key]) {
 | 
        
           |  |  | 352 |                 isInteracted = true;
 | 
        
           |  |  | 353 |             }
 | 
        
           |  |  | 354 |         });
 | 
        
           |  |  | 355 |   | 
        
           |  |  | 356 |         return isInteracted;
 | 
        
           |  |  | 357 |     };
 | 
        
           |  |  | 358 |   | 
        
           |  |  | 359 |     /**
 | 
        
           |  |  | 360 |      * Handles the start of dragging an item.
 | 
        
           |  |  | 361 |      *
 | 
        
           |  |  | 362 |      * @param {Event} e the touch start or mouse down event.
 | 
        
           |  |  | 363 |      */
 | 
        
           |  |  | 364 |     DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {
 | 
        
           |  |  | 365 |         var thisQ = this,
 | 
        
           |  |  | 366 |             drag = $(e.target).closest('.draghome'),
 | 
        
           |  |  | 367 |             currentIndex = this.calculateZIndex(),
 | 
        
           |  |  | 368 |             newIndex = currentIndex + 2;
 | 
        
           |  |  | 369 |   | 
        
           |  |  | 370 |         var info = dragDrop.prepare(e);
 | 
        
           |  |  | 371 |         if (!info.start || drag.hasClass('beingdragged')) {
 | 
        
           |  |  | 372 |             return;
 | 
        
           |  |  | 373 |         }
 | 
        
           |  |  | 374 |   | 
        
           |  |  | 375 |         drag.addClass('beingdragged').css('transform', '').css('z-index', newIndex);
 | 
        
           |  |  | 376 |         var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
 | 
        
           |  |  | 377 |         if (currentPlace !== null) {
 | 
        
           |  |  | 378 |             this.setInputValue(currentPlace, 0);
 | 
        
           |  |  | 379 |             drag.removeClass('inplace' + currentPlace);
 | 
        
           |  |  | 380 |             var hiddenDrop = thisQ.getDrop(drag, currentPlace);
 | 
        
           |  |  | 381 |             if (hiddenDrop.length) {
 | 
        
           |  |  | 382 |                 hiddenDrop.addClass('active');
 | 
        
           |  |  | 383 |                 drag.offset(hiddenDrop.offset());
 | 
        
           |  |  | 384 |             }
 | 
        
           |  |  | 385 |         } else {
 | 
        
           |  |  | 386 |             var hiddenDrag = thisQ.getDragClone(drag);
 | 
        
           |  |  | 387 |             if (hiddenDrag.length) {
 | 
        
           |  |  | 388 |                 if (drag.hasClass('infinite')) {
 | 
        
           |  |  | 389 |                     var noOfDrags = this.noOfDropsInGroup(thisQ.getGroup(drag));
 | 
        
           |  |  | 390 |                     var cloneDrags = this.getInfiniteDragClones(drag, false);
 | 
        
           |  |  | 391 |                     if (cloneDrags.length < noOfDrags) {
 | 
        
           |  |  | 392 |                         var cloneDrag = drag.clone();
 | 
        
           |  |  | 393 |                         cloneDrag.removeClass('beingdragged');
 | 
        
           |  |  | 394 |                         cloneDrag.removeAttr('tabindex');
 | 
        
           |  |  | 395 |                         hiddenDrag.after(cloneDrag);
 | 
        
           |  |  | 396 |                         questionManager.addEventHandlersToDrag(cloneDrag);
 | 
        
           |  |  | 397 |                         drag.offset(cloneDrag.offset());
 | 
        
           |  |  | 398 |                     } else {
 | 
        
           |  |  | 399 |                         hiddenDrag.addClass('active');
 | 
        
           |  |  | 400 |                         drag.offset(hiddenDrag.offset());
 | 
        
           |  |  | 401 |                     }
 | 
        
           |  |  | 402 |                 } else {
 | 
        
           |  |  | 403 |                     hiddenDrag.addClass('active');
 | 
        
           |  |  | 404 |                     drag.offset(hiddenDrag.offset());
 | 
        
           |  |  | 405 |                 }
 | 
        
           |  |  | 406 |             }
 | 
        
           |  |  | 407 |         }
 | 
        
           |  |  | 408 |   | 
        
           |  |  | 409 |         dragDrop.start(e, drag, function(x, y, drag) {
 | 
        
           |  |  | 410 |             thisQ.dragMove(x, y, drag);
 | 
        
           |  |  | 411 |         }, function(x, y, drag) {
 | 
        
           |  |  | 412 |             thisQ.dragEnd(x, y, drag);
 | 
        
           |  |  | 413 |         });
 | 
        
           |  |  | 414 |     };
 | 
        
           |  |  | 415 |   | 
        
           |  |  | 416 |     /**
 | 
        
           |  |  | 417 |      * Called whenever the currently dragged items moves.
 | 
        
           |  |  | 418 |      *
 | 
        
           |  |  | 419 |      * @param {Number} pageX the x position.
 | 
        
           |  |  | 420 |      * @param {Number} pageY the y position.
 | 
        
           |  |  | 421 |      * @param {jQuery} drag the item being moved.
 | 
        
           |  |  | 422 |      */
 | 
        
           |  |  | 423 |     DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {
 | 
        
           |  |  | 424 |         var thisQ = this,
 | 
        
           |  |  | 425 |             highlighted = false;
 | 
        
           |  |  | 426 |         this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {
 | 
        
           |  |  | 427 |             var drop = $(dropNode);
 | 
        
           |  |  | 428 |             if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted) {
 | 
        
           |  |  | 429 |                 highlighted = true;
 | 
        
           |  |  | 430 |                 drop.addClass('valid-drag-over-drop');
 | 
        
           |  |  | 431 |             } else {
 | 
        
           |  |  | 432 |                 drop.removeClass('valid-drag-over-drop');
 | 
        
           |  |  | 433 |             }
 | 
        
           |  |  | 434 |         });
 | 
        
           |  |  | 435 |         this.getRoot().find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {
 | 
        
           |  |  | 436 |             var drop = $(dropNode);
 | 
        
           |  |  | 437 |             if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted && !thisQ.isDragSameAsDrop(drag, drop)) {
 | 
        
           |  |  | 438 |                 highlighted = true;
 | 
        
           |  |  | 439 |                 drop.addClass('valid-drag-over-drop');
 | 
        
           |  |  | 440 |             } else {
 | 
        
           |  |  | 441 |                 drop.removeClass('valid-drag-over-drop');
 | 
        
           |  |  | 442 |             }
 | 
        
           |  |  | 443 |         });
 | 
        
           |  |  | 444 |     };
 | 
        
           |  |  | 445 |   | 
        
           |  |  | 446 |     /**
 | 
        
           |  |  | 447 |      * Called when user drops a drag item.
 | 
        
           |  |  | 448 |      *
 | 
        
           |  |  | 449 |      * @param {Number} pageX the x position.
 | 
        
           |  |  | 450 |      * @param {Number} pageY the y position.
 | 
        
           |  |  | 451 |      * @param {jQuery} drag the item being moved.
 | 
        
           |  |  | 452 |      */
 | 
        
           |  |  | 453 |     DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {
 | 
        
           |  |  | 454 |         var thisQ = this,
 | 
        
           |  |  | 455 |             root = this.getRoot(),
 | 
        
           |  |  | 456 |             placed = false;
 | 
        
           |  |  | 457 |   | 
        
           |  |  | 458 |         // Looking for drag that was dropped on a dropzone.
 | 
        
           |  |  | 459 |         root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {
 | 
        
           |  |  | 460 |             var drop = $(dropNode);
 | 
        
           |  |  | 461 |             if (!thisQ.isPointInDrop(pageX, pageY, drop)) {
 | 
        
           |  |  | 462 |                 // Not this drop.
 | 
        
           |  |  | 463 |                 return true;
 | 
        
           |  |  | 464 |             }
 | 
        
           |  |  | 465 |   | 
        
           |  |  | 466 |             // Now put this drag into the drop.
 | 
        
           |  |  | 467 |             drop.removeClass('valid-drag-over-drop');
 | 
        
           |  |  | 468 |             thisQ.sendDragToDrop(drag, drop);
 | 
        
           |  |  | 469 |             placed = true;
 | 
        
           |  |  | 470 |             return false; // Stop the each() here.
 | 
        
           |  |  | 471 |         });
 | 
        
           |  |  | 472 |   | 
        
           |  |  | 473 |         if (!placed) {
 | 
        
           |  |  | 474 |             // Looking for drag that was dropped on a placed drag.
 | 
        
           |  |  | 475 |             root.find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) {
 | 
        
           |  |  | 476 |                 var placedDrag = $(placedNode);
 | 
        
           |  |  | 477 |                 if (!thisQ.isPointInDrop(pageX, pageY, placedDrag) || thisQ.isDragSameAsDrop(drag, placedDrag)) {
 | 
        
           |  |  | 478 |                     // Not this placed drag.
 | 
        
           |  |  | 479 |                     return true;
 | 
        
           |  |  | 480 |                 }
 | 
        
           |  |  | 481 |   | 
        
           |  |  | 482 |                 // Now put this drag into the drop.
 | 
        
           |  |  | 483 |                 placedDrag.removeClass('valid-drag-over-drop');
 | 
        
           |  |  | 484 |                 var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace');
 | 
        
           |  |  | 485 |                 var drop = thisQ.getDrop(drag, currentPlace);
 | 
        
           |  |  | 486 |                 thisQ.sendDragToDrop(drag, drop);
 | 
        
           |  |  | 487 |                 placed = true;
 | 
        
           |  |  | 488 |                 return false; // Stop the each() here.
 | 
        
           |  |  | 489 |             });
 | 
        
           |  |  | 490 |         }
 | 
        
           |  |  | 491 |   | 
        
           |  |  | 492 |         if (!placed) {
 | 
        
           |  |  | 493 |             this.sendDragHome(drag);
 | 
        
           |  |  | 494 |         }
 | 
        
           |  |  | 495 |     };
 | 
        
           |  |  | 496 |   | 
        
           |  |  | 497 |     /**
 | 
        
           |  |  | 498 |      * Animate a drag item into a given place (or back home).
 | 
        
           |  |  | 499 |      *
 | 
        
           |  |  | 500 |      * @param {jQuery|null} drag the item to place. If null, clear the place.
 | 
        
           |  |  | 501 |      * @param {jQuery} drop the place to put it.
 | 
        
           |  |  | 502 |      */
 | 
        
           |  |  | 503 |     DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {
 | 
        
           |  |  | 504 |         // Is there already a drag in this drop? if so, evict it.
 | 
        
           |  |  | 505 |         var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));
 | 
        
           |  |  | 506 |         if (oldDrag.length !== 0) {
 | 
        
           |  |  | 507 |             oldDrag.addClass('beingdragged');
 | 
        
           |  |  | 508 |             oldDrag.offset(oldDrag.offset());
 | 
        
           |  |  | 509 |             var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');
 | 
        
           |  |  | 510 |             var hiddenDrop = this.getDrop(oldDrag, currentPlace);
 | 
        
           |  |  | 511 |             hiddenDrop.addClass('active');
 | 
        
           |  |  | 512 |             this.sendDragHome(oldDrag);
 | 
        
           |  |  | 513 |         }
 | 
        
           |  |  | 514 |   | 
        
           |  |  | 515 |         if (drag.length === 0) {
 | 
        
           |  |  | 516 |             this.setInputValue(this.getPlace(drop), 0);
 | 
        
           |  |  | 517 |             if (drop.data('isfocus')) {
 | 
        
           |  |  | 518 |                 drop.focus();
 | 
        
           |  |  | 519 |             }
 | 
        
           |  |  | 520 |         } else {
 | 
        
           |  |  | 521 |             this.setInputValue(this.getPlace(drop), this.getChoice(drag));
 | 
        
           |  |  | 522 |             drag.removeClass('unplaced')
 | 
        
           |  |  | 523 |                 .addClass('placed inplace' + this.getPlace(drop));
 | 
        
           |  |  | 524 |             drag.attr('tabindex', 0);
 | 
        
           |  |  | 525 |             this.animateTo(drag, drop);
 | 
        
           |  |  | 526 |         }
 | 
        
           |  |  | 527 |     };
 | 
        
           |  |  | 528 |   | 
        
           |  |  | 529 |     /**
 | 
        
           |  |  | 530 |      * Animate a drag back to its home.
 | 
        
           |  |  | 531 |      *
 | 
        
           |  |  | 532 |      * @param {jQuery} drag the item being moved.
 | 
        
           |  |  | 533 |      */
 | 
        
           |  |  | 534 |     DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {
 | 
        
           |  |  | 535 |         var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
 | 
        
           |  |  | 536 |         if (currentPlace !== null) {
 | 
        
           |  |  | 537 |             drag.removeClass('inplace' + currentPlace);
 | 
        
           |  |  | 538 |         }
 | 
        
           |  |  | 539 |         drag.data('unplaced', true);
 | 
        
           |  |  | 540 |   | 
        
           |  |  | 541 |         this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));
 | 
        
           |  |  | 542 |     };
 | 
        
           |  |  | 543 |   | 
        
           |  |  | 544 |     /**
 | 
        
           |  |  | 545 |      * Handles keyboard events on drops.
 | 
        
           |  |  | 546 |      *
 | 
        
           |  |  | 547 |      * Drops are focusable. Once focused, right/down/space switches to the next choice, and
 | 
        
           |  |  | 548 |      * left/up switches to the previous. Escape clear.
 | 
        
           |  |  | 549 |      *
 | 
        
           |  |  | 550 |      * @param {KeyboardEvent} e
 | 
        
           |  |  | 551 |      */
 | 
        
           |  |  | 552 |     DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {
 | 
        
           |  |  | 553 |         var drop = $(e.target).closest('.dropzone');
 | 
        
           |  |  | 554 |         if (drop.length === 0) {
 | 
        
           |  |  | 555 |             var placedDrag = $(e.target);
 | 
        
           |  |  | 556 |             var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');
 | 
        
           |  |  | 557 |             if (currentPlace !== null) {
 | 
        
           |  |  | 558 |                 drop = this.getDrop(placedDrag, currentPlace);
 | 
        
           |  |  | 559 |             }
 | 
        
           |  |  | 560 |         }
 | 
        
           |  |  | 561 |         var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),
 | 
        
           |  |  | 562 |             nextDrag = $();
 | 
        
           |  |  | 563 |   | 
        
           |  |  | 564 |         switch (e.keyCode) {
 | 
        
           |  |  | 565 |             case keys.space:
 | 
        
           |  |  | 566 |             case keys.arrowRight:
 | 
        
           |  |  | 567 |             case keys.arrowDown:
 | 
        
           |  |  | 568 |                 nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);
 | 
        
           |  |  | 569 |                 break;
 | 
        
           |  |  | 570 |   | 
        
           |  |  | 571 |             case keys.arrowLeft:
 | 
        
           |  |  | 572 |             case keys.arrowUp:
 | 
        
           |  |  | 573 |                 nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);
 | 
        
           |  |  | 574 |                 break;
 | 
        
           |  |  | 575 |   | 
        
           |  |  | 576 |             case keys.escape:
 | 
        
           |  |  | 577 |                 questionManager.isKeyboardNavigation = false;
 | 
        
           |  |  | 578 |                 break;
 | 
        
           |  |  | 579 |   | 
        
           |  |  | 580 |             default:
 | 
        
           |  |  | 581 |                 questionManager.isKeyboardNavigation = false;
 | 
        
           |  |  | 582 |                 return; // To avoid the preventDefault below.
 | 
        
           |  |  | 583 |         }
 | 
        
           |  |  | 584 |   | 
        
           |  |  | 585 |         if (nextDrag.length) {
 | 
        
           |  |  | 586 |             nextDrag.data('isfocus', true);
 | 
        
           |  |  | 587 |             nextDrag.addClass('beingdragged');
 | 
        
           |  |  | 588 |             var hiddenDrag = this.getDragClone(nextDrag);
 | 
        
           |  |  | 589 |             if (hiddenDrag.length) {
 | 
        
           |  |  | 590 |                 if (nextDrag.hasClass('infinite')) {
 | 
        
           |  |  | 591 |                     var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));
 | 
        
           |  |  | 592 |                     var cloneDrags = this.getInfiniteDragClones(nextDrag, false);
 | 
        
           |  |  | 593 |                     if (cloneDrags.length < noOfDrags) {
 | 
        
           |  |  | 594 |                         var cloneDrag = nextDrag.clone();
 | 
        
           |  |  | 595 |                         cloneDrag.removeClass('beingdragged');
 | 
        
           |  |  | 596 |                         cloneDrag.removeAttr('tabindex');
 | 
        
           |  |  | 597 |                         hiddenDrag.after(cloneDrag);
 | 
        
           |  |  | 598 |                         questionManager.addEventHandlersToDrag(cloneDrag);
 | 
        
           |  |  | 599 |                         nextDrag.offset(cloneDrag.offset());
 | 
        
           |  |  | 600 |                     } else {
 | 
        
           |  |  | 601 |                         hiddenDrag.addClass('active');
 | 
        
           |  |  | 602 |                         nextDrag.offset(hiddenDrag.offset());
 | 
        
           |  |  | 603 |                     }
 | 
        
           |  |  | 604 |                 } else {
 | 
        
           |  |  | 605 |                     hiddenDrag.addClass('active');
 | 
        
           |  |  | 606 |                     nextDrag.offset(hiddenDrag.offset());
 | 
        
           |  |  | 607 |                 }
 | 
        
           |  |  | 608 |             }
 | 
        
           |  |  | 609 |         } else {
 | 
        
           |  |  | 610 |             drop.data('isfocus', true);
 | 
        
           |  |  | 611 |         }
 | 
        
           |  |  | 612 |   | 
        
           |  |  | 613 |         e.preventDefault();
 | 
        
           |  |  | 614 |         this.sendDragToDrop(nextDrag, drop);
 | 
        
           |  |  | 615 |     };
 | 
        
           |  |  | 616 |   | 
        
           |  |  | 617 |     /**
 | 
        
           |  |  | 618 |      * Choose the next drag in a group.
 | 
        
           |  |  | 619 |      *
 | 
        
           |  |  | 620 |      * @param {int} group which group.
 | 
        
           |  |  | 621 |      * @param {jQuery} drag current choice (empty jQuery if there isn't one).
 | 
        
           |  |  | 622 |      * @return {jQuery} the next drag in that group, or null if there wasn't one.
 | 
        
           |  |  | 623 |      */
 | 
        
           |  |  | 624 |     DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {
 | 
        
           |  |  | 625 |         var choice,
 | 
        
           |  |  | 626 |             numChoices = this.noOfChoicesInGroup(group);
 | 
        
           |  |  | 627 |   | 
        
           |  |  | 628 |         if (drag.length === 0) {
 | 
        
           |  |  | 629 |             choice = 1; // Was empty, so we want to select the first choice.
 | 
        
           |  |  | 630 |         } else {
 | 
        
           |  |  | 631 |             choice = this.getChoice(drag) + 1;
 | 
        
           |  |  | 632 |         }
 | 
        
           |  |  | 633 |   | 
        
           |  |  | 634 |         var next = this.getUnplacedChoice(group, choice);
 | 
        
           |  |  | 635 |         while (next.length === 0 && choice < numChoices) {
 | 
        
           |  |  | 636 |             choice++;
 | 
        
           |  |  | 637 |             next = this.getUnplacedChoice(group, choice);
 | 
        
           |  |  | 638 |         }
 | 
        
           |  |  | 639 |   | 
        
           |  |  | 640 |         return next;
 | 
        
           |  |  | 641 |     };
 | 
        
           |  |  | 642 |   | 
        
           |  |  | 643 |     /**
 | 
        
           |  |  | 644 |      * Choose the previous drag in a group.
 | 
        
           |  |  | 645 |      *
 | 
        
           |  |  | 646 |      * @param {int} group which group.
 | 
        
           |  |  | 647 |      * @param {jQuery} drag current choice (empty jQuery if there isn't one).
 | 
        
           |  |  | 648 |      * @return {jQuery} the next drag in that group, or null if there wasn't one.
 | 
        
           |  |  | 649 |      */
 | 
        
           |  |  | 650 |     DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {
 | 
        
           |  |  | 651 |         var choice;
 | 
        
           |  |  | 652 |   | 
        
           |  |  | 653 |         if (drag.length === 0) {
 | 
        
           |  |  | 654 |             choice = this.noOfChoicesInGroup(group);
 | 
        
           |  |  | 655 |         } else {
 | 
        
           |  |  | 656 |             choice = this.getChoice(drag) - 1;
 | 
        
           |  |  | 657 |         }
 | 
        
           |  |  | 658 |   | 
        
           |  |  | 659 |         var previous = this.getUnplacedChoice(group, choice);
 | 
        
           |  |  | 660 |         while (previous.length === 0 && choice > 1) {
 | 
        
           |  |  | 661 |             choice--;
 | 
        
           |  |  | 662 |             previous = this.getUnplacedChoice(group, choice);
 | 
        
           |  |  | 663 |         }
 | 
        
           |  |  | 664 |   | 
        
           |  |  | 665 |         // Does this choice exist?
 | 
        
           |  |  | 666 |         return previous;
 | 
        
           |  |  | 667 |     };
 | 
        
           |  |  | 668 |   | 
        
           |  |  | 669 |     /**
 | 
        
           |  |  | 670 |      * Animate an object to the given destination.
 | 
        
           |  |  | 671 |      *
 | 
        
           |  |  | 672 |      * @param {jQuery} drag the element to be animated.
 | 
        
           |  |  | 673 |      * @param {jQuery} target element marking the place to move it to.
 | 
        
           |  |  | 674 |      */
 | 
        
           |  |  | 675 |     DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {
 | 
        
           |  |  | 676 |         var currentPos = drag.offset(),
 | 
        
           |  |  | 677 |             targetPos = target.offset(),
 | 
        
           |  |  | 678 |             thisQ = this;
 | 
        
           |  |  | 679 |   | 
        
           |  |  | 680 |         M.util.js_pending('qtype_ddimageortext-animate-' + thisQ.containerId);
 | 
        
           |  |  | 681 |         // Animate works in terms of CSS position, whereas locating an object
 | 
        
           |  |  | 682 |         // on the page works best with jQuery offset() function. So, to get
 | 
        
           |  |  | 683 |         // the right target position, we work out the required change in
 | 
        
           |  |  | 684 |         // offset() and then add that to the current CSS position.
 | 
        
           |  |  | 685 |         drag.animate(
 | 
        
           |  |  | 686 |             {
 | 
        
           |  |  | 687 |                 left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,
 | 
        
           |  |  | 688 |                 top: parseInt(drag.css('top')) + targetPos.top - currentPos.top
 | 
        
           |  |  | 689 |             },
 | 
        
           |  |  | 690 |             {
 | 
        
           |  |  | 691 |                 duration: 'fast',
 | 
        
           |  |  | 692 |                 done: function() {
 | 
        
           |  |  | 693 |                     $('body').trigger('qtype_ddimageortext-dragmoved', [drag, target, thisQ]);
 | 
        
           |  |  | 694 |                     M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId);
 | 
        
           |  |  | 695 |                 }
 | 
        
           |  |  | 696 |             }
 | 
        
           |  |  | 697 |         );
 | 
        
           |  |  | 698 |     };
 | 
        
           |  |  | 699 |   | 
        
           |  |  | 700 |     /**
 | 
        
           |  |  | 701 |      * Detect if a point is inside a given DOM node.
 | 
        
           |  |  | 702 |      *
 | 
        
           |  |  | 703 |      * @param {Number} pageX the x position.
 | 
        
           |  |  | 704 |      * @param {Number} pageY the y position.
 | 
        
           |  |  | 705 |      * @param {jQuery} drop the node to check (typically a drop).
 | 
        
           |  |  | 706 |      * @return {boolean} whether the point is inside the node.
 | 
        
           |  |  | 707 |      */
 | 
        
           |  |  | 708 |     DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {
 | 
        
           |  |  | 709 |         var position = drop.offset();
 | 
        
           |  |  | 710 |         if (drop.hasClass('draghome')) {
 | 
        
           |  |  | 711 |             return pageX >= position.left && pageX < position.left + drop.outerWidth()
 | 
        
           |  |  | 712 |                 && pageY >= position.top && pageY < position.top + drop.outerHeight();
 | 
        
           |  |  | 713 |         }
 | 
        
           |  |  | 714 |         return pageX >= position.left && pageX < position.left + drop.width()
 | 
        
           |  |  | 715 |             && pageY >= position.top && pageY < position.top + drop.height();
 | 
        
           |  |  | 716 |     };
 | 
        
           |  |  | 717 |   | 
        
           |  |  | 718 |     /**
 | 
        
           |  |  | 719 |      * Set the value of the hidden input for a place, to record what is currently there.
 | 
        
           |  |  | 720 |      *
 | 
        
           |  |  | 721 |      * @param {int} place which place to set the input value for.
 | 
        
           |  |  | 722 |      * @param {int} choice the value to set.
 | 
        
           |  |  | 723 |      */
 | 
        
           |  |  | 724 |     DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {
 | 
        
           |  |  | 725 |         this.getRoot().find('input.placeinput.place' + place).val(choice);
 | 
        
           |  |  | 726 |     };
 | 
        
           |  |  | 727 |   | 
        
           |  |  | 728 |     /**
 | 
        
           |  |  | 729 |      * Get the outer div for this question.
 | 
        
           |  |  | 730 |      *
 | 
        
           |  |  | 731 |      * @returns {jQuery} containing that div.
 | 
        
           |  |  | 732 |      */
 | 
        
           |  |  | 733 |     DragDropOntoImageQuestion.prototype.getRoot = function() {
 | 
        
           |  |  | 734 |         return $(document.getElementById(this.containerId));
 | 
        
           |  |  | 735 |     };
 | 
        
           |  |  | 736 |   | 
        
           |  |  | 737 |     /**
 | 
        
           |  |  | 738 |      * Get the img that is the background image.
 | 
        
           |  |  | 739 |      * @returns {jQuery} containing that img.
 | 
        
           |  |  | 740 |      */
 | 
        
           |  |  | 741 |     DragDropOntoImageQuestion.prototype.bgImage = function() {
 | 
        
           |  |  | 742 |         return this.getRoot().find('img.dropbackground');
 | 
        
           |  |  | 743 |     };
 | 
        
           |  |  | 744 |   | 
        
           |  |  | 745 |     /**
 | 
        
           |  |  | 746 |      * Get drag home for a given choice.
 | 
        
           |  |  | 747 |      *
 | 
        
           |  |  | 748 |      * @param {int} group the group.
 | 
        
           |  |  | 749 |      * @param {int} choice the choice number.
 | 
        
           |  |  | 750 |      * @returns {jQuery} containing that div.
 | 
        
           |  |  | 751 |      */
 | 
        
           |  |  | 752 |     DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {
 | 
        
           |  |  | 753 |         if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {
 | 
        
           |  |  | 754 |             return this.getRoot().find('.dragitemgroup' + group +
 | 
        
           |  |  | 755 |                 ' .draghome.infinite' +
 | 
        
           |  |  | 756 |                 '.choice' + choice +
 | 
        
           |  |  | 757 |                 '.group' + group);
 | 
        
           |  |  | 758 |         }
 | 
        
           |  |  | 759 |         return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);
 | 
        
           |  |  | 760 |     };
 | 
        
           |  |  | 761 |   | 
        
           |  |  | 762 |     /**
 | 
        
           |  |  | 763 |      * Get an unplaced choice for a particular group.
 | 
        
           |  |  | 764 |      *
 | 
        
           |  |  | 765 |      * @param {int} group the group.
 | 
        
           |  |  | 766 |      * @param {int} choice the choice number.
 | 
        
           |  |  | 767 |      * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.
 | 
        
           |  |  | 768 |      */
 | 
        
           |  |  | 769 |     DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {
 | 
        
           |  |  | 770 |         return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);
 | 
        
           |  |  | 771 |     };
 | 
        
           |  |  | 772 |   | 
        
           |  |  | 773 |     /**
 | 
        
           |  |  | 774 |      * Get the drag that is currently in a given place.
 | 
        
           |  |  | 775 |      *
 | 
        
           |  |  | 776 |      * @param {int} place the place number.
 | 
        
           |  |  | 777 |      * @return {jQuery} the current drag (or an empty jQuery if none).
 | 
        
           |  |  | 778 |      */
 | 
        
           |  |  | 779 |     DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {
 | 
        
           |  |  | 780 |         return this.getRoot().find('.ddarea .draghome.inplace' + place);
 | 
        
           |  |  | 781 |     };
 | 
        
           |  |  | 782 |   | 
        
           |  |  | 783 |     /**
 | 
        
           |  |  | 784 |      * Return the number of blanks in a given group.
 | 
        
           |  |  | 785 |      *
 | 
        
           |  |  | 786 |      * @param {int} group the group number.
 | 
        
           |  |  | 787 |      * @returns {int} the number of drops.
 | 
        
           |  |  | 788 |      */
 | 
        
           |  |  | 789 |     DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {
 | 
        
           |  |  | 790 |         return this.getRoot().find('.dropzone.group' + group).length;
 | 
        
           |  |  | 791 |     };
 | 
        
           |  |  | 792 |   | 
        
           |  |  | 793 |     /**
 | 
        
           |  |  | 794 |      * Return the number of choices in a given group.
 | 
        
           |  |  | 795 |      *
 | 
        
           |  |  | 796 |      * @param {int} group the group number.
 | 
        
           |  |  | 797 |      * @returns {int} the number of choices.
 | 
        
           |  |  | 798 |      */
 | 
        
           |  |  | 799 |     DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {
 | 
        
           |  |  | 800 |         return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;
 | 
        
           |  |  | 801 |     };
 | 
        
           |  |  | 802 |   | 
        
           |  |  | 803 |     /**
 | 
        
           |  |  | 804 |      * Return the number at the end of the CSS class name with the given prefix.
 | 
        
           |  |  | 805 |      *
 | 
        
           |  |  | 806 |      * @param {jQuery} node
 | 
        
           |  |  | 807 |      * @param {String} prefix name prefix
 | 
        
           |  |  | 808 |      * @returns {Number|null} the suffix if found, else null.
 | 
        
           |  |  | 809 |      */
 | 
        
           |  |  | 810 |     DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {
 | 
        
           |  |  | 811 |         var classes = node.attr('class');
 | 
        
           |  |  | 812 |         if (classes !== '') {
 | 
        
           |  |  | 813 |             var classesArr = classes.split(' ');
 | 
        
           |  |  | 814 |             for (var index = 0; index < classesArr.length; index++) {
 | 
        
           |  |  | 815 |                 var patt1 = new RegExp('^' + prefix + '([0-9])+$');
 | 
        
           |  |  | 816 |                 if (patt1.test(classesArr[index])) {
 | 
        
           |  |  | 817 |                     var patt2 = new RegExp('([0-9])+$');
 | 
        
           |  |  | 818 |                     var match = patt2.exec(classesArr[index]);
 | 
        
           |  |  | 819 |                     return Number(match[0]);
 | 
        
           |  |  | 820 |                 }
 | 
        
           |  |  | 821 |             }
 | 
        
           |  |  | 822 |         }
 | 
        
           |  |  | 823 |         return null;
 | 
        
           |  |  | 824 |     };
 | 
        
           |  |  | 825 |   | 
        
           |  |  | 826 |     /**
 | 
        
           |  |  | 827 |      * Get the choice number of a drag.
 | 
        
           |  |  | 828 |      *
 | 
        
           |  |  | 829 |      * @param {jQuery} drag the drag.
 | 
        
           |  |  | 830 |      * @returns {Number} the choice number.
 | 
        
           |  |  | 831 |      */
 | 
        
           |  |  | 832 |     DragDropOntoImageQuestion.prototype.getChoice = function(drag) {
 | 
        
           |  |  | 833 |         return this.getClassnameNumericSuffix(drag, 'choice');
 | 
        
           |  |  | 834 |     };
 | 
        
           |  |  | 835 |   | 
        
           |  |  | 836 |     /**
 | 
        
           |  |  | 837 |      * Given a DOM node that is significant to this question
 | 
        
           |  |  | 838 |      * (drag, drop, ...) get the group it belongs to.
 | 
        
           |  |  | 839 |      *
 | 
        
           |  |  | 840 |      * @param {jQuery} node a DOM node.
 | 
        
           |  |  | 841 |      * @returns {Number} the group it belongs to.
 | 
        
           |  |  | 842 |      */
 | 
        
           |  |  | 843 |     DragDropOntoImageQuestion.prototype.getGroup = function(node) {
 | 
        
           |  |  | 844 |         return this.getClassnameNumericSuffix(node, 'group');
 | 
        
           |  |  | 845 |     };
 | 
        
           |  |  | 846 |   | 
        
           |  |  | 847 |     /**
 | 
        
           |  |  | 848 |      * Get the place number of a drop, or its corresponding hidden input.
 | 
        
           |  |  | 849 |      *
 | 
        
           |  |  | 850 |      * @param {jQuery} node the DOM node.
 | 
        
           |  |  | 851 |      * @returns {Number} the place number.
 | 
        
           |  |  | 852 |      */
 | 
        
           |  |  | 853 |     DragDropOntoImageQuestion.prototype.getPlace = function(node) {
 | 
        
           |  |  | 854 |         return this.getClassnameNumericSuffix(node, 'place');
 | 
        
           |  |  | 855 |     };
 | 
        
           |  |  | 856 |   | 
        
           |  |  | 857 |     /**
 | 
        
           |  |  | 858 |      * Get drag clone for a given drag.
 | 
        
           |  |  | 859 |      *
 | 
        
           |  |  | 860 |      * @param {jQuery} drag the drag.
 | 
        
           |  |  | 861 |      * @returns {jQuery} the drag's clone.
 | 
        
           |  |  | 862 |      */
 | 
        
           |  |  | 863 |     DragDropOntoImageQuestion.prototype.getDragClone = function(drag) {
 | 
        
           |  |  | 864 |         return this.getRoot().find('.dragitemgroup' +
 | 
        
           |  |  | 865 |             this.getGroup(drag) +
 | 
        
           |  |  | 866 |             ' .draghome' +
 | 
        
           |  |  | 867 |             '.choice' + this.getChoice(drag) +
 | 
        
           |  |  | 868 |             '.group' + this.getGroup(drag) +
 | 
        
           |  |  | 869 |             '.dragplaceholder');
 | 
        
           |  |  | 870 |     };
 | 
        
           |  |  | 871 |   | 
        
           |  |  | 872 |     /**
 | 
        
           |  |  | 873 |      * Get infinite drag clones for given drag.
 | 
        
           |  |  | 874 |      *
 | 
        
           |  |  | 875 |      * @param {jQuery} drag the drag.
 | 
        
           |  |  | 876 |      * @param {Boolean} inHome in the home area or not.
 | 
        
           |  |  | 877 |      * @returns {jQuery} the drag's clones.
 | 
        
           |  |  | 878 |      */
 | 
        
           |  |  | 879 |     DragDropOntoImageQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {
 | 
        
           |  |  | 880 |         if (inHome) {
 | 
        
           |  |  | 881 |             return this.getRoot().find('.dragitemgroup' +
 | 
        
           |  |  | 882 |                 this.getGroup(drag) +
 | 
        
           |  |  | 883 |                 ' .draghome' +
 | 
        
           |  |  | 884 |                 '.choice' + this.getChoice(drag) +
 | 
        
           |  |  | 885 |                 '.group' + this.getGroup(drag) +
 | 
        
           |  |  | 886 |                 '.infinite').not('.dragplaceholder');
 | 
        
           |  |  | 887 |         }
 | 
        
           |  |  | 888 |         return this.getRoot().find('.draghome' +
 | 
        
           |  |  | 889 |             '.choice' + this.getChoice(drag) +
 | 
        
           |  |  | 890 |             '.group' + this.getGroup(drag) +
 | 
        
           |  |  | 891 |             '.infinite').not('.dragplaceholder');
 | 
        
           |  |  | 892 |     };
 | 
        
           |  |  | 893 |   | 
        
           |  |  | 894 |     /**
 | 
        
           |  |  | 895 |      * Get drop for a given drag and place.
 | 
        
           |  |  | 896 |      *
 | 
        
           |  |  | 897 |      * @param {jQuery} drag the drag.
 | 
        
           |  |  | 898 |      * @param {Integer} currentPlace the current place of drag.
 | 
        
           |  |  | 899 |      * @returns {jQuery} the drop's clone.
 | 
        
           |  |  | 900 |      */
 | 
        
           |  |  | 901 |     DragDropOntoImageQuestion.prototype.getDrop = function(drag, currentPlace) {
 | 
        
           |  |  | 902 |         return this.getRoot().find('.dropzone.group' + this.getGroup(drag) + '.place' + currentPlace);
 | 
        
           |  |  | 903 |     };
 | 
        
           |  |  | 904 |   | 
        
           |  |  | 905 |     /**
 | 
        
           |  |  | 906 |      * Handle when the window is resized.
 | 
        
           |  |  | 907 |      */
 | 
        
           |  |  | 908 |     DragDropOntoImageQuestion.prototype.handleResize = function() {
 | 
        
           |  |  | 909 |         var thisQ = this,
 | 
        
           |  |  | 910 |             bgRatio = this.bgRatio();
 | 
        
           |  |  | 911 |         if (this.isPrinting) {
 | 
        
           |  |  | 912 |             bgRatio = 1;
 | 
        
           |  |  | 913 |         }
 | 
        
           |  |  | 914 |   | 
        
           |  |  | 915 |         this.getRoot().find('.ddarea .dropzone').each(function(i, dropNode) {
 | 
        
           |  |  | 916 |             $(dropNode)
 | 
        
           |  |  | 917 |                 .css('left', parseInt($(dropNode).data('originX')) * parseFloat(bgRatio))
 | 
        
           |  |  | 918 |                 .css('top', parseInt($(dropNode).data('originY')) * parseFloat(bgRatio));
 | 
        
           |  |  | 919 |             thisQ.handleElementScale(dropNode, 'left top');
 | 
        
           |  |  | 920 |         });
 | 
        
           |  |  | 921 |   | 
        
           |  |  | 922 |         this.getRoot().find('div.droparea .draghome').not('.beingdragged').each(function(key, drag) {
 | 
        
           |  |  | 923 |             $(drag)
 | 
        
           |  |  | 924 |                 .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio))
 | 
        
           |  |  | 925 |                 .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio));
 | 
        
           |  |  | 926 |             thisQ.handleElementScale(drag, 'left top');
 | 
        
           |  |  | 927 |         });
 | 
        
           |  |  | 928 |     };
 | 
        
           |  |  | 929 |   | 
        
           |  |  | 930 |     /**
 | 
        
           |  |  | 931 |      * Return the background ratio.
 | 
        
           |  |  | 932 |      *
 | 
        
           |  |  | 933 |      * @returns {number} Background ratio.
 | 
        
           |  |  | 934 |      */
 | 
        
           |  |  | 935 |     DragDropOntoImageQuestion.prototype.bgRatio = function() {
 | 
        
           |  |  | 936 |         var bgImg = this.bgImage();
 | 
        
           |  |  | 937 |         var bgImgNaturalWidth = bgImg.get(0).naturalWidth;
 | 
        
           |  |  | 938 |         var bgImgClientWidth = bgImg.width();
 | 
        
           |  |  | 939 |   | 
        
           |  |  | 940 |         return bgImgClientWidth / bgImgNaturalWidth;
 | 
        
           |  |  | 941 |     };
 | 
        
           |  |  | 942 |   | 
        
           |  |  | 943 |     /**
 | 
        
           |  |  | 944 |      * Scale the drag if needed.
 | 
        
           |  |  | 945 |      *
 | 
        
           |  |  | 946 |      * @param {jQuery} element the item to place.
 | 
        
           |  |  | 947 |      * @param {String} type scaling type
 | 
        
           |  |  | 948 |      */
 | 
        
           |  |  | 949 |     DragDropOntoImageQuestion.prototype.handleElementScale = function(element, type) {
 | 
        
           |  |  | 950 |         var bgRatio = parseFloat(this.bgRatio());
 | 
        
           |  |  | 951 |         if (this.isPrinting) {
 | 
        
           |  |  | 952 |             bgRatio = 1;
 | 
        
           |  |  | 953 |         }
 | 
        
           |  |  | 954 |         $(element).css({
 | 
        
           |  |  | 955 |             '-webkit-transform': 'scale(' + bgRatio + ')',
 | 
        
           |  |  | 956 |             '-moz-transform': 'scale(' + bgRatio + ')',
 | 
        
           |  |  | 957 |             '-ms-transform': 'scale(' + bgRatio + ')',
 | 
        
           |  |  | 958 |             '-o-transform': 'scale(' + bgRatio + ')',
 | 
        
           |  |  | 959 |             'transform': 'scale(' + bgRatio + ')',
 | 
        
           |  |  | 960 |             'transform-origin': type
 | 
        
           |  |  | 961 |         });
 | 
        
           |  |  | 962 |     };
 | 
        
           |  |  | 963 |   | 
        
           |  |  | 964 |     /**
 | 
        
           |  |  | 965 |      * Calculate z-index value.
 | 
        
           |  |  | 966 |      *
 | 
        
           |  |  | 967 |      * @returns {number} z-index value
 | 
        
           |  |  | 968 |      */
 | 
        
           |  |  | 969 |     DragDropOntoImageQuestion.prototype.calculateZIndex = function() {
 | 
        
           |  |  | 970 |         var zIndex = 0;
 | 
        
           |  |  | 971 |         this.getRoot().find('.ddarea .dropzone, div.droparea .draghome').each(function(i, dropNode) {
 | 
        
           |  |  | 972 |             dropNode = $(dropNode);
 | 
        
           |  |  | 973 |             // Note that webkit browsers won't return the z-index value from the CSS stylesheet
 | 
        
           |  |  | 974 |             // if the element doesn't have a position specified. Instead it'll return "auto".
 | 
        
           |  |  | 975 |             var itemZIndex = dropNode.css('z-index') ? parseInt(dropNode.css('z-index')) : 0;
 | 
        
           |  |  | 976 |   | 
        
           |  |  | 977 |             if (itemZIndex > zIndex) {
 | 
        
           |  |  | 978 |                 zIndex = itemZIndex;
 | 
        
           |  |  | 979 |             }
 | 
        
           |  |  | 980 |         });
 | 
        
           |  |  | 981 |   | 
        
           |  |  | 982 |         return zIndex;
 | 
        
           |  |  | 983 |     };
 | 
        
           |  |  | 984 |   | 
        
           |  |  | 985 |     /**
 | 
        
           |  |  | 986 |      * Check that the drag is drop to it's clone.
 | 
        
           |  |  | 987 |      *
 | 
        
           |  |  | 988 |      * @param {jQuery} drag The drag.
 | 
        
           |  |  | 989 |      * @param {jQuery} drop The drop.
 | 
        
           |  |  | 990 |      * @returns {boolean}
 | 
        
           |  |  | 991 |      */
 | 
        
           |  |  | 992 |     DragDropOntoImageQuestion.prototype.isDragSameAsDrop = function(drag, drop) {
 | 
        
           |  |  | 993 |         return this.getChoice(drag) === this.getChoice(drop) && this.getGroup(drag) === this.getGroup(drop);
 | 
        
           |  |  | 994 |     };
 | 
        
           |  |  | 995 |   | 
        
           |  |  | 996 |     /**
 | 
        
           |  |  | 997 |      * Singleton object that handles all the DragDropOntoImageQuestions
 | 
        
           |  |  | 998 |      * on the page, and deals with event dispatching.
 | 
        
           |  |  | 999 |      * @type {Object}
 | 
        
           |  |  | 1000 |      */
 | 
        
           |  |  | 1001 |     var questionManager = {
 | 
        
           |  |  | 1002 |   | 
        
           |  |  | 1003 |         /**
 | 
        
           |  |  | 1004 |          * {boolean} ensures that the event handlers are only initialised once per page.
 | 
        
           |  |  | 1005 |          */
 | 
        
           |  |  | 1006 |         eventHandlersInitialised: false,
 | 
        
           |  |  | 1007 |   | 
        
           |  |  | 1008 |         /**
 | 
        
           |  |  | 1009 |          * {Object} ensures that the drag event handlers are only initialised once per question,
 | 
        
           |  |  | 1010 |          * indexed by containerId (id on the .que div).
 | 
        
           |  |  | 1011 |          */
 | 
        
           |  |  | 1012 |         dragEventHandlersInitialised: {},
 | 
        
           |  |  | 1013 |   | 
        
           |  |  | 1014 |         /**
 | 
        
           |  |  | 1015 |          * {boolean} is printing or not.
 | 
        
           |  |  | 1016 |          */
 | 
        
           |  |  | 1017 |         isPrinting: false,
 | 
        
           |  |  | 1018 |   | 
        
           |  |  | 1019 |         /**
 | 
        
           |  |  | 1020 |          * {boolean} is keyboard navigation or not.
 | 
        
           |  |  | 1021 |          */
 | 
        
           |  |  | 1022 |         isKeyboardNavigation: false,
 | 
        
           |  |  | 1023 |   | 
        
           |  |  | 1024 |         /**
 | 
        
           |  |  | 1025 |          * {Object} all the questions on this page, indexed by containerId (id on the .que div).
 | 
        
           |  |  | 1026 |          */
 | 
        
           |  |  | 1027 |         questions: {}, // An object containing all the information about each question on the page.
 | 
        
           |  |  | 1028 |   | 
        
           |  |  | 1029 |         /**
 | 
        
           |  |  | 1030 |          * Initialise one question.
 | 
        
           |  |  | 1031 |          *
 | 
        
           |  |  | 1032 |          * @method
 | 
        
           |  |  | 1033 |          * @param {String} containerId the id of the div.que that contains this question.
 | 
        
           |  |  | 1034 |          * @param {boolean} readOnly whether the question is read-only.
 | 
        
           |  |  | 1035 |          * @param {Array} places data.
 | 
        
           |  |  | 1036 |          */
 | 
        
           |  |  | 1037 |         init: function(containerId, readOnly, places) {
 | 
        
           |  |  | 1038 |             questionManager.questions[containerId] =
 | 
        
           |  |  | 1039 |                 new DragDropOntoImageQuestion(containerId, readOnly, places);
 | 
        
           |  |  | 1040 |             if (!questionManager.eventHandlersInitialised) {
 | 
        
           |  |  | 1041 |                 questionManager.setupEventHandlers();
 | 
        
           |  |  | 1042 |                 questionManager.eventHandlersInitialised = true;
 | 
        
           |  |  | 1043 |             }
 | 
        
           |  |  | 1044 |             if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) {
 | 
        
           |  |  | 1045 |                 questionManager.dragEventHandlersInitialised[containerId] = true;
 | 
        
           |  |  | 1046 |                 // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.
 | 
        
           |  |  | 1047 |                 var questionContainer = document.getElementById(containerId);
 | 
        
           |  |  | 1048 |                 if (questionContainer.classList.contains('ddimageortext') &&
 | 
        
           |  |  | 1049 |                     !questionContainer.classList.contains('qtype_ddimageortext-readonly')) {
 | 
        
           |  |  | 1050 |                     // TODO: Convert all the jQuery selectors and events to native Javascript.
 | 
        
           |  |  | 1051 |                     questionManager.addEventHandlersToDrag($(questionContainer).find('.draghome'));
 | 
        
           |  |  | 1052 |                 }
 | 
        
           |  |  | 1053 |             }
 | 
        
           |  |  | 1054 |         },
 | 
        
           |  |  | 1055 |   | 
        
           |  |  | 1056 |         /**
 | 
        
           |  |  | 1057 |          * Set up the event handlers that make this question type work. (Done once per page.)
 | 
        
           |  |  | 1058 |          */
 | 
        
           |  |  | 1059 |         setupEventHandlers: function() {
 | 
        
           |  |  | 1060 |             $('body')
 | 
        
           |  |  | 1061 |                 .on('keydown',
 | 
        
           |  |  | 1062 |                     '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',
 | 
        
           |  |  | 1063 |                     questionManager.handleKeyPress)
 | 
        
           |  |  | 1064 |                 .on('keydown',
 | 
        
           |  |  | 1065 |                     '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)',
 | 
        
           |  |  | 1066 |                     questionManager.handleKeyPress)
 | 
        
           |  |  | 1067 |                 .on('qtype_ddimageortext-dragmoved', questionManager.handleDragMoved);
 | 
        
           |  |  | 1068 |             $(window).on('resize', function() {
 | 
        
           |  |  | 1069 |                 questionManager.handleWindowResize(false);
 | 
        
           |  |  | 1070 |             });
 | 
        
           |  |  | 1071 |             window.addEventListener('beforeprint', function() {
 | 
        
           |  |  | 1072 |                 questionManager.isPrinting = true;
 | 
        
           |  |  | 1073 |                 questionManager.handleWindowResize(questionManager.isPrinting);
 | 
        
           |  |  | 1074 |             });
 | 
        
           |  |  | 1075 |             window.addEventListener('afterprint', function() {
 | 
        
           |  |  | 1076 |                 questionManager.isPrinting = false;
 | 
        
           |  |  | 1077 |                 questionManager.handleWindowResize(questionManager.isPrinting);
 | 
        
           |  |  | 1078 |             });
 | 
        
           |  |  | 1079 |             setTimeout(function() {
 | 
        
           |  |  | 1080 |                 questionManager.fixLayoutIfThingsMoved();
 | 
        
           |  |  | 1081 |             }, 100);
 | 
        
           |  |  | 1082 |         },
 | 
        
           |  |  | 1083 |   | 
        
           |  |  | 1084 |         /**
 | 
        
           |  |  | 1085 |          * Binding the drag/touch event again for newly created element.
 | 
        
           |  |  | 1086 |          *
 | 
        
           |  |  | 1087 |          * @param {jQuery} element Element to bind the event
 | 
        
           |  |  | 1088 |          */
 | 
        
           |  |  | 1089 |         addEventHandlersToDrag: function(element) {
 | 
        
           |  |  | 1090 |             // Unbind all the mousedown and touchstart events to prevent double binding.
 | 
        
           |  |  | 1091 |             element.unbind('mousedown touchstart');
 | 
        
           |  |  | 1092 |             element.on('mousedown touchstart', questionManager.handleDragStart);
 | 
        
           |  |  | 1093 |         },
 | 
        
           |  |  | 1094 |   | 
        
           |  |  | 1095 |         /**
 | 
        
           |  |  | 1096 |          * Handle mouse down / touch start events on drags.
 | 
        
           |  |  | 1097 |          * @param {Event} e the DOM event.
 | 
        
           |  |  | 1098 |          */
 | 
        
           |  |  | 1099 |         handleDragStart: function(e) {
 | 
        
           |  |  | 1100 |             e.preventDefault();
 | 
        
           |  |  | 1101 |             var question = questionManager.getQuestionForEvent(e);
 | 
        
           |  |  | 1102 |             if (question) {
 | 
        
           |  |  | 1103 |                 question.handleDragStart(e);
 | 
        
           |  |  | 1104 |             }
 | 
        
           |  |  | 1105 |         },
 | 
        
           |  |  | 1106 |   | 
        
           |  |  | 1107 |         /**
 | 
        
           |  |  | 1108 |          * Handle key down / press events on drags.
 | 
        
           |  |  | 1109 |          * @param {KeyboardEvent} e
 | 
        
           |  |  | 1110 |          */
 | 
        
           |  |  | 1111 |         handleKeyPress: function(e) {
 | 
        
           |  |  | 1112 |             if (questionManager.isKeyboardNavigation) {
 | 
        
           |  |  | 1113 |                 return;
 | 
        
           |  |  | 1114 |             }
 | 
        
           |  |  | 1115 |             questionManager.isKeyboardNavigation = true;
 | 
        
           |  |  | 1116 |             var question = questionManager.getQuestionForEvent(e);
 | 
        
           |  |  | 1117 |             if (question) {
 | 
        
           |  |  | 1118 |                 question.handleKeyPress(e);
 | 
        
           |  |  | 1119 |             }
 | 
        
           |  |  | 1120 |         },
 | 
        
           |  |  | 1121 |   | 
        
           |  |  | 1122 |         /**
 | 
        
           |  |  | 1123 |          * Handle when the window is resized.
 | 
        
           |  |  | 1124 |          * @param {boolean} isPrinting
 | 
        
           |  |  | 1125 |          */
 | 
        
           |  |  | 1126 |         handleWindowResize: function(isPrinting) {
 | 
        
           |  |  | 1127 |             for (var containerId in questionManager.questions) {
 | 
        
           |  |  | 1128 |                 if (questionManager.questions.hasOwnProperty(containerId)) {
 | 
        
           |  |  | 1129 |                     questionManager.questions[containerId].isPrinting = isPrinting;
 | 
        
           |  |  | 1130 |                     questionManager.questions[containerId].handleResize();
 | 
        
           |  |  | 1131 |                 }
 | 
        
           |  |  | 1132 |             }
 | 
        
           |  |  | 1133 |         },
 | 
        
           |  |  | 1134 |   | 
        
           |  |  | 1135 |         /**
 | 
        
           |  |  | 1136 |          * Sometimes, despite our best efforts, things change in a way that cannot
 | 
        
           |  |  | 1137 |          * be specifically caught (e.g. dock expanding or collapsing in Boost).
 | 
        
           |  |  | 1138 |          * Therefore, we need to periodically check everything is in the right position.
 | 
        
           |  |  | 1139 |          */
 | 
        
           |  |  | 1140 |         fixLayoutIfThingsMoved: function() {
 | 
        
           |  |  | 1141 |             this.handleWindowResize(questionManager.isPrinting);
 | 
        
           |  |  | 1142 |             // We use setTimeout after finishing work, rather than setInterval,
 | 
        
           |  |  | 1143 |             // in case positioning things is slow. We want 100 ms gap
 | 
        
           |  |  | 1144 |             // between executions, not what setInterval does.
 | 
        
           |  |  | 1145 |             setTimeout(function() {
 | 
        
           |  |  | 1146 |                 questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);
 | 
        
           |  |  | 1147 |             }, 100);
 | 
        
           |  |  | 1148 |         },
 | 
        
           |  |  | 1149 |   | 
        
           |  |  | 1150 |         /**
 | 
        
           |  |  | 1151 |          * Handle when drag moved.
 | 
        
           |  |  | 1152 |          *
 | 
        
           |  |  | 1153 |          * @param {Event} e the event.
 | 
        
           |  |  | 1154 |          * @param {jQuery} drag the drag
 | 
        
           |  |  | 1155 |          * @param {jQuery} target the target
 | 
        
           |  |  | 1156 |          * @param {DragDropOntoImageQuestion} thisQ the question.
 | 
        
           |  |  | 1157 |          */
 | 
        
           |  |  | 1158 |         handleDragMoved: function(e, drag, target, thisQ) {
 | 
        
           |  |  | 1159 |             drag.removeClass('beingdragged').css('z-index', '');
 | 
        
           |  |  | 1160 |             drag.css('top', target.position().top).css('left', target.position().left);
 | 
        
           |  |  | 1161 |             target.after(drag);
 | 
        
           |  |  | 1162 |             target.removeClass('active');
 | 
        
           |  |  | 1163 |             if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {
 | 
        
           |  |  | 1164 |                 drag.removeClass('placed').addClass('unplaced');
 | 
        
           |  |  | 1165 |                 drag.removeAttr('tabindex');
 | 
        
           |  |  | 1166 |                 drag.removeData('unplaced');
 | 
        
           |  |  | 1167 |                 drag.css('top', '')
 | 
        
           |  |  | 1168 |                     .css('left', '')
 | 
        
           |  |  | 1169 |                     .css('transform', '');
 | 
        
           |  |  | 1170 |                 if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {
 | 
        
           |  |  | 1171 |                     thisQ.getInfiniteDragClones(drag, true).first().remove();
 | 
        
           |  |  | 1172 |                 }
 | 
        
           |  |  | 1173 |             } else {
 | 
        
           |  |  | 1174 |                 drag.data('originX', target.data('originX')).data('originY', target.data('originY'));
 | 
        
           |  |  | 1175 |                 thisQ.handleElementScale(drag, 'left top');
 | 
        
           |  |  | 1176 |             }
 | 
        
           |  |  | 1177 |             if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {
 | 
        
           |  |  | 1178 |                 drag.focus();
 | 
        
           |  |  | 1179 |                 drag.removeData('isfocus');
 | 
        
           |  |  | 1180 |             }
 | 
        
           |  |  | 1181 |             if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {
 | 
        
           |  |  | 1182 |                 target.removeData('isfocus');
 | 
        
           |  |  | 1183 |             }
 | 
        
           |  |  | 1184 |             if (questionManager.isKeyboardNavigation) {
 | 
        
           |  |  | 1185 |                 questionManager.isKeyboardNavigation = false;
 | 
        
           |  |  | 1186 |             }
 | 
        
           |  |  | 1187 |             if (thisQ.isQuestionInteracted()) {
 | 
        
           |  |  | 1188 |                 // The user has interacted with the draggable items. We need to mark the form as dirty.
 | 
        
           |  |  | 1189 |                 questionManager.handleFormDirty();
 | 
        
           |  |  | 1190 |                 // Save the new answered value.
 | 
        
           |  |  | 1191 |                 thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();
 | 
        
           |  |  | 1192 |             }
 | 
        
           |  |  | 1193 |         },
 | 
        
           |  |  | 1194 |   | 
        
           |  |  | 1195 |         /**
 | 
        
           |  |  | 1196 |          * Given an event, work out which question it effects.
 | 
        
           |  |  | 1197 |          * @param {Event} e the event.
 | 
        
           |  |  | 1198 |          * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.
 | 
        
           |  |  | 1199 |          */
 | 
        
           |  |  | 1200 |         getQuestionForEvent: function(e) {
 | 
        
           |  |  | 1201 |             var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');
 | 
        
           |  |  | 1202 |             return questionManager.questions[containerId];
 | 
        
           |  |  | 1203 |         },
 | 
        
           |  |  | 1204 |   | 
        
           |  |  | 1205 |         /**
 | 
        
           |  |  | 1206 |          * Handle when the form is dirty.
 | 
        
           |  |  | 1207 |          */
 | 
        
           |  |  | 1208 |         handleFormDirty: function() {
 | 
        
           |  |  | 1209 |             const responseForm = document.getElementById('responseform');
 | 
        
           |  |  | 1210 |             FormChangeChecker.markFormAsDirty(responseForm);
 | 
        
           |  |  | 1211 |         }
 | 
        
           |  |  | 1212 |     };
 | 
        
           |  |  | 1213 |   | 
        
           |  |  | 1214 |     /**
 | 
        
           |  |  | 1215 |      * @alias module:qtype_ddimageortext/question
 | 
        
           |  |  | 1216 |      */
 | 
        
           |  |  | 1217 |     return {
 | 
        
           |  |  | 1218 |         init: questionManager.init
 | 
        
           |  |  | 1219 |     };
 | 
        
           |  |  | 1220 | });
 |