Commit | Line | Data |
---|---|---|
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 | |
ebf91776 TH |
20 | * @copyright 2018 The Open University |
21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
22 | */ | |
23 | define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) { | |
24 | ||
25 | "use strict"; | |
26 | ||
27 | /** | |
28 | * Initialise one drag-drop onto image question. | |
29 | * | |
30 | * @param {String} containerId id of the outer div for this question. | |
31 | * @param {boolean} readOnly whether the question is being displayed read-only. | |
32 | * @param {Array} places Information about the drop places. | |
33 | * @constructor | |
34 | */ | |
35 | function DragDropOntoImageQuestion(containerId, readOnly, places) { | |
36 | this.containerId = containerId; | |
37 | M.util.js_pending('qtype_ddimageortext-init-' + this.containerId); | |
38 | this.places = places; | |
39 | this.allImagesLoaded = false; | |
40 | this.imageLoadingTimeoutId = null; | |
a05ef130 | 41 | this.isPrinting = false; |
ebf91776 TH |
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 | } | |
a05ef130 | 178 | root.find('.dropzones').append('<div class="dropzone active group' + place.group + |
ebf91776 TH |
179 | ' place' + i + '" tabindex="0">' + |
180 | '<span class="accesshide">' + label + '</span> </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; | |
a05ef130 HN |
192 | thisQ.getRoot().find('.draghome').each(function(index, dragHome) { |
193 | var drag = $(dragHome); | |
194 | var placeHolder = drag.clone(); | |
195 | placeHolder.removeClass(); | |
196 | placeHolder.addClass('draghome choice' + | |
197 | thisQ.getChoice(drag) + ' group' + | |
198 | thisQ.getGroup(drag) + ' dragplaceholder'); | |
199 | drag.before(placeHolder); | |
ebf91776 TH |
200 | }); |
201 | }; | |
202 | ||
203 | /** | |
204 | * Clone drag item for one choice. | |
205 | * | |
206 | * @param {jQuery} dragHome the drag home to clone. | |
207 | */ | |
208 | DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) { | |
209 | if (dragHome.hasClass('infinite')) { | |
210 | var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome)); | |
211 | for (var i = 0; i < noOfDrags; i++) { | |
212 | this.cloneDrag(dragHome); | |
213 | } | |
214 | } else { | |
215 | this.cloneDrag(dragHome); | |
216 | } | |
217 | }; | |
218 | ||
219 | /** | |
220 | * Clone drag item. | |
221 | * | |
222 | * @param {jQuery} dragHome | |
223 | */ | |
224 | DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) { | |
225 | var drag = dragHome.clone(); | |
226 | drag.removeClass('draghome') | |
227 | .addClass('drag unplaced moodle-has-zindex') | |
228 | .offset(dragHome.offset()); | |
229 | this.getRoot().find('.dragitems').append(drag); | |
230 | }; | |
231 | ||
232 | /** | |
233 | * Update the position of drags. | |
234 | */ | |
235 | DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() { | |
236 | var thisQ = this, | |
237 | root = this.getRoot(), | |
a05ef130 | 238 | bgRatio = this.bgRatio(); |
ebf91776 TH |
239 | |
240 | // Move the drops into position. | |
241 | root.find('.ddarea .dropzone').each(function(i, dropNode) { | |
242 | var drop = $(dropNode), | |
243 | place = thisQ.places[thisQ.getPlace(drop)]; | |
244 | // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation. | |
a05ef130 HN |
245 | drop.css('left', parseInt(place.xy[0]) * bgRatio) |
246 | .css('top', parseInt(place.xy[1]) * bgRatio); | |
247 | drop.data('originX', parseInt(place.xy[0])) | |
248 | .data('originY', parseInt(place.xy[1])); | |
249 | thisQ.handleElementScale(drop, 'left top'); | |
ebf91776 TH |
250 | }); |
251 | ||
252 | // First move all items back home. | |
a05ef130 | 253 | root.find('.draghome').not('.dragplaceholder').each(function(i, dragNode) { |
ebf91776 TH |
254 | var drag = $(dragNode), |
255 | currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace'); | |
256 | drag.addClass('unplaced') | |
a05ef130 HN |
257 | .removeClass('placed'); |
258 | drag.removeAttr('tabindex'); | |
ebf91776 TH |
259 | if (currentPlace !== null) { |
260 | drag.removeClass('inplace' + currentPlace); | |
261 | } | |
262 | }); | |
263 | ||
264 | // Then place the ones that should be placed. | |
265 | root.find('input.placeinput').each(function(i, inputNode) { | |
266 | var input = $(inputNode), | |
267 | choice = input.val(); | |
a05ef130 | 268 | if (choice.length === 0 || (choice.length > 0 && choice === '0')) { |
ebf91776 TH |
269 | // No item in this place. |
270 | return; | |
271 | } | |
272 | ||
273 | var place = thisQ.getPlace(input); | |
a05ef130 HN |
274 | // Get the unplaced drag. |
275 | var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice); | |
276 | // Get the clone of the drag. | |
277 | var hiddenDrag = thisQ.getDragClone(unplacedDrag); | |
278 | if (hiddenDrag.length) { | |
279 | if (unplacedDrag.hasClass('infinite')) { | |
280 | var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag)); | |
281 | var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false); | |
282 | if (cloneDrags.length < noOfDrags) { | |
283 | var cloneDrag = unplacedDrag.clone(); | |
284 | cloneDrag.removeClass('beingdragged'); | |
285 | cloneDrag.removeAttr('tabindex'); | |
286 | hiddenDrag.after(cloneDrag); | |
287 | } else { | |
288 | hiddenDrag.addClass('active'); | |
289 | } | |
290 | } else { | |
291 | hiddenDrag.addClass('active'); | |
292 | } | |
293 | } | |
e5153d93 | 294 | |
a05ef130 HN |
295 | // Send the drag to drop. |
296 | var drop = root.find('.dropzone.place' + place); | |
297 | thisQ.sendDragToDrop(unplacedDrag, drop); | |
298 | }); | |
ebf91776 TH |
299 | }; |
300 | ||
301 | /** | |
302 | * Handles the start of dragging an item. | |
303 | * | |
304 | * @param {Event} e the touch start or mouse down event. | |
305 | */ | |
306 | DragDropOntoImageQuestion.prototype.handleDragStart = function(e) { | |
307 | var thisQ = this, | |
a05ef130 HN |
308 | drag = $(e.target).closest('.draghome'), |
309 | currentIndex = this.calculateZIndex(), | |
310 | newIndex = currentIndex + 2; | |
ebf91776 TH |
311 | |
312 | var info = dragDrop.prepare(e); | |
313 | if (!info.start) { | |
314 | return; | |
315 | } | |
316 | ||
a05ef130 | 317 | drag.addClass('beingdragged').css('transform', '').css('z-index', newIndex); |
ebf91776 TH |
318 | var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace'); |
319 | if (currentPlace !== null) { | |
320 | this.setInputValue(currentPlace, 0); | |
321 | drag.removeClass('inplace' + currentPlace); | |
a05ef130 HN |
322 | var hiddenDrop = thisQ.getDrop(drag, currentPlace); |
323 | if (hiddenDrop.length) { | |
324 | hiddenDrop.addClass('active'); | |
325 | drag.offset(hiddenDrop.offset()); | |
326 | } | |
327 | } else { | |
328 | var hiddenDrag = thisQ.getDragClone(drag); | |
329 | if (hiddenDrag.length) { | |
330 | if (drag.hasClass('infinite')) { | |
331 | var noOfDrags = this.noOfDropsInGroup(thisQ.getGroup(drag)); | |
332 | var cloneDrags = this.getInfiniteDragClones(drag, false); | |
333 | if (cloneDrags.length < noOfDrags) { | |
334 | var cloneDrag = drag.clone(); | |
335 | cloneDrag.removeClass('beingdragged'); | |
336 | cloneDrag.removeAttr('tabindex'); | |
337 | hiddenDrag.after(cloneDrag); | |
5fa8f1c8 | 338 | questionManager.addEventHandlersToDrag(cloneDrag); |
a05ef130 HN |
339 | drag.offset(cloneDrag.offset()); |
340 | } else { | |
341 | hiddenDrag.addClass('active'); | |
342 | drag.offset(hiddenDrag.offset()); | |
343 | } | |
344 | } else { | |
345 | hiddenDrag.addClass('active'); | |
346 | drag.offset(hiddenDrag.offset()); | |
347 | } | |
348 | } | |
ebf91776 TH |
349 | } |
350 | ||
ebf91776 TH |
351 | dragDrop.start(e, drag, function(x, y, drag) { |
352 | thisQ.dragMove(x, y, drag); | |
353 | }, function(x, y, drag) { | |
354 | thisQ.dragEnd(x, y, drag); | |
355 | }); | |
356 | }; | |
357 | ||
358 | /** | |
359 | * Called whenever the currently dragged items moves. | |
360 | * | |
361 | * @param {Number} pageX the x position. | |
362 | * @param {Number} pageY the y position. | |
363 | * @param {jQuery} drag the item being moved. | |
364 | */ | |
365 | DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) { | |
d67bcd6b HN |
366 | var thisQ = this, |
367 | highlighted = false; | |
ebf91776 TH |
368 | this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) { |
369 | var drop = $(dropNode); | |
d67bcd6b HN |
370 | if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted) { |
371 | highlighted = true; | |
ebf91776 TH |
372 | drop.addClass('valid-drag-over-drop'); |
373 | } else { | |
374 | drop.removeClass('valid-drag-over-drop'); | |
375 | } | |
376 | }); | |
a05ef130 HN |
377 | this.getRoot().find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) { |
378 | var drop = $(dropNode); | |
84b7d233 | 379 | if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted && !thisQ.isDragSameAsDrop(drag, drop)) { |
d67bcd6b | 380 | highlighted = true; |
a05ef130 HN |
381 | drop.addClass('valid-drag-over-drop'); |
382 | } else { | |
383 | drop.removeClass('valid-drag-over-drop'); | |
384 | } | |
385 | }); | |
ebf91776 TH |
386 | }; |
387 | ||
388 | /** | |
389 | * Called when user drops a drag item. | |
390 | * | |
391 | * @param {Number} pageX the x position. | |
392 | * @param {Number} pageY the y position. | |
393 | * @param {jQuery} drag the item being moved. | |
394 | */ | |
395 | DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) { | |
396 | var thisQ = this, | |
397 | root = this.getRoot(), | |
398 | placed = false; | |
d67bcd6b HN |
399 | |
400 | // Looking for drag that was dropped on a dropzone. | |
ebf91776 TH |
401 | root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) { |
402 | var drop = $(dropNode); | |
403 | if (!thisQ.isPointInDrop(pageX, pageY, drop)) { | |
404 | // Not this drop. | |
405 | return true; | |
406 | } | |
407 | ||
408 | // Now put this drag into the drop. | |
409 | drop.removeClass('valid-drag-over-drop'); | |
410 | thisQ.sendDragToDrop(drag, drop); | |
411 | placed = true; | |
412 | return false; // Stop the each() here. | |
413 | }); | |
414 | ||
d67bcd6b HN |
415 | if (!placed) { |
416 | // Looking for drag that was dropped on a placed drag. | |
417 | root.find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) { | |
418 | var placedDrag = $(placedNode); | |
84b7d233 | 419 | if (!thisQ.isPointInDrop(pageX, pageY, placedDrag) || thisQ.isDragSameAsDrop(drag, placedDrag)) { |
d67bcd6b HN |
420 | // Not this placed drag. |
421 | return true; | |
422 | } | |
a05ef130 | 423 | |
d67bcd6b HN |
424 | // Now put this drag into the drop. |
425 | placedDrag.removeClass('valid-drag-over-drop'); | |
426 | var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace'); | |
427 | var drop = thisQ.getDrop(drag, currentPlace); | |
428 | thisQ.sendDragToDrop(drag, drop); | |
429 | placed = true; | |
430 | return false; // Stop the each() here. | |
431 | }); | |
432 | } | |
a05ef130 | 433 | |
ebf91776 TH |
434 | if (!placed) { |
435 | this.sendDragHome(drag); | |
436 | } | |
437 | }; | |
438 | ||
439 | /** | |
440 | * Animate a drag item into a given place (or back home). | |
441 | * | |
442 | * @param {jQuery|null} drag the item to place. If null, clear the place. | |
443 | * @param {jQuery} drop the place to put it. | |
444 | */ | |
445 | DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) { | |
446 | // Is there already a drag in this drop? if so, evict it. | |
447 | var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop)); | |
448 | if (oldDrag.length !== 0) { | |
a05ef130 HN |
449 | oldDrag.addClass('beingdragged'); |
450 | oldDrag.offset(oldDrag.offset()); | |
451 | var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace'); | |
452 | var hiddenDrop = this.getDrop(oldDrag, currentPlace); | |
453 | hiddenDrop.addClass('active'); | |
ebf91776 TH |
454 | this.sendDragHome(oldDrag); |
455 | } | |
456 | ||
457 | if (drag.length === 0) { | |
458 | this.setInputValue(this.getPlace(drop), 0); | |
a05ef130 HN |
459 | if (drop.data('isfocus')) { |
460 | drop.focus(); | |
461 | } | |
ebf91776 TH |
462 | } else { |
463 | this.setInputValue(this.getPlace(drop), this.getChoice(drag)); | |
464 | drag.removeClass('unplaced') | |
465 | .addClass('placed inplace' + this.getPlace(drop)); | |
a05ef130 | 466 | drag.attr('tabindex', 0); |
ebf91776 TH |
467 | this.animateTo(drag, drop); |
468 | } | |
469 | }; | |
470 | ||
471 | /** | |
472 | * Animate a drag back to its home. | |
473 | * | |
474 | * @param {jQuery} drag the item being moved. | |
475 | */ | |
476 | DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) { | |
ebf91776 TH |
477 | var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace'); |
478 | if (currentPlace !== null) { | |
479 | drag.removeClass('inplace' + currentPlace); | |
480 | } | |
a05ef130 | 481 | drag.data('unplaced', true); |
ebf91776 TH |
482 | |
483 | this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag))); | |
484 | }; | |
485 | ||
486 | /** | |
487 | * Handles keyboard events on drops. | |
488 | * | |
489 | * Drops are focusable. Once focused, right/down/space switches to the next choice, and | |
490 | * left/up switches to the previous. Escape clear. | |
491 | * | |
492 | * @param {KeyboardEvent} e | |
493 | */ | |
494 | DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) { | |
a05ef130 HN |
495 | var drop = $(e.target).closest('.dropzone'); |
496 | if (drop.length === 0) { | |
497 | var placedDrag = $(e.target); | |
498 | var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace'); | |
499 | if (currentPlace !== null) { | |
500 | drop = this.getDrop(placedDrag, currentPlace); | |
501 | } | |
502 | } | |
503 | var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)), | |
ebf91776 TH |
504 | nextDrag = $(); |
505 | ||
506 | switch (e.keyCode) { | |
507 | case keys.space: | |
508 | case keys.arrowRight: | |
509 | case keys.arrowDown: | |
510 | nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag); | |
511 | break; | |
512 | ||
513 | case keys.arrowLeft: | |
514 | case keys.arrowUp: | |
515 | nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag); | |
516 | break; | |
517 | ||
518 | case keys.escape: | |
81d7fd7a | 519 | questionManager.isKeyboardNavigation = false; |
ebf91776 TH |
520 | break; |
521 | ||
522 | default: | |
a05ef130 | 523 | questionManager.isKeyboardNavigation = false; |
ebf91776 TH |
524 | return; // To avoid the preventDefault below. |
525 | } | |
526 | ||
a05ef130 HN |
527 | if (nextDrag.length) { |
528 | nextDrag.data('isfocus', true); | |
529 | nextDrag.addClass('beingdragged'); | |
530 | var hiddenDrag = this.getDragClone(nextDrag); | |
531 | if (hiddenDrag.length) { | |
532 | if (nextDrag.hasClass('infinite')) { | |
533 | var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag)); | |
534 | var cloneDrags = this.getInfiniteDragClones(nextDrag, false); | |
535 | if (cloneDrags.length < noOfDrags) { | |
536 | var cloneDrag = nextDrag.clone(); | |
537 | cloneDrag.removeClass('beingdragged'); | |
538 | cloneDrag.removeAttr('tabindex'); | |
539 | hiddenDrag.after(cloneDrag); | |
5fa8f1c8 | 540 | questionManager.addEventHandlersToDrag(cloneDrag); |
a05ef130 HN |
541 | nextDrag.offset(cloneDrag.offset()); |
542 | } else { | |
543 | hiddenDrag.addClass('active'); | |
544 | nextDrag.offset(hiddenDrag.offset()); | |
545 | } | |
546 | } else { | |
547 | hiddenDrag.addClass('active'); | |
548 | nextDrag.offset(hiddenDrag.offset()); | |
549 | } | |
550 | } | |
551 | } else { | |
552 | drop.data('isfocus', true); | |
553 | } | |
554 | ||
ebf91776 TH |
555 | e.preventDefault(); |
556 | this.sendDragToDrop(nextDrag, drop); | |
557 | }; | |
558 | ||
559 | /** | |
560 | * Choose the next drag in a group. | |
561 | * | |
562 | * @param {int} group which group. | |
563 | * @param {jQuery} drag current choice (empty jQuery if there isn't one). | |
564 | * @return {jQuery} the next drag in that group, or null if there wasn't one. | |
565 | */ | |
566 | DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) { | |
567 | var choice, | |
568 | numChoices = this.noOfChoicesInGroup(group); | |
569 | ||
570 | if (drag.length === 0) { | |
571 | choice = 1; // Was empty, so we want to select the first choice. | |
572 | } else { | |
573 | choice = this.getChoice(drag) + 1; | |
574 | } | |
575 | ||
576 | var next = this.getUnplacedChoice(group, choice); | |
577 | while (next.length === 0 && choice < numChoices) { | |
578 | choice++; | |
579 | next = this.getUnplacedChoice(group, choice); | |
580 | } | |
581 | ||
582 | return next; | |
583 | }; | |
584 | ||
585 | /** | |
586 | * Choose the previous drag in a group. | |
587 | * | |
588 | * @param {int} group which group. | |
589 | * @param {jQuery} drag current choice (empty jQuery if there isn't one). | |
590 | * @return {jQuery} the next drag in that group, or null if there wasn't one. | |
591 | */ | |
592 | DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) { | |
593 | var choice; | |
594 | ||
595 | if (drag.length === 0) { | |
596 | choice = this.noOfChoicesInGroup(group); | |
597 | } else { | |
598 | choice = this.getChoice(drag) - 1; | |
599 | } | |
600 | ||
601 | var previous = this.getUnplacedChoice(group, choice); | |
602 | while (previous.length === 0 && choice > 1) { | |
603 | choice--; | |
604 | previous = this.getUnplacedChoice(group, choice); | |
605 | } | |
606 | ||
607 | // Does this choice exist? | |
608 | return previous; | |
609 | }; | |
610 | ||
611 | /** | |
612 | * Animate an object to the given destination. | |
613 | * | |
614 | * @param {jQuery} drag the element to be animated. | |
615 | * @param {jQuery} target element marking the place to move it to. | |
616 | */ | |
617 | DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) { | |
618 | var currentPos = drag.offset(), | |
a05ef130 HN |
619 | targetPos = target.offset(), |
620 | thisQ = this; | |
ebf91776 | 621 | |
a05ef130 | 622 | M.util.js_pending('qtype_ddimageortext-animate-' + thisQ.containerId); |
ebf91776 TH |
623 | // Animate works in terms of CSS position, whereas locating an object |
624 | // on the page works best with jQuery offset() function. So, to get | |
625 | // the right target position, we work out the required change in | |
626 | // offset() and then add that to the current CSS position. | |
627 | drag.animate( | |
628 | { | |
629 | left: parseInt(drag.css('left')) + targetPos.left - currentPos.left, | |
630 | top: parseInt(drag.css('top')) + targetPos.top - currentPos.top | |
631 | }, | |
632 | { | |
633 | duration: 'fast', | |
634 | done: function() { | |
5fa8f1c8 | 635 | $('body').trigger('qtype_ddimageortext-dragmoved', [drag, target, thisQ]); |
a05ef130 | 636 | M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId); |
ebf91776 TH |
637 | } |
638 | } | |
639 | ); | |
640 | }; | |
641 | ||
642 | /** | |
643 | * Detect if a point is inside a given DOM node. | |
644 | * | |
645 | * @param {Number} pageX the x position. | |
646 | * @param {Number} pageY the y position. | |
647 | * @param {jQuery} drop the node to check (typically a drop). | |
648 | * @return {boolean} whether the point is inside the node. | |
649 | */ | |
650 | DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) { | |
651 | var position = drop.offset(); | |
a05ef130 HN |
652 | if (drop.hasClass('draghome')) { |
653 | return pageX >= position.left && pageX < position.left + drop.outerWidth() | |
654 | && pageY >= position.top && pageY < position.top + drop.outerHeight(); | |
655 | } | |
ebf91776 TH |
656 | return pageX >= position.left && pageX < position.left + drop.width() |
657 | && pageY >= position.top && pageY < position.top + drop.height(); | |
658 | }; | |
659 | ||
660 | /** | |
661 | * Set the value of the hidden input for a place, to record what is currently there. | |
662 | * | |
663 | * @param {int} place which place to set the input value for. | |
664 | * @param {int} choice the value to set. | |
665 | */ | |
666 | DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) { | |
667 | this.getRoot().find('input.placeinput.place' + place).val(choice); | |
668 | }; | |
669 | ||
670 | /** | |
671 | * Get the outer div for this question. | |
672 | * | |
673 | * @returns {jQuery} containing that div. | |
674 | */ | |
675 | DragDropOntoImageQuestion.prototype.getRoot = function() { | |
676 | return $(document.getElementById(this.containerId)); | |
677 | }; | |
678 | ||
679 | /** | |
680 | * Get the img that is the background image. | |
681 | * @returns {jQuery} containing that img. | |
682 | */ | |
683 | DragDropOntoImageQuestion.prototype.bgImage = function() { | |
684 | return this.getRoot().find('img.dropbackground'); | |
685 | }; | |
686 | ||
687 | /** | |
688 | * Get drag home for a given choice. | |
689 | * | |
690 | * @param {int} group the group. | |
691 | * @param {int} choice the choice number. | |
692 | * @returns {jQuery} containing that div. | |
693 | */ | |
694 | DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) { | |
a05ef130 HN |
695 | if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) { |
696 | return this.getRoot().find('.dragitemgroup' + group + | |
697 | ' .draghome.infinite' + | |
698 | '.choice' + choice + | |
699 | '.group' + group); | |
700 | } | |
701 | return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice); | |
ebf91776 TH |
702 | }; |
703 | ||
704 | /** | |
705 | * Get an unplaced choice for a particular group. | |
706 | * | |
707 | * @param {int} group the group. | |
708 | * @param {int} choice the choice number. | |
709 | * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty. | |
710 | */ | |
711 | DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) { | |
a05ef130 | 712 | return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1); |
ebf91776 TH |
713 | }; |
714 | ||
715 | /** | |
716 | * Get the drag that is currently in a given place. | |
717 | * | |
718 | * @param {int} place the place number. | |
719 | * @return {jQuery} the current drag (or an empty jQuery if none). | |
720 | */ | |
721 | DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) { | |
a05ef130 | 722 | return this.getRoot().find('.ddarea .draghome.inplace' + place); |
ebf91776 TH |
723 | }; |
724 | ||
725 | /** | |
726 | * Return the number of blanks in a given group. | |
727 | * | |
728 | * @param {int} group the group number. | |
729 | * @returns {int} the number of drops. | |
730 | */ | |
731 | DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) { | |
732 | return this.getRoot().find('.dropzone.group' + group).length; | |
733 | }; | |
734 | ||
735 | /** | |
736 | * Return the number of choices in a given group. | |
737 | * | |
738 | * @param {int} group the group number. | |
739 | * @returns {int} the number of choices. | |
740 | */ | |
741 | DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) { | |
742 | return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length; | |
743 | }; | |
744 | ||
745 | /** | |
746 | * Return the number at the end of the CSS class name with the given prefix. | |
747 | * | |
748 | * @param {jQuery} node | |
749 | * @param {String} prefix name prefix | |
750 | * @returns {Number|null} the suffix if found, else null. | |
751 | */ | |
752 | DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) { | |
753 | var classes = node.attr('class'); | |
754 | if (classes !== '') { | |
755 | var classesArr = classes.split(' '); | |
756 | for (var index = 0; index < classesArr.length; index++) { | |
757 | var patt1 = new RegExp('^' + prefix + '([0-9])+$'); | |
758 | if (patt1.test(classesArr[index])) { | |
759 | var patt2 = new RegExp('([0-9])+$'); | |
760 | var match = patt2.exec(classesArr[index]); | |
761 | return Number(match[0]); | |
762 | } | |
763 | } | |
764 | } | |
765 | return null; | |
766 | }; | |
767 | ||
768 | /** | |
769 | * Get the choice number of a drag. | |
770 | * | |
771 | * @param {jQuery} drag the drag. | |
772 | * @returns {Number} the choice number. | |
773 | */ | |
774 | DragDropOntoImageQuestion.prototype.getChoice = function(drag) { | |
775 | return this.getClassnameNumericSuffix(drag, 'choice'); | |
776 | }; | |
777 | ||
778 | /** | |
779 | * Given a DOM node that is significant to this question | |
780 | * (drag, drop, ...) get the group it belongs to. | |
781 | * | |
782 | * @param {jQuery} node a DOM node. | |
783 | * @returns {Number} the group it belongs to. | |
784 | */ | |
785 | DragDropOntoImageQuestion.prototype.getGroup = function(node) { | |
786 | return this.getClassnameNumericSuffix(node, 'group'); | |
787 | }; | |
788 | ||
789 | /** | |
790 | * Get the place number of a drop, or its corresponding hidden input. | |
791 | * | |
792 | * @param {jQuery} node the DOM node. | |
793 | * @returns {Number} the place number. | |
794 | */ | |
795 | DragDropOntoImageQuestion.prototype.getPlace = function(node) { | |
796 | return this.getClassnameNumericSuffix(node, 'place'); | |
797 | }; | |
798 | ||
a05ef130 HN |
799 | /** |
800 | * Get drag clone for a given drag. | |
801 | * | |
802 | * @param {jQuery} drag the drag. | |
803 | * @returns {jQuery} the drag's clone. | |
804 | */ | |
805 | DragDropOntoImageQuestion.prototype.getDragClone = function(drag) { | |
806 | return this.getRoot().find('.dragitemgroup' + | |
807 | this.getGroup(drag) + | |
808 | ' .draghome' + | |
809 | '.choice' + this.getChoice(drag) + | |
810 | '.group' + this.getGroup(drag) + | |
811 | '.dragplaceholder'); | |
812 | }; | |
813 | ||
814 | /** | |
815 | * Get infinite drag clones for given drag. | |
816 | * | |
817 | * @param {jQuery} drag the drag. | |
818 | * @param {Boolean} inHome in the home area or not. | |
819 | * @returns {jQuery} the drag's clones. | |
820 | */ | |
821 | DragDropOntoImageQuestion.prototype.getInfiniteDragClones = function(drag, inHome) { | |
822 | if (inHome) { | |
823 | return this.getRoot().find('.dragitemgroup' + | |
824 | this.getGroup(drag) + | |
825 | ' .draghome' + | |
826 | '.choice' + this.getChoice(drag) + | |
827 | '.group' + this.getGroup(drag) + | |
828 | '.infinite').not('.dragplaceholder'); | |
829 | } | |
830 | return this.getRoot().find('.draghome' + | |
831 | '.choice' + this.getChoice(drag) + | |
832 | '.group' + this.getGroup(drag) + | |
833 | '.infinite').not('.dragplaceholder'); | |
834 | }; | |
835 | ||
836 | /** | |
837 | * Get drop for a given drag and place. | |
838 | * | |
839 | * @param {jQuery} drag the drag. | |
840 | * @param {Integer} currentPlace the current place of drag. | |
841 | * @returns {jQuery} the drop's clone. | |
842 | */ | |
843 | DragDropOntoImageQuestion.prototype.getDrop = function(drag, currentPlace) { | |
844 | return this.getRoot().find('.dropzone.group' + this.getGroup(drag) + '.place' + currentPlace); | |
845 | }; | |
846 | ||
847 | /** | |
848 | * Handle when the window is resized. | |
849 | */ | |
850 | DragDropOntoImageQuestion.prototype.handleResize = function() { | |
851 | var thisQ = this, | |
852 | bgRatio = this.bgRatio(); | |
853 | if (this.isPrinting) { | |
854 | bgRatio = 1; | |
855 | } | |
856 | ||
857 | this.getRoot().find('.ddarea .dropzone').each(function(i, dropNode) { | |
858 | $(dropNode) | |
859 | .css('left', parseInt($(dropNode).data('originX')) * parseFloat(bgRatio)) | |
860 | .css('top', parseInt($(dropNode).data('originY')) * parseFloat(bgRatio)); | |
861 | thisQ.handleElementScale(dropNode, 'left top'); | |
862 | }); | |
863 | ||
864 | this.getRoot().find('div.droparea .draghome').not('.beingdragged').each(function(key, drag) { | |
865 | $(drag) | |
866 | .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio)) | |
867 | .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio)); | |
868 | thisQ.handleElementScale(drag, 'left top'); | |
869 | }); | |
870 | }; | |
871 | ||
872 | /** | |
873 | * Return the background ratio. | |
874 | * | |
875 | * @returns {number} Background ratio. | |
876 | */ | |
877 | DragDropOntoImageQuestion.prototype.bgRatio = function() { | |
878 | var bgImg = this.bgImage(); | |
879 | var bgImgNaturalWidth = bgImg.get(0).naturalWidth; | |
880 | var bgImgClientWidth = bgImg.width(); | |
881 | ||
882 | return bgImgClientWidth / bgImgNaturalWidth; | |
883 | }; | |
884 | ||
885 | /** | |
886 | * Scale the drag if needed. | |
887 | * | |
888 | * @param {jQuery} element the item to place. | |
889 | * @param {String} type scaling type | |
890 | */ | |
891 | DragDropOntoImageQuestion.prototype.handleElementScale = function(element, type) { | |
892 | var bgRatio = parseFloat(this.bgRatio()); | |
893 | if (this.isPrinting) { | |
894 | bgRatio = 1; | |
895 | } | |
896 | $(element).css({ | |
897 | '-webkit-transform': 'scale(' + bgRatio + ')', | |
898 | '-moz-transform': 'scale(' + bgRatio + ')', | |
899 | '-ms-transform': 'scale(' + bgRatio + ')', | |
900 | '-o-transform': 'scale(' + bgRatio + ')', | |
901 | 'transform': 'scale(' + bgRatio + ')', | |
902 | 'transform-origin': type | |
903 | }); | |
904 | }; | |
905 | ||
906 | /** | |
907 | * Calculate z-index value. | |
908 | * | |
909 | * @returns {number} z-index value | |
910 | */ | |
911 | DragDropOntoImageQuestion.prototype.calculateZIndex = function() { | |
912 | var zIndex = 0; | |
913 | this.getRoot().find('.ddarea .dropzone, div.droparea .draghome').each(function(i, dropNode) { | |
914 | dropNode = $(dropNode); | |
915 | // Note that webkit browsers won't return the z-index value from the CSS stylesheet | |
916 | // if the element doesn't have a position specified. Instead it'll return "auto". | |
917 | var itemZIndex = dropNode.css('z-index') ? parseInt(dropNode.css('z-index')) : 0; | |
918 | ||
919 | if (itemZIndex > zIndex) { | |
920 | zIndex = itemZIndex; | |
921 | } | |
922 | }); | |
923 | ||
924 | return zIndex; | |
925 | }; | |
926 | ||
dc293a2e HN |
927 | /** |
928 | * Check that the drag is drop to it's clone. | |
929 | * | |
930 | * @param {jQuery} drag The drag. | |
931 | * @param {jQuery} drop The drop. | |
932 | * @returns {boolean} | |
933 | */ | |
934 | DragDropOntoImageQuestion.prototype.isDragSameAsDrop = function(drag, drop) { | |
935 | return this.getChoice(drag) === this.getChoice(drop) && this.getGroup(drag) === this.getGroup(drop); | |
936 | }; | |
937 | ||
ebf91776 TH |
938 | /** |
939 | * Singleton object that handles all the DragDropOntoImageQuestions | |
940 | * on the page, and deals with event dispatching. | |
941 | * @type {Object} | |
942 | */ | |
943 | var questionManager = { | |
944 | ||
945 | /** | |
946 | * {boolean} ensures that the event handlers are only initialised once per page. | |
947 | */ | |
948 | eventHandlersInitialised: false, | |
949 | ||
b39f9824 HN |
950 | /** |
951 | * {Object} ensures that the drag event handlers are only initialised once per question, | |
952 | * indexed by containerId (id on the .que div). | |
953 | */ | |
954 | dragEventHandlersInitialised: {}, | |
955 | ||
a05ef130 HN |
956 | /** |
957 | * {boolean} is printing or not. | |
958 | */ | |
959 | isPrinting: false, | |
960 | ||
961 | /** | |
962 | * {boolean} is keyboard navigation or not. | |
963 | */ | |
964 | isKeyboardNavigation: false, | |
965 | ||
ebf91776 TH |
966 | /** |
967 | * {Object} all the questions on this page, indexed by containerId (id on the .que div). | |
968 | */ | |
969 | questions: {}, // An object containing all the information about each question on the page. | |
970 | ||
971 | /** | |
972 | * Initialise one question. | |
973 | * | |
4f422785 | 974 | * @method |
ebf91776 TH |
975 | * @param {String} containerId the id of the div.que that contains this question. |
976 | * @param {boolean} readOnly whether the question is read-only. | |
977 | * @param {Array} places data. | |
978 | */ | |
979 | init: function(containerId, readOnly, places) { | |
980 | questionManager.questions[containerId] = | |
981 | new DragDropOntoImageQuestion(containerId, readOnly, places); | |
982 | if (!questionManager.eventHandlersInitialised) { | |
983 | questionManager.setupEventHandlers(); | |
984 | questionManager.eventHandlersInitialised = true; | |
985 | } | |
b39f9824 HN |
986 | if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) { |
987 | questionManager.dragEventHandlersInitialised[containerId] = true; | |
988 | // We do not use the body event here to prevent the other event on Mobile device, such as scroll event. | |
989 | var questionContainer = document.getElementById(containerId); | |
990 | if (questionContainer.classList.contains('ddimageortext') && | |
991 | !questionContainer.classList.contains('qtype_ddimageortext-readonly')) { | |
992 | // TODO: Convert all the jQuery selectors and events to native Javascript. | |
993 | questionManager.addEventHandlersToDrag($(questionContainer).find('.draghome')); | |
994 | } | |
995 | } | |
ebf91776 TH |
996 | }, |
997 | ||
998 | /** | |
999 | * Set up the event handlers that make this question type work. (Done once per page.) | |
1000 | */ | |
1001 | setupEventHandlers: function() { | |
1002 | $('body') | |
ebf91776 TH |
1003 | .on('keydown', |
1004 | '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone', | |
a05ef130 HN |
1005 | questionManager.handleKeyPress) |
1006 | .on('keydown', | |
1007 | '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)', | |
1008 | questionManager.handleKeyPress) | |
5fa8f1c8 | 1009 | .on('qtype_ddimageortext-dragmoved', questionManager.handleDragMoved); |
a05ef130 HN |
1010 | $(window).on('resize', function() { |
1011 | questionManager.handleWindowResize(false); | |
1012 | }); | |
1013 | window.addEventListener('beforeprint', function() { | |
1014 | questionManager.isPrinting = true; | |
1015 | questionManager.handleWindowResize(questionManager.isPrinting); | |
1016 | }); | |
1017 | window.addEventListener('afterprint', function() { | |
1018 | questionManager.isPrinting = false; | |
1019 | questionManager.handleWindowResize(questionManager.isPrinting); | |
1020 | }); | |
1021 | setTimeout(function() { | |
1022 | questionManager.fixLayoutIfThingsMoved(); | |
1023 | }, 100); | |
ebf91776 TH |
1024 | }, |
1025 | ||
5fa8f1c8 HN |
1026 | /** |
1027 | * Binding the drag/touch event again for newly created element. | |
1028 | * | |
1029 | * @param {jQuery} element Element to bind the event | |
1030 | */ | |
1031 | addEventHandlersToDrag: function(element) { | |
dc293a2e HN |
1032 | // Unbind all the mousedown and touchstart events to prevent double binding. |
1033 | element.unbind('mousedown touchstart'); | |
5fa8f1c8 HN |
1034 | element.on('mousedown touchstart', questionManager.handleDragStart); |
1035 | }, | |
1036 | ||
ebf91776 TH |
1037 | /** |
1038 | * Handle mouse down / touch start events on drags. | |
1039 | * @param {Event} e the DOM event. | |
1040 | */ | |
1041 | handleDragStart: function(e) { | |
1042 | e.preventDefault(); | |
1043 | var question = questionManager.getQuestionForEvent(e); | |
1044 | if (question) { | |
1045 | question.handleDragStart(e); | |
1046 | } | |
1047 | }, | |
1048 | ||
1049 | /** | |
1050 | * Handle key down / press events on drags. | |
1051 | * @param {KeyboardEvent} e | |
1052 | */ | |
1053 | handleKeyPress: function(e) { | |
a05ef130 HN |
1054 | if (questionManager.isKeyboardNavigation) { |
1055 | return; | |
1056 | } | |
1057 | questionManager.isKeyboardNavigation = true; | |
ebf91776 TH |
1058 | var question = questionManager.getQuestionForEvent(e); |
1059 | if (question) { | |
1060 | question.handleKeyPress(e); | |
1061 | } | |
1062 | }, | |
1063 | ||
1064 | /** | |
1065 | * Handle when the window is resized. | |
a05ef130 | 1066 | * @param {boolean} isPrinting |
ebf91776 | 1067 | */ |
a05ef130 | 1068 | handleWindowResize: function(isPrinting) { |
ebf91776 TH |
1069 | for (var containerId in questionManager.questions) { |
1070 | if (questionManager.questions.hasOwnProperty(containerId)) { | |
a05ef130 HN |
1071 | questionManager.questions[containerId].isPrinting = isPrinting; |
1072 | questionManager.questions[containerId].handleResize(); | |
ebf91776 TH |
1073 | } |
1074 | } | |
1075 | }, | |
1076 | ||
e5153d93 TH |
1077 | /** |
1078 | * Sometimes, despite our best efforts, things change in a way that cannot | |
1079 | * be specifically caught (e.g. dock expanding or collapsing in Boost). | |
1080 | * Therefore, we need to periodically check everything is in the right position. | |
1081 | */ | |
1082 | fixLayoutIfThingsMoved: function() { | |
a05ef130 | 1083 | this.handleWindowResize(questionManager.isPrinting); |
e5153d93 TH |
1084 | // We use setTimeout after finishing work, rather than setInterval, |
1085 | // in case positioning things is slow. We want 100 ms gap | |
1086 | // between executions, not what setInterval does. | |
a05ef130 HN |
1087 | setTimeout(function() { |
1088 | questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting); | |
1089 | }, 100); | |
1090 | }, | |
1091 | ||
1092 | /** | |
1093 | * Handle when drag moved. | |
1094 | * | |
1095 | * @param {Event} e the event. | |
1096 | * @param {jQuery} drag the drag | |
1097 | * @param {jQuery} target the target | |
1098 | * @param {DragDropOntoImageQuestion} thisQ the question. | |
1099 | */ | |
1100 | handleDragMoved: function(e, drag, target, thisQ) { | |
1101 | drag.removeClass('beingdragged').css('z-index', ''); | |
1102 | drag.css('top', target.position().top).css('left', target.position().left); | |
1103 | target.after(drag); | |
1104 | target.removeClass('active'); | |
1105 | if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) { | |
1106 | drag.removeClass('placed').addClass('unplaced'); | |
1107 | drag.removeAttr('tabindex'); | |
1108 | drag.removeData('unplaced'); | |
1109 | drag.css('top', '') | |
1110 | .css('left', '') | |
1111 | .css('transform', ''); | |
1112 | if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) { | |
1113 | thisQ.getInfiniteDragClones(drag, true).first().remove(); | |
1114 | } | |
1115 | } else { | |
1116 | drag.data('originX', target.data('originX')).data('originY', target.data('originY')); | |
1117 | thisQ.handleElementScale(drag, 'left top'); | |
1118 | } | |
1119 | if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) { | |
1120 | drag.focus(); | |
1121 | drag.removeData('isfocus'); | |
1122 | } | |
1123 | if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) { | |
1124 | target.removeData('isfocus'); | |
1125 | } | |
1126 | if (questionManager.isKeyboardNavigation) { | |
1127 | questionManager.isKeyboardNavigation = false; | |
1128 | } | |
e5153d93 TH |
1129 | }, |
1130 | ||
ebf91776 TH |
1131 | /** |
1132 | * Given an event, work out which question it effects. | |
1133 | * @param {Event} e the event. | |
1134 | * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined. | |
1135 | */ | |
1136 | getQuestionForEvent: function(e) { | |
1137 | var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id'); | |
1138 | return questionManager.questions[containerId]; | |
1139 | } | |
1140 | }; | |
1141 | ||
1142 | /** | |
1143 | * @alias module:qtype_ddimageortext/question | |
1144 | */ | |
1145 | return { | |
ebf91776 TH |
1146 | init: questionManager.init |
1147 | }; | |
1148 | }); |