weekly release 3.9dev
[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);
ebf91776
TH
118 };
119
120 /**
121 * In each group, resize all the items to be the same size.
122 */
123 DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {
124 var thisQ = this;
d434dd7a
TH
125 this.getRoot().find('.draghomes > div').each(function(i, node) {
126 thisQ.resizeAllDragsAndDropsInGroup(
127 thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));
ebf91776
TH
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 });
e5153d93
TH
271
272 this.bgImage().data('prev-top', bgPosition.top).data('prev-left', bgPosition.left);
273 };
274
275 /**
276 * Check to see if the background image has moved. If so, refresh the layout.
277 */
278 DragDropOntoImageQuestion.prototype.fixLayoutIfBackgroundMoved = function() {
279 var bgImage = this.bgImage(),
280 bgPosition = bgImage.offset(),
281 prevTop = bgImage.data('prev-top'),
282 prevLeft = bgImage.data('prev-left');
283 if (prevLeft === undefined || prevTop === undefined) {
284 // Question is not set up yet. Nothing to do.
285 return;
286 }
287 if (prevTop === bgPosition.top && prevLeft === bgPosition.left) {
288 // Things have not moved.
289 return;
290 }
291 // We need to reposition things.
292 this.positionDragsAndDrops();
ebf91776
TH
293 };
294
295 /**
296 * Handles the start of dragging an item.
297 *
298 * @param {Event} e the touch start or mouse down event.
299 */
300 DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {
301 var thisQ = this,
302 drag = $(e.target).closest('.drag');
303
304 var info = dragDrop.prepare(e);
305 if (!info.start) {
306 return;
307 }
308
309 var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
310 if (currentPlace !== null) {
311 this.setInputValue(currentPlace, 0);
312 drag.removeClass('inplace' + currentPlace);
313 }
314
315 drag.addClass('beingdragged');
316 dragDrop.start(e, drag, function(x, y, drag) {
317 thisQ.dragMove(x, y, drag);
318 }, function(x, y, drag) {
319 thisQ.dragEnd(x, y, drag);
320 });
321 };
322
323 /**
324 * Called whenever the currently dragged items moves.
325 *
326 * @param {Number} pageX the x position.
327 * @param {Number} pageY the y position.
328 * @param {jQuery} drag the item being moved.
329 */
330 DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {
331 var thisQ = this;
332 this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {
333 var drop = $(dropNode);
334 if (thisQ.isPointInDrop(pageX, pageY, drop)) {
335 drop.addClass('valid-drag-over-drop');
336 } else {
337 drop.removeClass('valid-drag-over-drop');
338 }
339 });
340 };
341
342 /**
343 * Called when user drops a drag item.
344 *
345 * @param {Number} pageX the x position.
346 * @param {Number} pageY the y position.
347 * @param {jQuery} drag the item being moved.
348 */
349 DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {
350 var thisQ = this,
351 root = this.getRoot(),
352 placed = false;
353 root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {
354 var drop = $(dropNode);
355 if (!thisQ.isPointInDrop(pageX, pageY, drop)) {
356 // Not this drop.
357 return true;
358 }
359
360 // Now put this drag into the drop.
361 drop.removeClass('valid-drag-over-drop');
362 thisQ.sendDragToDrop(drag, drop);
363 placed = true;
364 return false; // Stop the each() here.
365 });
366
367 if (!placed) {
368 this.sendDragHome(drag);
369 }
370 };
371
372 /**
373 * Animate a drag item into a given place (or back home).
374 *
375 * @param {jQuery|null} drag the item to place. If null, clear the place.
376 * @param {jQuery} drop the place to put it.
377 */
378 DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {
379 // Is there already a drag in this drop? if so, evict it.
380 var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));
381 if (oldDrag.length !== 0) {
382 this.sendDragHome(oldDrag);
383 }
384
385 if (drag.length === 0) {
386 this.setInputValue(this.getPlace(drop), 0);
387 } else {
388 this.setInputValue(this.getPlace(drop), this.getChoice(drag));
389 drag.removeClass('unplaced')
390 .addClass('placed inplace' + this.getPlace(drop));
391 this.animateTo(drag, drop);
392 }
393 };
394
395 /**
396 * Animate a drag back to its home.
397 *
398 * @param {jQuery} drag the item being moved.
399 */
400 DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {
401 drag.removeClass('placed').addClass('unplaced');
402 var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
403 if (currentPlace !== null) {
404 drag.removeClass('inplace' + currentPlace);
405 }
406
407 this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));
408 };
409
410 /**
411 * Handles keyboard events on drops.
412 *
413 * Drops are focusable. Once focused, right/down/space switches to the next choice, and
414 * left/up switches to the previous. Escape clear.
415 *
416 * @param {KeyboardEvent} e
417 */
418 DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {
419 var drop = $(e.target).closest('.dropzone'),
420 currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),
421 nextDrag = $();
422
423 switch (e.keyCode) {
424 case keys.space:
425 case keys.arrowRight:
426 case keys.arrowDown:
427 nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);
428 break;
429
430 case keys.arrowLeft:
431 case keys.arrowUp:
432 nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);
433 break;
434
435 case keys.escape:
436 break;
437
438 default:
439 return; // To avoid the preventDefault below.
440 }
441
442 e.preventDefault();
443 this.sendDragToDrop(nextDrag, drop);
444 };
445
446 /**
447 * Choose the next drag in a group.
448 *
449 * @param {int} group which group.
450 * @param {jQuery} drag current choice (empty jQuery if there isn't one).
451 * @return {jQuery} the next drag in that group, or null if there wasn't one.
452 */
453 DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {
454 var choice,
455 numChoices = this.noOfChoicesInGroup(group);
456
457 if (drag.length === 0) {
458 choice = 1; // Was empty, so we want to select the first choice.
459 } else {
460 choice = this.getChoice(drag) + 1;
461 }
462
463 var next = this.getUnplacedChoice(group, choice);
464 while (next.length === 0 && choice < numChoices) {
465 choice++;
466 next = this.getUnplacedChoice(group, choice);
467 }
468
469 return next;
470 };
471
472 /**
473 * Choose the previous drag in a group.
474 *
475 * @param {int} group which group.
476 * @param {jQuery} drag current choice (empty jQuery if there isn't one).
477 * @return {jQuery} the next drag in that group, or null if there wasn't one.
478 */
479 DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {
480 var choice;
481
482 if (drag.length === 0) {
483 choice = this.noOfChoicesInGroup(group);
484 } else {
485 choice = this.getChoice(drag) - 1;
486 }
487
488 var previous = this.getUnplacedChoice(group, choice);
489 while (previous.length === 0 && choice > 1) {
490 choice--;
491 previous = this.getUnplacedChoice(group, choice);
492 }
493
494 // Does this choice exist?
495 return previous;
496 };
497
498 /**
499 * Animate an object to the given destination.
500 *
501 * @param {jQuery} drag the element to be animated.
502 * @param {jQuery} target element marking the place to move it to.
503 */
504 DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {
505 var currentPos = drag.offset(),
506 targetPos = target.offset();
507 drag.addClass('beingdragged');
508
509 // Animate works in terms of CSS position, whereas locating an object
510 // on the page works best with jQuery offset() function. So, to get
511 // the right target position, we work out the required change in
512 // offset() and then add that to the current CSS position.
513 drag.animate(
514 {
515 left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,
516 top: parseInt(drag.css('top')) + targetPos.top - currentPos.top
517 },
518 {
519 duration: 'fast',
520 done: function() {
521 drag.removeClass('beingdragged');
522 // It seems that the animation sometimes leaves the drag
523 // one pixel out of position. Put it in exactly the right place.
524 drag.offset(targetPos);
525 }
526 }
527 );
528 };
529
530 /**
531 * Detect if a point is inside a given DOM node.
532 *
533 * @param {Number} pageX the x position.
534 * @param {Number} pageY the y position.
535 * @param {jQuery} drop the node to check (typically a drop).
536 * @return {boolean} whether the point is inside the node.
537 */
538 DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {
539 var position = drop.offset();
540 return pageX >= position.left && pageX < position.left + drop.width()
541 && pageY >= position.top && pageY < position.top + drop.height();
542 };
543
544 /**
545 * Set the value of the hidden input for a place, to record what is currently there.
546 *
547 * @param {int} place which place to set the input value for.
548 * @param {int} choice the value to set.
549 */
550 DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {
551 this.getRoot().find('input.placeinput.place' + place).val(choice);
552 };
553
554 /**
555 * Get the outer div for this question.
556 *
557 * @returns {jQuery} containing that div.
558 */
559 DragDropOntoImageQuestion.prototype.getRoot = function() {
560 return $(document.getElementById(this.containerId));
561 };
562
563 /**
564 * Get the img that is the background image.
565 * @returns {jQuery} containing that img.
566 */
567 DragDropOntoImageQuestion.prototype.bgImage = function() {
568 return this.getRoot().find('img.dropbackground');
569 };
570
571 /**
572 * Get drag home for a given choice.
573 *
574 * @param {int} group the group.
575 * @param {int} choice the choice number.
576 * @returns {jQuery} containing that div.
577 */
578 DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {
579 return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice);
580 };
581
582 /**
583 * Get an unplaced choice for a particular group.
584 *
585 * @param {int} group the group.
586 * @param {int} choice the choice number.
587 * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.
588 */
589 DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {
590 return this.getRoot().find('.ddarea .drag.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);
591 };
592
593 /**
594 * Get the drag that is currently in a given place.
595 *
596 * @param {int} place the place number.
597 * @return {jQuery} the current drag (or an empty jQuery if none).
598 */
599 DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {
600 return this.getRoot().find('.ddarea .drag.inplace' + place);
601 };
602
603 /**
604 * Return the number of blanks in a given group.
605 *
606 * @param {int} group the group number.
607 * @returns {int} the number of drops.
608 */
609 DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {
610 return this.getRoot().find('.dropzone.group' + group).length;
611 };
612
613 /**
614 * Return the number of choices in a given group.
615 *
616 * @param {int} group the group number.
617 * @returns {int} the number of choices.
618 */
619 DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {
620 return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;
621 };
622
623 /**
624 * Return the number at the end of the CSS class name with the given prefix.
625 *
626 * @param {jQuery} node
627 * @param {String} prefix name prefix
628 * @returns {Number|null} the suffix if found, else null.
629 */
630 DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {
631 var classes = node.attr('class');
632 if (classes !== '') {
633 var classesArr = classes.split(' ');
634 for (var index = 0; index < classesArr.length; index++) {
635 var patt1 = new RegExp('^' + prefix + '([0-9])+$');
636 if (patt1.test(classesArr[index])) {
637 var patt2 = new RegExp('([0-9])+$');
638 var match = patt2.exec(classesArr[index]);
639 return Number(match[0]);
640 }
641 }
642 }
643 return null;
644 };
645
646 /**
647 * Get the choice number of a drag.
648 *
649 * @param {jQuery} drag the drag.
650 * @returns {Number} the choice number.
651 */
652 DragDropOntoImageQuestion.prototype.getChoice = function(drag) {
653 return this.getClassnameNumericSuffix(drag, 'choice');
654 };
655
656 /**
657 * Given a DOM node that is significant to this question
658 * (drag, drop, ...) get the group it belongs to.
659 *
660 * @param {jQuery} node a DOM node.
661 * @returns {Number} the group it belongs to.
662 */
663 DragDropOntoImageQuestion.prototype.getGroup = function(node) {
664 return this.getClassnameNumericSuffix(node, 'group');
665 };
666
667 /**
668 * Get the place number of a drop, or its corresponding hidden input.
669 *
670 * @param {jQuery} node the DOM node.
671 * @returns {Number} the place number.
672 */
673 DragDropOntoImageQuestion.prototype.getPlace = function(node) {
674 return this.getClassnameNumericSuffix(node, 'place');
675 };
676
677 /**
678 * Singleton object that handles all the DragDropOntoImageQuestions
679 * on the page, and deals with event dispatching.
680 * @type {Object}
681 */
682 var questionManager = {
683
684 /**
685 * {boolean} ensures that the event handlers are only initialised once per page.
686 */
687 eventHandlersInitialised: false,
688
689 /**
690 * {Object} all the questions on this page, indexed by containerId (id on the .que div).
691 */
692 questions: {}, // An object containing all the information about each question on the page.
693
694 /**
695 * Initialise one question.
696 *
697 * @param {String} containerId the id of the div.que that contains this question.
698 * @param {boolean} readOnly whether the question is read-only.
699 * @param {Array} places data.
700 */
701 init: function(containerId, readOnly, places) {
702 questionManager.questions[containerId] =
703 new DragDropOntoImageQuestion(containerId, readOnly, places);
704 if (!questionManager.eventHandlersInitialised) {
705 questionManager.setupEventHandlers();
706 questionManager.eventHandlersInitialised = true;
707 }
708 },
709
710 /**
711 * Set up the event handlers that make this question type work. (Done once per page.)
712 */
713 setupEventHandlers: function() {
714 $('body')
715 .on('mousedown touchstart',
716 '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dragitems .drag',
717 questionManager.handleDragStart)
718 .on('keydown',
719 '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',
720 questionManager.handleKeyPress);
721 $(window).on('resize', questionManager.handleWindowResize);
e5153d93 722 setTimeout(questionManager.fixLayoutIfThingsMoved, 100);
ebf91776
TH
723 },
724
725 /**
726 * Handle mouse down / touch start events on drags.
727 * @param {Event} e the DOM event.
728 */
729 handleDragStart: function(e) {
730 e.preventDefault();
731 var question = questionManager.getQuestionForEvent(e);
732 if (question) {
733 question.handleDragStart(e);
734 }
735 },
736
737 /**
738 * Handle key down / press events on drags.
739 * @param {KeyboardEvent} e
740 */
741 handleKeyPress: function(e) {
742 var question = questionManager.getQuestionForEvent(e);
743 if (question) {
744 question.handleKeyPress(e);
745 }
746 },
747
748 /**
749 * Handle when the window is resized.
750 */
751 handleWindowResize: function() {
752 for (var containerId in questionManager.questions) {
753 if (questionManager.questions.hasOwnProperty(containerId)) {
754 questionManager.questions[containerId].positionDragsAndDrops();
755 }
756 }
757 },
758
e5153d93
TH
759 /**
760 * Sometimes, despite our best efforts, things change in a way that cannot
761 * be specifically caught (e.g. dock expanding or collapsing in Boost).
762 * Therefore, we need to periodically check everything is in the right position.
763 */
764 fixLayoutIfThingsMoved: function() {
765 for (var containerId in questionManager.questions) {
766 if (questionManager.questions.hasOwnProperty(containerId)) {
767 questionManager.questions[containerId].fixLayoutIfBackgroundMoved();
768 }
769 }
770
771 // We use setTimeout after finishing work, rather than setInterval,
772 // in case positioning things is slow. We want 100 ms gap
773 // between executions, not what setInterval does.
774 setTimeout(questionManager.fixLayoutIfThingsMoved, 100);
775 },
776
ebf91776
TH
777 /**
778 * Given an event, work out which question it effects.
779 * @param {Event} e the event.
780 * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.
781 */
782 getQuestionForEvent: function(e) {
783 var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');
784 return questionManager.questions[containerId];
785 }
786 };
787
788 /**
789 * @alias module:qtype_ddimageortext/question
790 */
791 return {
792 /**
793 * Initialise one drag-drop onto image question.
794 *
795 * @param {String} containerId id of the outer div for this question.
796 * @param {boolean} readOnly whether the question is being displayed read-only.
797 * @param {Array} Information about the drop places.
798 */
799 init: questionManager.init
800 };
801});