Merge branch 'MDL-70114-310' of git://github.com/bmbrands/moodle into MOODLE_310_STABLE
[moodle.git] / question / type / ddmarker / amd / src / form.js
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/>.
16 /**
17  * This class provides the enhancements to the drag-drop marker editing form.
18  *
19  * @package    qtype_ddmarker
20  * @subpackage form
21  * @copyright  2018 The Open University
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes'], function($, dragDrop, Shapes) {
27     "use strict";
29     /**
30      * Create the manager object that deals with keeping everything synchronised for one drop zone.
31      *
32      * @param {int} dropzoneNo the index of this drop zone in the form. 0, 1, ....
33      * @constructor
34      */
35     function DropZoneManager(dropzoneNo) {
36         this.dropzoneNo = dropzoneNo;
37         this.svgEl = null;
39         this.shape = Shapes.make(this.getShapeType(), this.getLabel());
40         this.updateCoordinatesFromForm();
41     }
43     /**
44      * Update the coordinates from a particular string.
45      *
46      * @param {SVGElement} [svg] the SVG element that is the preview.
47      */
48     DropZoneManager.prototype.updateCoordinatesFromForm = function(svg) {
49         var coordinates = this.getCoordinates(),
50             currentNumPoints = this.shape.getType() === 'polygon' && this.shape.points.length;
51         if (this.shape.getCoordinates() === coordinates) {
52             return;
53         }
54         // We don't need to scale the shape for editing form.
55         if (!this.shape.parse(coordinates, 1)) {
56             // Invalid coordinates. Don't update the preview.
57             return;
58         }
60         if (this.shape.getType() === 'polygon' && currentNumPoints !== this.shape.points.length) {
61             // Polygon, and size has changed.
62             var currentyActive = this.isActive();
63             this.removeFromSvg();
64             if (svg) {
65                 this.addToSvg(svg);
66                 if (currentyActive) {
67                     this.setActive();
68                 }
69             }
70         } else {
71             // Simple update.
72             this.updateSvgEl();
73         }
74         // Update the rounded coordinates if needed.
75         this.setCoordinatesInForm();
76     };
78     /**
79      * Update the label.
80      */
81     DropZoneManager.prototype.updateLabel = function() {
82         var label = this.getLabel();
83         if (this.shape.label !== label) {
84             this.shape.label = label;
85             this.updateSvgEl();
86         }
87     };
89     /**
90      * Handle if the type of shape has changed.
91      *
92      * @param {SVGElement} [svg] an SVG element to add this new shape to.
93      */
94     DropZoneManager.prototype.changeShape = function(svg) {
95         var newShapeType = this.getShapeType(),
96             currentyActive = this.isActive();
98         if (newShapeType === this.shape.getType()) {
99             return;
100         }
102         // It has really changed.
103         this.removeFromSvg();
104         this.shape = Shapes.getSimilar(newShapeType, this.shape);
105         if (svg) {
106             this.addToSvg(svg);
107             if (currentyActive) {
108                 this.setActive();
109             }
110         }
111         this.setCoordinatesInForm();
112     };
114     /**
115      * Add this drop zone to an SVG graphic.
116      *
117      * @param {SVGElement} svg the SVG image to which to add this drop zone.
118      */
119     DropZoneManager.prototype.addToSvg = function(svg) {
120         if (this.svgEl !== null) {
121             throw new Error('this.svgEl already set');
122         }
123         this.svgEl = this.shape.makeSvg(svg);
124         if (!this.svgEl) {
125             return;
126         }
127         this.svgEl.setAttribute('class', 'dropzone');
128         this.svgEl.setAttribute('data-dropzone-no', this.dropzoneNo);
130         // Add handles.
131         var handles = this.shape.getHandlePositions();
132         if (handles === null) {
133             return;
134         }
136         var moveHandle = Shapes.createSvgElement(this.svgEl, 'circle');
137         moveHandle.setAttribute('cx', handles.moveHandle.x);
138         moveHandle.setAttribute('cy', handles.moveHandle.y);
139         moveHandle.setAttribute('r', 7);
140         moveHandle.setAttribute('class', 'handle move');
142         for (var i = 0; i < handles.editHandles.length; ++i) {
143             this.makeEditHandle(i, handles.editHandles[i]);
144         }
145     };
147     /**
148      * Add a new edit handle.
149      *
150      * @param {int} index the handle index.
151      * @param {Point} point the point at which to add the handle.
152      */
153     DropZoneManager.prototype.makeEditHandle = function(index, point) {
154         var editHandle = Shapes.createSvgElement(this.svgEl, 'rect');
155         editHandle.setAttribute('x', point.x - 6);
156         editHandle.setAttribute('y', point.y - 6);
157         editHandle.setAttribute('width', 11);
158         editHandle.setAttribute('height', 11);
159         editHandle.setAttribute('class', 'handle edit');
160         editHandle.setAttribute('data-edit-handle-no', index);
161     };
163     /**
164      * Remove this drop zone from an SVG image.
165      */
166     DropZoneManager.prototype.removeFromSvg = function() {
167         if (this.svgEl !== null) {
168             this.svgEl.parentNode.removeChild(this.svgEl);
169             this.svgEl = null;
170         }
171     };
173     /**
174      * Update the shape of this drop zone (but not type) in an SVG image.
175      */
176     DropZoneManager.prototype.updateSvgEl = function() {
177         if (this.svgEl === null) {
178             return;
179         }
181         this.shape.updateSvg(this.svgEl);
183         // Adjust handles.
184         var handles = this.shape.getHandlePositions();
185         if (handles === null) {
186             return;
187         }
189         // Move handle.
190         // The shape + its label are the first two children of svgEl.
191         // Then come the move handle followed by the edit handles.
192         this.svgEl.childNodes[2].setAttribute('cx', handles.moveHandle.x);
193         this.svgEl.childNodes[2].setAttribute('cy', handles.moveHandle.y);
195         // Edit handles.
196         for (var i = 0; i < handles.editHandles.length; ++i) {
197             this.svgEl.childNodes[3 + i].setAttribute('x', handles.editHandles[i].x - 6);
198             this.svgEl.childNodes[3 + i].setAttribute('y', handles.editHandles[i].y - 6);
199         }
200     };
202     /**
203      * Find out of this drop zone is currently being edited.
204      *
205      * @return {boolean} true if it is.
206      */
207     DropZoneManager.prototype.isActive = function() {
208         return this.svgEl !== null && this.svgEl.getAttribute('class').match(/\bactive\b/);
209     };
211     /**
212      * Set this drop zone as being edited.
213      */
214     DropZoneManager.prototype.setActive = function() {
215         // Move this one to last, so that it is always on top.
216         // (Otherwise the handles may not be able to receive events.)
217         var parent = this.svgEl.parentNode;
218         parent.removeChild(this.svgEl);
219         parent.appendChild(this.svgEl);
220         this.svgEl.setAttribute('class', this.svgEl.getAttribute('class') + ' active');
221     };
223     /**
224      * Set the coordinates in the form to match the current shape.
225      */
226     DropZoneManager.prototype.setCoordinatesInForm = function() {
227         dragDropForm.form.setFormValue('drops', [this.dropzoneNo, 'coords'], this.shape.getCoordinates());
228     };
230     /**
231      * Returns the coordinates for a drop zone from the text input in the form.
232      * @returns {string} the coordinates.
233      */
234     DropZoneManager.prototype.getCoordinates = function() {
235         return dragDropForm.form.getFormValue('drops', [this.dropzoneNo, 'coords']).replace(/\s*/g, '');
236     };
238     /**
239      * Returns the selected marker number from the dropdown in the form.
240      * @returns {int} choice number.
241      */
242     DropZoneManager.prototype.getChoiceNo = function() {
243         return dragDropForm.form.getFormValue('drops', [this.dropzoneNo, 'choice']);
244     };
246     /**
247      * Returns the selected marker number from the dropdown in the form.
248      * @returns {String} marker label text.
249      */
250     DropZoneManager.prototype.getLabel = function() {
251         return dragDropForm.form.getMarkerText(this.getChoiceNo());
252     };
255     /**
256      * Returns the selected type of shape in the form.
257      * @returns {String} 'circle', 'rectangle' or 'polygon'.
258      */
259     DropZoneManager.prototype.getShapeType = function() {
260         return dragDropForm.form.getFormValue('drops', [this.dropzoneNo, 'shape']);
261     };
263     /**
264      * Start responding to dragging the move handle.
265      * @param {Event} e Event object
266      */
267     DropZoneManager.prototype.handleMove = function(e) {
268         var info = dragDrop.prepare(e);
269         if (!info.start) {
270             return;
271         }
273         var movingDropZone = this,
274                 lastX = info.x,
275                 lastY = info.y,
276                 dragProxy = this.makeDragProxy(info.x, info.y),
277                 bgImg = $('fieldset#id_previewareaheader .dropbackground'),
278                 maxX = bgImg.width(),
279                 maxY = bgImg.height();
281         dragDrop.start(e, $(dragProxy), function(pageX, pageY) {
282             movingDropZone.shape.move(pageX - lastX, pageY - lastY, maxX, maxY);
283             lastX = pageX;
284             lastY = pageY;
285             movingDropZone.updateSvgEl();
286             movingDropZone.setCoordinatesInForm();
287         }, function() {
288             document.body.removeChild(dragProxy);
289         });
290     };
292     /**
293      * Start responding to dragging the move handle.
294      * @param {Event} e Event object
295      * @param {int} handleIndex
296      * @param {SVGElement} [svg] an SVG element to add this new shape to.
297      */
298     DropZoneManager.prototype.handleEdit = function(e, handleIndex, svg) {
299         var info = dragDrop.prepare(e);
300         if (!info.start) {
301             return;
302         }
304         // For polygons, CTRL + drag adds a new point.
305         if (this.shape.getType() === 'polygon' && (e.ctrlKey || e.metaKey)) {
306             this.shape.addNewPointAfter(handleIndex);
307             this.removeFromSvg();
308             this.addToSvg(svg);
309             this.setActive();
310         }
312         var changingDropZone = this,
313             lastX = info.x,
314             lastY = info.y,
315             dragProxy = this.makeDragProxy(info.x, info.y),
316             bgImg = $('fieldset#id_previewareaheader .dropbackground'),
317             maxX = bgImg.width(),
318             maxY = bgImg.height();
320         dragDrop.start(e, $(dragProxy), function(pageX, pageY) {
321             changingDropZone.shape.edit(handleIndex, pageX - lastX, pageY - lastY, maxX, maxY);
322             lastX = pageX;
323             lastY = pageY;
324             changingDropZone.updateSvgEl();
325             changingDropZone.setCoordinatesInForm();
326         }, function() {
327             document.body.removeChild(dragProxy);
328             changingDropZone.shape.normalizeShape();
329             changingDropZone.updateSvgEl();
330             changingDropZone.setCoordinatesInForm();
331         });
332     };
334     /**
335      * Make an invisible drag proxy.
336      *
337      * @param {int} x x position .
338      * @param {int} y y position.
339      * @returns {HTMLElement} the drag proxy.
340      */
341     DropZoneManager.prototype.makeDragProxy = function(x, y) {
342         var dragProxy = document.createElement('div');
343         dragProxy.style.position = 'absolute';
344         dragProxy.style.top = y + 'px';
345         dragProxy.style.left = x + 'px';
346         dragProxy.style.width = '1px';
347         dragProxy.style.height = '1px';
348         document.body.appendChild(dragProxy);
349         return dragProxy;
350     };
352     /**
353      * Singleton object for managing all the parts of the form.
354      */
355     var dragDropForm = {
357         /**
358          * @var {object} for interacting with the file pickers.
359          */
360         fp: null, // Object containing functions associated with the file picker.
362         /**
363          * @var {int} the number of drop-zones on the form.
364          */
365         noDropZones: null,
367         /**
368          * @var {DropZoneManager[]} the drop zones in the preview, indexed by drop zone number.
369          */
370         dropZones: [],
372         /**
373          * Initialise the form.
374          */
375         init: function() {
376             dragDropForm.fp = dragDropForm.filePickers();
377             dragDropForm.noDropZones = dragDropForm.form.getFormValue('nodropzone', []);
378             dragDropForm.setupPreviewArea();
379             dragDropForm.setOptionsForDragItemSelectors();
380             dragDropForm.createShapes();
381             dragDropForm.setupEventHandlers();
382             dragDropForm.waitForFilePickerToInitialise();
383         },
385         /**
386          * Add html for the preview area.
387          */
388         setupPreviewArea: function() {
389             $('fieldset#id_previewareaheader div.fcontainer').append(
390                 '<div class="ddarea que ddmarker">' +
391                 '   <div id="ddm-droparea" class="droparea">' +
392                 '       <img class="dropbackground" />' +
393                 '       <div id="ddm-dropzone" class="dropzones">' +
394                 '       </div>' +
395                 '   </div>' +
396                 '</div>');
397         },
399         /**
400          * When a new marker is added this function updates the Marker dropdown controls in Drop zones.
401          */
402         setOptionsForDragItemSelectors: function() {
403             var dragItemsOptions = {'0': ''};
404             var noItems = dragDropForm.form.getFormValue('noitems', []);
405             var selectedValues = [];
406             var selector;
407             var i, label;
408             for (i = 1; i <= noItems; i++) {
409                 label = dragDropForm.form.getMarkerText(i);
410                 if (label !== "") {
411                     // HTML escape the label.
412                     dragItemsOptions[i] = $('<div/>').text(label).html();
413                 }
414             }
415             // Get all the currently selected drags for each drop.
416             for (i = 0; i < dragDropForm.noDropZones; i++) {
417                 selector = $('#id_drops_' + i + '_choice');
418                 selectedValues[i] = Number(selector.val());
419             }
420             for (i = 0; i < dragDropForm.noDropZones; i++) {
421                 selector = $('#id_drops_' + i + '_choice');
422                 // Remove all options for drag choice.
423                 selector.find('option').remove();
424                 // And recreate the options.
425                 for (var value in dragItemsOptions) {
426                     value = Number(value);
427                     var option = '<option value="' + value + '">' + dragItemsOptions[value] + '</option>';
428                     selector.append(option);
429                     var optionnode = selector.find('option[value="' + value + '"]');
432                     if (value === 0) {
433                         continue; // The 'no item' option is always selectable.
434                     }
436                     // Is this the currently selected value?
437                     if (value === selectedValues[i]) {
438                         optionnode.attr('selected', true);
439                         continue; // If it s selected, we must leave it enabled.
440                     }
442                     // Count how many times it is used, and if necessary, disable.
443                     var noofdrags = dragDropForm.form.getFormValue('drags', [value - 1, 'noofdrags']);
444                     if (Number(noofdrags) === 0) { // 'noofdrags === 0' means infinite.
445                         continue; // Nothing to check.
446                     }
448                     // Go through all selected values in drop downs.
449                     for (var k in selectedValues) {
450                         if (Number(selectedValues[k]) !== value) {
451                             continue;
452                         }
454                         // Count down 'noofdrags' and if reach zero then set disabled option for this drag item.
455                         if (Number(noofdrags) === 1) {
456                             optionnode.attr('disabled', true);
457                             break;
458                         } else {
459                             noofdrags--;
460                         }
461                     }
462                 }
464                 if (dragDropForm.dropZones.length > 0) {
465                     dragDropForm.dropZones[i].updateLabel();
466                 }
467             }
468         },
470         /**
471          * Create the shape representation of each dropZone.
472          */
473         createShapes: function() {
474             for (var dropzoneNo = 0; dropzoneNo < dragDropForm.noDropZones; dropzoneNo++) {
475                 dragDropForm.dropZones[dropzoneNo] = new DropZoneManager(dropzoneNo);
476             }
477         },
479         /**
480          * Events linked to form actions.
481          */
482         setupEventHandlers: function() {
483             // Changes to labels in the Markers section.
484             $('fieldset#id_draggableitemheader').on('change input', 'input, select', function() {
485                 dragDropForm.setOptionsForDragItemSelectors();
486             });
488             // Changes to Drop zones section: shape, coordinates and marker.
489             $('fieldset#id_dropzoneheader').on('change input', 'input, select', function(e) {
490                 var ids = e.currentTarget.name.match(/^drops\[(\d+)]\[([a-z]*)]$/);
491                 if (!ids) {
492                     return;
493                 }
495                 var dropzoneNo = ids[1],
496                     inputType = ids[2],
497                     dropZone = dragDropForm.dropZones[dropzoneNo];
499                 switch (inputType) {
500                     case 'shape':
501                         dropZone.changeShape(dragDropForm.form.getSvg());
502                         break;
504                     case 'coords':
505                         dropZone.updateCoordinatesFromForm(dragDropForm.form.getSvg());
506                         break;
508                     case 'choice':
509                         dropZone.updateLabel();
510                         break;
511                 }
512             });
514             // Click to toggle graphical editing.
515             var previewArea = $('fieldset#id_previewareaheader');
516             previewArea.on('click', 'g.dropzone', function(e) {
517                 var dropzoneNo = $(e.currentTarget).data('dropzone-no'),
518                     currentlyActive = dragDropForm.dropZones[dropzoneNo].isActive();
520                 $(dragDropForm.form.getSvg()).find('.dropzone.active').removeClass('active');
522                 if (!currentlyActive) {
523                     dragDropForm.dropZones[dropzoneNo].setActive();
524                 }
525             });
527             // Drag start on a move handle.
528             previewArea.on('mousedown touchstart', '.dropzone .handle.move', function(e) {
529                 var dropzoneNo = $(e.currentTarget).closest('g').data('dropzoneNo');
531                 dragDropForm.dropZones[dropzoneNo].handleMove(e);
532             });
534             // Drag start on a move handle.
535             previewArea.on('mousedown touchstart', '.dropzone .handle.edit', function(e) {
536                 var dropzoneNo = $(e.currentTarget).closest('g').data('dropzoneNo'),
537                     handleIndex = e.currentTarget.getAttribute('data-edit-handle-no');
539                 dragDropForm.dropZones[dropzoneNo].handleEdit(e, handleIndex, dragDropForm.form.getSvg());
540             });
541         },
543         /**
544          * Prevents adding drop zones until the preview background image is ready to load.
545          */
546         waitForFilePickerToInitialise: function() {
547             if (dragDropForm.fp.file('bgimage').href === null) {
548                 // It would be better to use an onload or onchange event rather than this timeout.
549                 // Unfortunately attempts to do this early are overwritten by filepicker during its loading.
550                 setTimeout(dragDropForm.waitForFilePickerToInitialise, 1000);
551                 return;
552             }
554             // From now on, when a new file gets loaded into the filepicker, update the preview.
555             // This is not in the setupEventHandlers section as it needs to be delayed until
556             // after filepicker's javascript has finished.
557             $('form.mform').on('change', '#id_bgimage', dragDropForm.loadPreviewImage);
559             dragDropForm.loadPreviewImage();
560         },
562         /**
563          * Loads the preview background image.
564          */
565         loadPreviewImage: function() {
566             $('fieldset#id_previewareaheader .dropbackground')
567                     .one('load', dragDropForm.afterPreviewImageLoaded)
568                     .attr('src', dragDropForm.fp.file('bgimage').href);
569         },
571         /**
572          * Functions to run after background image loaded.
573          */
574         afterPreviewImageLoaded: function() {
575             var bgImg = $('fieldset#id_previewareaheader .dropbackground');
576             // Place the dropzone area over the background image (adding one to account for the border).
577             $('#ddm-dropzone').css('position', 'relative').css('top', (bgImg.height() + 1) * -1);
578             $('#ddm-droparea').css('height', bgImg.height() + 20);
579             dragDropForm.updateSvgDisplay();
580         },
582         /**
583          * Draws or re-draws all dropzones in the preview area based on form data.
584          * Call this function when there is a change in the form data.
585          */
586         updateSvgDisplay: function() {
587             var bgImg = $('fieldset#id_previewareaheader .dropbackground'),
588                 dropzoneNo;
590             if (dragDropForm.form.getSvg()) {
591                 // Already exists, just need to be updated.
592                 for (dropzoneNo = 0; dropzoneNo < dragDropForm.noDropZones; dropzoneNo++) {
593                     dragDropForm.dropZones[dropzoneNo].updateSvgEl();
594                 }
596             } else {
597                 // Create.
598                 $('#ddm-dropzone').html('<svg xmlns="http://www.w3.org/2000/svg" class="dropzones" ' +
599                     'width="' + bgImg.outerWidth() + '" ' +
600                     'height="' + bgImg.outerHeight() + '"></svg>');
601                 for (dropzoneNo = 0; dropzoneNo < dragDropForm.noDropZones; dropzoneNo++) {
602                     dragDropForm.dropZones[dropzoneNo].addToSvg(dragDropForm.form.getSvg());
603                 }
604             }
605         },
607         /**
608          * Helper to make it easy to work with form elements with names like "drops[0][shape]".
609          */
610         form: {
611             /**
612              * Returns the label text for a marker.
613              * @param {int} markerNo
614              * @returns {string} Marker text
615              */
616             getMarkerText: function(markerNo) {
617                 if (Number(markerNo) !== 0) {
618                     var label = dragDropForm.form.getFormValue('drags', [markerNo - 1, 'label']);
619                     return label.replace(new RegExp("^\\s*(.*)\\s*$"), "$1");
620                 } else {
621                     return '';
622                 }
623             },
625             /**
626              * Get the SVG element, if there is one, otherwise return null.
627              *
628              * @returns {SVGElement|null} the SVG element or null.
629              */
630             getSvg: function() {
631                 var svg = $('fieldset#id_previewareaheader svg');
632                 if (svg.length === 0) {
633                     return null;
634                 } else {
635                     return svg[0];
636                 }
637             },
639             toNameWithIndex: function(name, indexes) {
640                 var indexString = name;
641                 for (var i = 0; i < indexes.length; i++) {
642                     indexString = indexString + '[' + indexes[i] + ']';
643                 }
644                 return indexString;
645             },
647             getEl: function(name, indexes) {
648                 var form = $('form.mform')[0];
649                 return form.elements[this.toNameWithIndex(name, indexes)];
650             },
652             /**
653              * Helper to get the value of a form elements with name like "drops[0][shape]".
654              *
655              * @param {String} name the base name, e.g. 'drops'.
656              * @param {String[]} indexes the indexes, e.g. ['0', 'shape'].
657              * @return {String} the value of that field.
658              */
659             getFormValue: function(name, indexes) {
660                 var el = this.getEl(name, indexes);
661                 if (el.type === 'checkbox') {
662                     return el.checked;
663                 } else {
664                     return el.value;
665                 }
666             },
668             /**
669              * Helper to get the value of a form elements with name like "drops[0][shape]".
670              *
671              * @param {String} name the base name, e.g. 'drops'.
672              * @param {String[]} indexes the indexes, e.g. ['0', 'shape'].
673              * @param {String} value the value to set.
674              */
675             setFormValue: function(name, indexes, value) {
676                 var el = this.getEl(name, indexes);
677                 if (el.type === 'checkbox') {
678                     el.checked = value;
679                 } else {
680                     el.value = value;
681                 }
682             }
683         },
685         /**
686          * Utility to get the file name and url from the filepicker.
687          * @returns {Object} object containing functions {file, name}
688          */
689         filePickers: function() {
690             var draftItemIdsToName;
691             var nameToParentNode;
692             if (draftItemIdsToName === undefined) {
693                 draftItemIdsToName = {};
694                 nameToParentNode = {};
695                 $('form.mform input.filepickerhidden').each(function(key, filepicker) {
696                     draftItemIdsToName[filepicker.value] = filepicker.name;
697                     nameToParentNode[filepicker.name] = filepicker.parentNode;
698                 });
699             }
700             return {
701                 file: function(name) {
702                     var fileAnchor = $(nameToParentNode[name]).find('div.filepicker-filelist a');
703                     if (fileAnchor.length) {
704                         return {href: fileAnchor.get(0).href, name: fileAnchor.get(0).innerHTML};
705                     } else {
706                         return {href: null, name: null};
707                     }
708                 },
709                 name: function(draftitemid) {
710                     return draftItemIdsToName[draftitemid];
711                 }
712             };
713         }
714     };
716     /**
717      * @alias module:qtype_ddmarker/form
718      */
719     return {
720         /**
721          * Initialise the form javascript features.
722          * @param {Object} maxBgimageSize object with two properties: width and height.
723          */
724         init: dragDropForm.init
725     };
726 });