Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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