MDL-63297 qtype_ddimageortext: re-implement JavaScript in AMD
[moodle.git] / question / type / ddimageortext / amd / src / question.js
CommitLineData
ebf91776
TH
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 * @package qtype_ddimageortext
21 * @copyright 2018 The Open University
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) {
25
26 "use strict";
27
28 /**
29 * Initialise one drag-drop onto image question.
30 *
31 * @param {String} containerId id of the outer div for this question.
32 * @param {boolean} readOnly whether the question is being displayed read-only.
33 * @param {Array} places Information about the drop places.
34 * @constructor
35 */
36 function DragDropOntoImageQuestion(containerId, readOnly, places) {
37 this.containerId = containerId;
38 M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);
39 this.places = places;
40 this.allImagesLoaded = false;
41 this.imageLoadingTimeoutId = null;
42 if (readOnly) {
43 this.getRoot().addClass('qtype_ddimageortext-readonly');
44 }
45
46 var thisQ = this;
47 this.getNotYetLoadedImages().one('load', function() {
48 thisQ.waitForAllImagesToBeLoaded();
49 });
50 this.waitForAllImagesToBeLoaded();
51 }
52
53 /**
54 * Waits until all images are loaded before calling setupQuestion().
55 *
56 * This function is called from the onLoad of each image, and also polls with
57 * a time-out, because image on-loads are allegedly unreliable.
58 */
59 DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {
60 var thisQ = this;
61
62 // This method may get called multiple times (via image on-loads or timeouts.
63 // If we are already done, don't do it again.
64 if (this.allImagesLoaded) {
65 return;
66 }
67
68 // Clear any current timeout, if set.
69 if (this.imageLoadingTimeoutId !== null) {
70 clearTimeout(this.imageLoadingTimeoutId);
71 }
72
73 // If we have not yet loaded all images, set a timeout to
74 // call ourselves again, since apparently images on-load
75 // events are flakey.
76 if (this.getNotYetLoadedImages().length > 0) {
77 this.imageLoadingTimeoutId = setTimeout(function() {
78 thisQ.waitForAllImagesToBeLoaded();
79 }, 100);
80 return;
81 }
82
83 // We now have all images. Carry on, but only after giving the layout a chance to settle down.
84 this.allImagesLoaded = true;
85 thisQ.setupQuestion();
86 };
87
88 /**
89 * Get any of the images in the drag-drop area that are not yet fully loaded.
90 *
91 * @returns {jQuery} those images.
92 */
93 DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {
94 var thisQ = this;
95 return this.getRoot().find('.ddarea img').not(function(i, imgNode) {
96 return thisQ.imageIsLoaded(imgNode);
97 });
98 };
99
100 /**
101 * Check if an image has loaded without errors.
102 *
103 * @param {HTMLImageElement} imgElement an image.
104 * @returns {boolean} true if this image has loaded without errors.
105 */
106 DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {
107 return imgElement.complete && imgElement.naturalHeight !== 0;
108 };
109
110 /**
111 * Set up the question, once all images have been loaded.
112 */
113 DragDropOntoImageQuestion.prototype.setupQuestion = function() {
114 this.resizeAllDragsAndDrops();
115 this.cloneDrags();
116 this.positionDragsAndDrops();
117 M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);
118
119 };
120
121 /**
122 * In each group, resize all the items to be the same size.
123 */
124 DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {
125 var thisQ = this;
126 this.getRoot().find('.draghomes > div').each(function(i) {
127 thisQ.resizeAllDragsAndDropsInGroup(i + 1);
128 });
129 };
130
131 /**
132 * In a given group, set all the drags and drops to be the same size.
133 *
134 * @param {int} group the group number.
135 */
136 DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {
137 var root = this.getRoot(),
138 dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),
139 maxWidth = 0,
140 maxHeight = 0;
141
142 // Find the maximum size of any drag in this groups.
143 dragHomes.each(function(i, drag) {
144 maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));
145 maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));
146 });
147
148 // The size we will want to set is a bit bigger than this.
149 maxWidth += 10;
150 maxHeight += 10;
151
152 // Set each drag home to that size.
153 dragHomes.each(function(i, drag) {
154 var left = Math.round((maxWidth - drag.offsetWidth) / 2),
155 top = Math.floor((maxHeight - drag.offsetHeight) / 2);
156 // Set top and left padding so the item is centred.
157 $(drag).css({
158 'padding-left': left + 'px',
159 'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',
160 'padding-top': top + 'px',
161 'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'
162 });
163 });
164
165 // Create the drops and make them the right size.
166 for (var i in this.places) {
167 if (!this.places.hasOwnProperty((i))) {
168 continue;
169 }
170 var place = this.places[i],
171 label = place.text;
172 if (parseInt(place.group) !== group) {
173 continue;
174 }
175 if (label === '') {
176 label = M.util.get_string('blank', 'qtype_ddimageortext');
177 }
178 root.find('.dropzones').append('<div class="dropzone group' + place.group +
179 ' place' + i + '" tabindex="0">' +
180 '<span class="accesshide">' + label + '</span>&nbsp;</div>');
181 root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);
182 }
183 };
184
185 /**
186 * Invisible 'drag homes' are output by the renderer. These have the same properties
187 * as the drag items but are invisible. We clone these invisible elements to make the
188 * actual drag items.
189 */
190 DragDropOntoImageQuestion.prototype.cloneDrags = function() {
191 var thisQ = this;
192 this.getRoot().find('.ddarea .draghome').each(function(index, dragHome) {
193 thisQ.cloneDragsForOneChoice($(dragHome));
194 });
195 };
196
197 /**
198 * Clone drag item for one choice.
199 *
200 * @param {jQuery} dragHome the drag home to clone.
201 */
202 DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {
203 if (dragHome.hasClass('infinite')) {
204 var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));
205 for (var i = 0; i < noOfDrags; i++) {
206 this.cloneDrag(dragHome);
207 }
208 } else {
209 this.cloneDrag(dragHome);
210 }
211 };
212
213 /**
214 * Clone drag item.
215 *
216 * @param {jQuery} dragHome
217 */
218 DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {
219 var drag = dragHome.clone();
220 drag.removeClass('draghome')
221 .addClass('drag unplaced moodle-has-zindex')
222 .offset(dragHome.offset());
223 this.getRoot().find('.dragitems').append(drag);
224 };
225
226 /**
227 * Update the position of drags.
228 */
229 DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {
230 var thisQ = this,
231 root = this.getRoot(),
232 bgPosition = this.bgImage().offset();
233
234 // Move the drops into position.
235 root.find('.ddarea .dropzone').each(function(i, dropNode) {
236 var drop = $(dropNode),
237 place = thisQ.places[thisQ.getPlace(drop)];
238 // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation.
239 drop.offset({
240 left: bgPosition.left + parseInt(place.xy[0]),
241 top: bgPosition.top + parseInt(place.xy[1])});
242 });
243
244 // First move all items back home.
245 root.find('.ddarea .drag').each(function(i, dragNode) {
246 var drag = $(dragNode),
247 currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');
248 drag.addClass('unplaced')
249 .removeClass('placed')
250 .offset(thisQ.getDragHome(thisQ.getGroup(drag), thisQ.getChoice(drag)).offset());
251 if (currentPlace !== null) {
252 drag.removeClass('inplace' + currentPlace);
253 }
254 });
255
256 // Then place the ones that should be placed.
257 root.find('input.placeinput').each(function(i, inputNode) {
258 var input = $(inputNode),
259 choice = input.val();
260 if (choice === '0') {
261 // No item in this place.
262 return;
263 }
264
265 var place = thisQ.getPlace(input);
266 thisQ.getUnplacedChoice(thisQ.getGroup(input), choice)
267 .removeClass('unplaced')
268 .addClass('placed inplace' + place)
269 .offset(root.find('.dropzone.place' + place).offset());
270 });
271 };
272
273 /**
274 * Handles the start of dragging an item.
275 *
276 * @param {Event} e the touch start or mouse down event.
277 */
278 DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {
279 var thisQ = this,
280 drag = $(e.target).closest('.drag');
281
282 var info = dragDrop.prepare(e);
283 if (!info.start) {
284 return;
285 }
286
287 var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
288 if (currentPlace !== null) {
289 this.setInputValue(currentPlace, 0);
290 drag.removeClass('inplace' + currentPlace);
291 }
292
293 drag.addClass('beingdragged');
294 dragDrop.start(e, drag, function(x, y, drag) {
295 thisQ.dragMove(x, y, drag);
296 }, function(x, y, drag) {
297 thisQ.dragEnd(x, y, drag);
298 });
299 };
300
301 /**
302 * Called whenever the currently dragged items moves.
303 *
304 * @param {Number} pageX the x position.
305 * @param {Number} pageY the y position.
306 * @param {jQuery} drag the item being moved.
307 */
308 DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {
309 var thisQ = this;
310 this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {
311 var drop = $(dropNode);
312 if (thisQ.isPointInDrop(pageX, pageY, drop)) {
313 drop.addClass('valid-drag-over-drop');
314 } else {
315 drop.removeClass('valid-drag-over-drop');
316 }
317 });
318 };
319
320 /**
321 * Called when user drops a drag item.
322 *
323 * @param {Number} pageX the x position.
324 * @param {Number} pageY the y position.
325 * @param {jQuery} drag the item being moved.
326 */
327 DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {
328 var thisQ = this,
329 root = this.getRoot(),
330 placed = false;
331 root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {
332 var drop = $(dropNode);
333 if (!thisQ.isPointInDrop(pageX, pageY, drop)) {
334 // Not this drop.
335 return true;
336 }
337
338 // Now put this drag into the drop.
339 drop.removeClass('valid-drag-over-drop');
340 thisQ.sendDragToDrop(drag, drop);
341 placed = true;
342 return false; // Stop the each() here.
343 });
344
345 if (!placed) {
346 this.sendDragHome(drag);
347 }
348 };
349
350 /**
351 * Animate a drag item into a given place (or back home).
352 *
353 * @param {jQuery|null} drag the item to place. If null, clear the place.
354 * @param {jQuery} drop the place to put it.
355 */
356 DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {
357 // Is there already a drag in this drop? if so, evict it.
358 var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));
359 if (oldDrag.length !== 0) {
360 this.sendDragHome(oldDrag);
361 }
362
363 if (drag.length === 0) {
364 this.setInputValue(this.getPlace(drop), 0);
365 } else {
366 this.setInputValue(this.getPlace(drop), this.getChoice(drag));
367 drag.removeClass('unplaced')
368 .addClass('placed inplace' + this.getPlace(drop));
369 this.animateTo(drag, drop);
370 }
371 };
372
373 /**
374 * Animate a drag back to its home.
375 *
376 * @param {jQuery} drag the item being moved.
377 */
378 DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {
379 drag.removeClass('placed').addClass('unplaced');
380 var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
381 if (currentPlace !== null) {
382 drag.removeClass('inplace' + currentPlace);
383 }
384
385 this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));
386 };
387
388 /**
389 * Handles keyboard events on drops.
390 *
391 * Drops are focusable. Once focused, right/down/space switches to the next choice, and
392 * left/up switches to the previous. Escape clear.
393 *
394 * @param {KeyboardEvent} e
395 */
396 DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {
397 var drop = $(e.target).closest('.dropzone'),
398 currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),
399 nextDrag = $();
400
401 switch (e.keyCode) {
402 case keys.space:
403 case keys.arrowRight:
404 case keys.arrowDown:
405 nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);
406 break;
407
408 case keys.arrowLeft:
409 case keys.arrowUp:
410 nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);
411 break;
412
413 case keys.escape:
414 break;
415
416 default:
417 return; // To avoid the preventDefault below.
418 }
419
420 e.preventDefault();
421 this.sendDragToDrop(nextDrag, drop);
422 };
423
424 /**
425 * Choose the next drag in a group.
426 *
427 * @param {int} group which group.
428 * @param {jQuery} drag current choice (empty jQuery if there isn't one).
429 * @return {jQuery} the next drag in that group, or null if there wasn't one.
430 */
431 DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {
432 var choice,
433 numChoices = this.noOfChoicesInGroup(group);
434
435 if (drag.length === 0) {
436 choice = 1; // Was empty, so we want to select the first choice.
437 } else {
438 choice = this.getChoice(drag) + 1;
439 }
440
441 var next = this.getUnplacedChoice(group, choice);
442 while (next.length === 0 && choice < numChoices) {
443 choice++;
444 next = this.getUnplacedChoice(group, choice);
445 }
446
447 return next;
448 };
449
450 /**
451 * Choose the previous drag in a group.
452 *
453 * @param {int} group which group.
454 * @param {jQuery} drag current choice (empty jQuery if there isn't one).
455 * @return {jQuery} the next drag in that group, or null if there wasn't one.
456 */
457 DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {
458 var choice;
459
460 if (drag.length === 0) {
461 choice = this.noOfChoicesInGroup(group);
462 } else {
463 choice = this.getChoice(drag) - 1;
464 }
465
466 var previous = this.getUnplacedChoice(group, choice);
467 while (previous.length === 0 && choice > 1) {
468 choice--;
469 previous = this.getUnplacedChoice(group, choice);
470 }
471
472 // Does this choice exist?
473 return previous;
474 };
475
476 /**
477 * Animate an object to the given destination.
478 *
479 * @param {jQuery} drag the element to be animated.
480 * @param {jQuery} target element marking the place to move it to.
481 */
482 DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {
483 var currentPos = drag.offset(),
484 targetPos = target.offset();
485 drag.addClass('beingdragged');
486
487 // Animate works in terms of CSS position, whereas locating an object
488 // on the page works best with jQuery offset() function. So, to get
489 // the right target position, we work out the required change in
490 // offset() and then add that to the current CSS position.
491 drag.animate(
492 {
493 left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,
494 top: parseInt(drag.css('top')) + targetPos.top - currentPos.top
495 },
496 {
497 duration: 'fast',
498 done: function() {
499 drag.removeClass('beingdragged');
500 // It seems that the animation sometimes leaves the drag
501 // one pixel out of position. Put it in exactly the right place.
502 drag.offset(targetPos);
503 }
504 }
505 );
506 };
507
508 /**
509 * Detect if a point is inside a given DOM node.
510 *
511 * @param {Number} pageX the x position.
512 * @param {Number} pageY the y position.
513 * @param {jQuery} drop the node to check (typically a drop).
514 * @return {boolean} whether the point is inside the node.
515 */
516 DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {
517 var position = drop.offset();
518 return pageX >= position.left && pageX < position.left + drop.width()
519 && pageY >= position.top && pageY < position.top + drop.height();
520 };
521
522 /**
523 * Set the value of the hidden input for a place, to record what is currently there.
524 *
525 * @param {int} place which place to set the input value for.
526 * @param {int} choice the value to set.
527 */
528 DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {
529 this.getRoot().find('input.placeinput.place' + place).val(choice);
530 };
531
532 /**
533 * Get the outer div for this question.
534 *
535 * @returns {jQuery} containing that div.
536 */
537 DragDropOntoImageQuestion.prototype.getRoot = function() {
538 return $(document.getElementById(this.containerId));
539 };
540
541 /**
542 * Get the img that is the background image.
543 * @returns {jQuery} containing that img.
544 */
545 DragDropOntoImageQuestion.prototype.bgImage = function() {
546 return this.getRoot().find('img.dropbackground');
547 };
548
549 /**
550 * Get drag home for a given choice.
551 *
552 * @param {int} group the group.
553 * @param {int} choice the choice number.
554 * @returns {jQuery} containing that div.
555 */
556 DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {
557 return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice);
558 };
559
560 /**
561 * Get an unplaced choice for a particular group.
562 *
563 * @param {int} group the group.
564 * @param {int} choice the choice number.
565 * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.
566 */
567 DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {
568 return this.getRoot().find('.ddarea .drag.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);
569 };
570
571 /**
572 * Get the drag that is currently in a given place.
573 *
574 * @param {int} place the place number.
575 * @return {jQuery} the current drag (or an empty jQuery if none).
576 */
577 DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {
578 return this.getRoot().find('.ddarea .drag.inplace' + place);
579 };
580
581 /**
582 * Return the number of blanks in a given group.
583 *
584 * @param {int} group the group number.
585 * @returns {int} the number of drops.
586 */
587 DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {
588 return this.getRoot().find('.dropzone.group' + group).length;
589 };
590
591 /**
592 * Return the number of choices in a given group.
593 *
594 * @param {int} group the group number.
595 * @returns {int} the number of choices.
596 */
597 DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {
598 return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;
599 };
600
601 /**
602 * Return the number at the end of the CSS class name with the given prefix.
603 *
604 * @param {jQuery} node
605 * @param {String} prefix name prefix
606 * @returns {Number|null} the suffix if found, else null.
607 */
608 DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {
609 var classes = node.attr('class');
610 if (classes !== '') {
611 var classesArr = classes.split(' ');
612 for (var index = 0; index < classesArr.length; index++) {
613 var patt1 = new RegExp('^' + prefix + '([0-9])+$');
614 if (patt1.test(classesArr[index])) {
615 var patt2 = new RegExp('([0-9])+$');
616 var match = patt2.exec(classesArr[index]);
617 return Number(match[0]);
618 }
619 }
620 }
621 return null;
622 };
623
624 /**
625 * Get the choice number of a drag.
626 *
627 * @param {jQuery} drag the drag.
628 * @returns {Number} the choice number.
629 */
630 DragDropOntoImageQuestion.prototype.getChoice = function(drag) {
631 return this.getClassnameNumericSuffix(drag, 'choice');
632 };
633
634 /**
635 * Given a DOM node that is significant to this question
636 * (drag, drop, ...) get the group it belongs to.
637 *
638 * @param {jQuery} node a DOM node.
639 * @returns {Number} the group it belongs to.
640 */
641 DragDropOntoImageQuestion.prototype.getGroup = function(node) {
642 return this.getClassnameNumericSuffix(node, 'group');
643 };
644
645 /**
646 * Get the place number of a drop, or its corresponding hidden input.
647 *
648 * @param {jQuery} node the DOM node.
649 * @returns {Number} the place number.
650 */
651 DragDropOntoImageQuestion.prototype.getPlace = function(node) {
652 return this.getClassnameNumericSuffix(node, 'place');
653 };
654
655 /**
656 * Singleton object that handles all the DragDropOntoImageQuestions
657 * on the page, and deals with event dispatching.
658 * @type {Object}
659 */
660 var questionManager = {
661
662 /**
663 * {boolean} ensures that the event handlers are only initialised once per page.
664 */
665 eventHandlersInitialised: false,
666
667 /**
668 * {Object} all the questions on this page, indexed by containerId (id on the .que div).
669 */
670 questions: {}, // An object containing all the information about each question on the page.
671
672 /**
673 * Initialise one question.
674 *
675 * @param {String} containerId the id of the div.que that contains this question.
676 * @param {boolean} readOnly whether the question is read-only.
677 * @param {Array} places data.
678 */
679 init: function(containerId, readOnly, places) {
680 questionManager.questions[containerId] =
681 new DragDropOntoImageQuestion(containerId, readOnly, places);
682 if (!questionManager.eventHandlersInitialised) {
683 questionManager.setupEventHandlers();
684 questionManager.eventHandlersInitialised = true;
685 }
686 },
687
688 /**
689 * Set up the event handlers that make this question type work. (Done once per page.)
690 */
691 setupEventHandlers: function() {
692 $('body')
693 .on('mousedown touchstart',
694 '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dragitems .drag',
695 questionManager.handleDragStart)
696 .on('keydown',
697 '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',
698 questionManager.handleKeyPress);
699 $(window).on('resize', questionManager.handleWindowResize);
700 },
701
702 /**
703 * Handle mouse down / touch start events on drags.
704 * @param {Event} e the DOM event.
705 */
706 handleDragStart: function(e) {
707 e.preventDefault();
708 var question = questionManager.getQuestionForEvent(e);
709 if (question) {
710 question.handleDragStart(e);
711 }
712 },
713
714 /**
715 * Handle key down / press events on drags.
716 * @param {KeyboardEvent} e
717 */
718 handleKeyPress: function(e) {
719 var question = questionManager.getQuestionForEvent(e);
720 if (question) {
721 question.handleKeyPress(e);
722 }
723 },
724
725 /**
726 * Handle when the window is resized.
727 */
728 handleWindowResize: function() {
729 for (var containerId in questionManager.questions) {
730 if (questionManager.questions.hasOwnProperty(containerId)) {
731 questionManager.questions[containerId].positionDragsAndDrops();
732 }
733 }
734 },
735
736 /**
737 * Given an event, work out which question it effects.
738 * @param {Event} e the event.
739 * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.
740 */
741 getQuestionForEvent: function(e) {
742 var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');
743 return questionManager.questions[containerId];
744 }
745 };
746
747 /**
748 * @alias module:qtype_ddimageortext/question
749 */
750 return {
751 /**
752 * Initialise one drag-drop onto image question.
753 *
754 * @param {String} containerId id of the outer div for this question.
755 * @param {boolean} readOnly whether the question is being displayed read-only.
756 * @param {Array} Information about the drop places.
757 */
758 init: questionManager.init
759 };
760});