From a05ef1307e073e9ef42feb01db87312050328f93 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Fri, 24 Apr 2020 02:27:11 +0700 Subject: [PATCH] MDL-58645 Drag and drop onto image questions: responsive support --- question/type/ddimageortext/amd/build/form.min.js | 4 +- .../type/ddimageortext/amd/build/form.min.js.map | 2 +- .../type/ddimageortext/amd/build/question.min.js | 4 +- .../ddimageortext/amd/build/question.min.js.map | 2 +- question/type/ddimageortext/amd/src/form.js | 2 +- question/type/ddimageortext/amd/src/question.js | 436 ++++++++++++++++++--- question/type/ddimageortext/rendererbase.php | 58 ++- question/type/ddimageortext/styles.css | 64 ++- .../tests/behat/behat_qtype_ddimageortext.php | 5 +- 9 files changed, 466 insertions(+), 111 deletions(-) rewrite question/type/ddimageortext/amd/build/form.min.js (95%) rewrite question/type/ddimageortext/amd/build/question.min.js (96%) rewrite question/type/ddimageortext/amd/build/question.min.js.map (98%) diff --git a/question/type/ddimageortext/amd/build/form.min.js b/question/type/ddimageortext/amd/build/form.min.js dissimilarity index 95% index c603b2ea976..5a60adc8ea1 100644 --- a/question/type/ddimageortext/amd/build/form.min.js +++ b/question/type/ddimageortext/amd/build/form.min.js @@ -1,2 +1,2 @@ -define ("qtype_ddimageortext/form",["jquery","core/dragdrop"],function(a,b){"use strict";var c={maxBgImageSize:null,maxDragImageSize:null,fp:null,init:function init(b,d){c.maxBgImageSize=b;c.maxDragImageSize=d;c.fp=c.filePickers();a("#id_previewareaheader").append("
");c.updateVisibilityOfFilePickers();c.setOptionsForDragItemSelectors();c.setupEventHandlers();c.waitForFilePickerToInitialise()},waitForFilePickerToInitialise:function waitForFilePickerToInitialise(){if(null===c.fp.file("bgimage").href){setTimeout(c.waitForFilePickerToInitialise,1e3);return}M.util.js_pending("dragDropToImageForm");a("form.mform").on("change",".filepickerhidden",function(){M.util.js_pending("dragDropToImageForm");c.loadPreviewImage()});c.loadPreviewImage()},loadPreviewImage:function loadPreviewImage(){a("fieldset#id_previewareaheader .dropbackground").one("load",c.afterPreviewImageLoaded).attr("src",c.fp.file("bgimage").href)},afterPreviewImageLoaded:function afterPreviewImageLoaded(){var b=a("fieldset#id_previewareaheader .dropbackground");c.constrainImageSize(b,c.maxBgImageSize);c.createDropZones();M.util.js_complete("dragDropToImageForm")},constrainImageSize:function constrainImageSize(a,b){var c=Math.max(a.width()/b.width,a.height()/b.height);if(1")}else if(""!==i){b.append("
"+i+"
")}}c.waitForAllDropImagesToBeLoaded()},waitForAllDropImagesToBeLoaded:function waitForAllDropImagesToBeLoaded(){var b=a(".dropzones img").not(function(a,b){return c.imageIsLoaded(b)});if(0"+b[l]+"");var m=j.find("option[value=\""+l+"\"]");if(parseInt(l)===parseInt(k)){m.attr("selected",!0)}else if(c.isItemUsed(parseInt(l))){m.attr("disabled",!0)}}}},isItemUsed:function isItemUsed(b){if(0===b){return!1}if(c.form.getFormValue("drags",[b-1,"infinite"])){return!1}return 0!==a("fieldset#id_dropzoneheader select").filter(function(c,d){return parseInt(a(d).val())===b}).length},dragStart:function dragStart(d){var e=a(d.target).closest(".droppreview"),f=b.prepare(d);if(!f.start){return}b.start(d,e,function(a,b,d){c.dragMove(d)},function(){c.dragEnd()})},dragMove:function dragMove(b){var d=a("fieldset#id_previewareaheader .dropbackground"),e=d.offset(),f=b.data("dropNo"),g=b.offset(),h=Math.round(g.left-e.left),i=Math.round(g.top-e.top);h=Math.max(0,Math.min(h,d.width()-b.width()-10));i=Math.max(0,Math.min(i,d.height()-b.height()-10));c.form.setFormValue("drops",[f,"xleft"],h);c.form.setFormValue("drops",[f,"ytop"],i)},dragEnd:function dragEnd(){c.updateDropZones()},form:{toNameWithIndex:function toNameWithIndex(a,b){for(var c=a,d=0;d
");c.updateVisibilityOfFilePickers();c.setOptionsForDragItemSelectors();c.setupEventHandlers();c.waitForFilePickerToInitialise()},waitForFilePickerToInitialise:function waitForFilePickerToInitialise(){if(null===c.fp.file("bgimage").href){setTimeout(c.waitForFilePickerToInitialise,1e3);return}M.util.js_pending("dragDropToImageForm");a("form.mform").on("change",".filepickerhidden",function(){M.util.js_pending("dragDropToImageForm");c.loadPreviewImage()});c.loadPreviewImage()},loadPreviewImage:function loadPreviewImage(){a("fieldset#id_previewareaheader .dropbackground").one("load",c.afterPreviewImageLoaded).attr("src",c.fp.file("bgimage").href)},afterPreviewImageLoaded:function afterPreviewImageLoaded(){var b=a("fieldset#id_previewareaheader .dropbackground");c.constrainImageSize(b,c.maxBgImageSize);c.createDropZones();M.util.js_complete("dragDropToImageForm")},constrainImageSize:function constrainImageSize(a,b){var c=Math.max(a.width()/b.width,a.height()/b.height);if(1")}else if(""!==i){b.append("
"+i+"
")}}c.waitForAllDropImagesToBeLoaded()},waitForAllDropImagesToBeLoaded:function waitForAllDropImagesToBeLoaded(){var b=a(".dropzones img").not(function(a,b){return c.imageIsLoaded(b)});if(0"+b[l]+"");var m=j.find("option[value=\""+l+"\"]");if(parseInt(l)===parseInt(k)){m.attr("selected",!0)}else if(c.isItemUsed(parseInt(l))){m.attr("disabled",!0)}}}},isItemUsed:function isItemUsed(b){if(0===b){return!1}if(c.form.getFormValue("drags",[b-1,"infinite"])){return!1}return 0!==a("fieldset#id_dropzoneheader select").filter(function(c,d){return parseInt(a(d).val())===b}).length},dragStart:function dragStart(d){var e=a(d.target).closest(".droppreview"),f=b.prepare(d);if(!f.start){return}b.start(d,e,function(a,b,d){c.dragMove(d)},function(){c.dragEnd()})},dragMove:function dragMove(b){var d=a("fieldset#id_previewareaheader .dropbackground"),e=d.offset(),f=b.data("dropNo"),g=b.offset(),h=Math.round(g.left-e.left),i=Math.round(g.top-e.top);h=Math.max(0,Math.min(h,d.width()-b.width()-10));i=Math.max(0,Math.min(i,d.height()-b.height()-10));c.form.setFormValue("drops",[f,"xleft"],h);c.form.setFormValue("drops",[f,"ytop"],i)},dragEnd:function dragEnd(){c.updateDropZones()},form:{toNameWithIndex:function toNameWithIndex(a,b){for(var c=a,d=0;d.\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/form\n * @package qtype_ddimageortext\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/dragdrop'], function($, dragDrop) {\n\n \"use strict\";\n\n /**\n * Singleton object to handle progressive enhancement of the\n * drag-drop onto image question editing form.\n * @type {Object}\n */\n var dragDropToImageForm = {\n /**\n * @var {Object} with properties width and height.\n */\n maxBgImageSize: null,\n\n /**\n * @var {Object} with properties width and height.\n */\n maxDragImageSize: null,\n\n /**\n * @var {object} for interacting with the file pickers.\n */\n fp: null, // Object containing functions associated with the file picker.\n\n /**\n * Initialise the form javascript features.\n *\n * @param {Object} maxBgImageSize object with two properties: width and height.\n * @param {Object} maxDragImageSize object with two properties: width and height.\n */\n init: function(maxBgImageSize, maxDragImageSize) {\n dragDropToImageForm.maxBgImageSize = maxBgImageSize;\n dragDropToImageForm.maxDragImageSize = maxDragImageSize;\n dragDropToImageForm.fp = dragDropToImageForm.filePickers();\n\n $('#id_previewareaheader').append(\n '
' +\n '
' +\n ' ' +\n '
' +\n '
' +\n '
' +\n '
');\n\n dragDropToImageForm.updateVisibilityOfFilePickers();\n dragDropToImageForm.setOptionsForDragItemSelectors();\n dragDropToImageForm.setupEventHandlers();\n dragDropToImageForm.waitForFilePickerToInitialise();\n },\n\n /**\n * Waits for the file-pickers to be sufficiently ready before initialising the preview.\n */\n waitForFilePickerToInitialise: function() {\n if (dragDropToImageForm.fp.file('bgimage').href === null) {\n // It would be better to use an onload or onchange event rather than this timeout.\n // Unfortunately attempts to do this early are overwritten by filepicker during its loading.\n setTimeout(dragDropToImageForm.waitForFilePickerToInitialise, 1000);\n return;\n }\n M.util.js_pending('dragDropToImageForm');\n\n // From now on, when a new file gets loaded into the filepicker, update the preview.\n // This is not in the setupEventHandlers section as it needs to be delayed until\n // after filepicker's javascript has finished.\n $('form.mform').on('change', '.filepickerhidden', function() {\n M.util.js_pending('dragDropToImageForm');\n dragDropToImageForm.loadPreviewImage();\n });\n\n dragDropToImageForm.loadPreviewImage();\n },\n\n /**\n * Loads the preview background image.\n */\n loadPreviewImage: function() {\n $('fieldset#id_previewareaheader .dropbackground')\n .one('load', dragDropToImageForm.afterPreviewImageLoaded)\n .attr('src', dragDropToImageForm.fp.file('bgimage').href);\n },\n\n /**\n * After the background image is loaded, continue setting up the preview.\n */\n afterPreviewImageLoaded: function() {\n var bgImg = $('fieldset#id_previewareaheader .dropbackground');\n dragDropToImageForm.constrainImageSize(bgImg, dragDropToImageForm.maxBgImageSize);\n dragDropToImageForm.createDropZones();\n M.util.js_complete('dragDropToImageForm');\n },\n\n /**\n * Limits an image display size to the given maximums.\n *\n * @param {jQuery} img the image.\n * @param {Object} maxSize with width and height properties.\n */\n constrainImageSize: function(img, maxSize) {\n var reduceby = Math.max(img.width() / maxSize.width,\n img.height() / maxSize.height);\n if (reduceby > 1) {\n img.css('width', Math.floor(img.width() / reduceby));\n }\n img.addClass('constrained');\n },\n\n /**\n * Create, or recreate all the drop zones.\n */\n createDropZones: function() {\n var dropZoneHolder = $('.dropzones');\n dropZoneHolder.empty();\n\n var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;\n if (bgimageurl === null) {\n return; // There is not currently a valid preview to update.\n }\n\n var numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']);\n if (dragNo === '0') {\n continue;\n }\n dragNo = dragNo - 1;\n var group = dragDropToImageForm.form.getFormValue('drags', [dragNo, 'draggroup']),\n label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {\n var imgUrl = dragDropToImageForm.fp.file('dragitem[' + dragNo + ']').href;\n if (imgUrl === null) {\n continue;\n }\n // Althoug these are previews of drops, we also add the class name 'drag',\n dropZoneHolder.append('\"'');\n\n } else if (label !== '') {\n dropZoneHolder.append('
' + label + '
');\n }\n }\n\n dragDropToImageForm.waitForAllDropImagesToBeLoaded();\n },\n\n /**\n * This polls until all the drop-zone images have loaded, and then calls updateDropZones().\n */\n waitForAllDropImagesToBeLoaded: function() {\n var notYetLoadedImages = $('.dropzones img').not(function(i, imgNode) {\n return dragDropToImageForm.imageIsLoaded(imgNode);\n });\n\n if (notYetLoadedImages.length > 0) {\n setTimeout(function() {\n dragDropToImageForm.waitForAllDropImagesToBeLoaded();\n }, 100);\n return;\n }\n\n dragDropToImageForm.updateDropZones();\n },\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n imageIsLoaded: function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n },\n\n /**\n * Set the size and position of all the drop zones.\n */\n updateDropZones: function() {\n var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;\n if (bgimageurl === null) {\n return; // There is not currently a valid preview to update.\n }\n\n var dropBackgroundPosition = $('fieldset#id_previewareaheader .dropbackground').offset(),\n numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n\n // Move each drop to the right position and update the text.\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var drop = $('.dropzones .drop' + dropNo);\n if (drop.length === 0) {\n continue;\n }\n var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']) - 1;\n\n drop.offset({\n left: dropBackgroundPosition.left +\n parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'xleft'])),\n top: dropBackgroundPosition.top +\n parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'ytop']))\n });\n\n var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n if (drop.is('img')) {\n drop.attr('alt', label);\n } else {\n drop.html(label);\n }\n }\n\n // Resize them to the same size.\n $('.dropzones .droppreview').css('padding', '0');\n var numGroups = $('select.draggroup').first().find('option').length;\n for (var group = 1; group <= numGroups; group++) {\n dragDropToImageForm.resizeAllDragsAndDropsInGroup(group);\n }\n },\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n resizeAllDragsAndDropsInGroup: function(group) {\n var drops = $('.dropzones .droppreview.group' + group),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n drops.each(function(i, drop) {\n maxWidth = Math.max(maxWidth, Math.ceil(drop.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drop.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n drops.each(function(i, drop) {\n var left = Math.round((maxWidth - drop.offsetWidth) / 2),\n top = Math.floor((maxHeight - drop.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drop).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drop.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drop.offsetHeight - top) + 'px'\n });\n });\n },\n\n /**\n * Events linked to form actions.\n */\n setupEventHandlers: function() {\n // Changes to settings in the draggable items section.\n $('fieldset#id_draggableitemheader')\n .on('change input', 'input, select', function(e) {\n var input = $(e.target).closest('select, input');\n if (input.hasClass('dragitemtype')) {\n dragDropToImageForm.updateVisibilityOfFilePickers();\n }\n\n dragDropToImageForm.setOptionsForDragItemSelectors();\n\n if (input.is('.dragitemtype, .draggroup')) {\n dragDropToImageForm.createDropZones();\n } else if (input.is('.draglabel')) {\n dragDropToImageForm.updateDropZones();\n }\n });\n\n // Changes to Drop zones section: left, top and drag item.\n $('fieldset#id_dropzoneheader').on('change input', 'input, select', function(e) {\n var input = $(e.target).closest('select, input');\n if (input.is('select')) {\n dragDropToImageForm.createDropZones();\n } else {\n dragDropToImageForm.updateDropZones();\n }\n });\n\n // Moving drop zones in the preview.\n $('fieldset#id_previewareaheader').on('mousedown touchstart', '.droppreview', function(e) {\n dragDropToImageForm.dragStart(e);\n });\n\n $(window).on('resize', function() {\n dragDropToImageForm.updateDropZones();\n });\n },\n\n /**\n * Update all the drag item filepickers, so they are only shown for\n */\n updateVisibilityOfFilePickers: function() {\n var numDrags = dragDropToImageForm.form.getFormValue('noitems', []);\n for (var dragNo = 0; dragNo < numDrags; dragNo++) {\n var picker = $('input#id_dragitem_' + dragNo).closest('.fitem_ffilepicker');\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {\n picker.show();\n } else {\n picker.hide();\n }\n }\n },\n\n\n setOptionsForDragItemSelectors: function() {\n var dragItemOptions = {'0': ''},\n numDrags = dragDropToImageForm.form.getFormValue('noitems', []),\n numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n\n // Work out the list of options.\n for (var dragNo = 0; dragNo < numDrags; dragNo++) {\n var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n var file = dragDropToImageForm.fp.file(dragDropToImageForm.form.toNameWithIndex('dragitem', [dragNo]));\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype']) && file.name !== null) {\n dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label + ' (' + file.name + ')';\n } else if (label !== '') {\n dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label;\n }\n }\n\n // Initialise each select.\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var selector = $('#id_drops_' + dropNo + '_choice');\n\n var selectedvalue = selector.val();\n selector.find('option').remove();\n for (var value in dragItemOptions) {\n if (!dragItemOptions.hasOwnProperty(value)) {\n continue;\n }\n selector.append('');\n var optionnode = selector.find('option[value=\"' + value + '\"]');\n if (parseInt(value) === parseInt(selectedvalue)) {\n optionnode.attr('selected', true);\n } else if (dragDropToImageForm.isItemUsed(parseInt(value))) {\n optionnode.attr('disabled', true);\n }\n }\n }\n },\n\n /**\n * Checks if the specified drag option is already used somewhere.\n *\n * @param {Number} value of the drag item to check\n * @return {Boolean} true if item is allocated to dropzone\n */\n isItemUsed: function(value) {\n if (value === 0) {\n return false; // None option can always be selected.\n }\n\n if (dragDropToImageForm.form.getFormValue('drags', [value - 1, 'infinite'])) {\n return false; // Infinite, so can't be used up.\n }\n\n return $('fieldset#id_dropzoneheader select').filter(function(i, selectNode) {\n return parseInt($(selectNode).val()) === value;\n }).length !== 0;\n },\n\n /**\n * Handles when a dropzone in dragged in the preview.\n * @param {Object} e Event object\n */\n dragStart: function(e) {\n var drop = $(e.target).closest('.droppreview');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragDrop.start(e, drop, function(x, y, drop) {\n dragDropToImageForm.dragMove(drop);\n }, function() {\n dragDropToImageForm.dragEnd();\n });\n },\n\n /**\n * Handles update while a drop is being dragged.\n *\n * @param {jQuery} drop the drop preview being moved.\n */\n dragMove: function(drop) {\n var backgroundImage = $('fieldset#id_previewareaheader .dropbackground'),\n backgroundPosition = backgroundImage.offset(),\n dropNo = drop.data('dropNo'),\n dropPosition = drop.offset(),\n left = Math.round(dropPosition.left - backgroundPosition.left),\n top = Math.round(dropPosition.top - backgroundPosition.top);\n\n // Constrain coordinates to be inside the background.\n // The -10 here matches the +10 in resizeAllDragsAndDropsInGroup().\n left = Math.max(0, Math.min(left, backgroundImage.width() - drop.width() - 10));\n top = Math.max(0, Math.min(top, backgroundImage.height() - drop.height() - 10));\n\n // Update the form.\n dragDropToImageForm.form.setFormValue('drops', [dropNo, 'xleft'], left);\n dragDropToImageForm.form.setFormValue('drops', [dropNo, 'ytop'], top);\n },\n\n /**\n * Handles when the drag ends.\n */\n dragEnd: function() {\n // Redraw, in case the position was constrained.\n dragDropToImageForm.updateDropZones();\n },\n\n /**\n * Low level operations on form.\n */\n form: {\n toNameWithIndex: function(name, indexes) {\n var indexString = name;\n for (var i = 0; i < indexes.length; i++) {\n indexString = indexString + '[' + indexes[i] + ']';\n }\n return indexString;\n },\n\n getEl: function(name, indexes) {\n var form = $('form.mform')[0];\n return form.elements[this.toNameWithIndex(name, indexes)];\n },\n\n /**\n * Helper to get the value of a form elements with name like \"drops[0][xleft]\".\n *\n * @param {String} name the base name, e.g. 'drops'.\n * @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].\n * @return {String} the value of that field.\n */\n getFormValue: function(name, indexes) {\n var el = this.getEl(name, indexes);\n if (!el.type) {\n el = el[el.length - 1];\n }\n if (el.type === 'checkbox') {\n return el.checked;\n } else {\n return el.value;\n }\n },\n\n /**\n * Helper to get the value of a form elements with name like \"drops[0][xleft]\".\n *\n * @param {String} name the base name, e.g. 'drops'.\n * @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].\n * @param {String|Number} value the value to set.\n */\n setFormValue: function(name, indexes, value) {\n var el = this.getEl(name, indexes);\n if (el.type === 'checkbox') {\n el.checked = value;\n } else {\n el.value = value;\n }\n }\n },\n\n /**\n * Utility to get the file name and url from the filepicker.\n * @returns {Object} object containing functions {file, name}\n */\n filePickers: function() {\n var draftItemIdsToName;\n var nameToParentNode;\n\n if (draftItemIdsToName === undefined) {\n draftItemIdsToName = {};\n nameToParentNode = {};\n var fp = $('form.mform input.filepickerhidden');\n fp.each(function(index, filepicker) {\n draftItemIdsToName[filepicker.value] = filepicker.name;\n nameToParentNode[filepicker.name] = filepicker.parentNode;\n });\n }\n\n return {\n file: function(name) {\n var parentNode = $(nameToParentNode[name]);\n var fileAnchor = parentNode.find('div.filepicker-filelist a');\n if (fileAnchor.length) {\n return {href: fileAnchor.get(0).href, name: fileAnchor.get(0).innerHTML};\n } else {\n return {href: null, name: null};\n }\n },\n\n name: function(draftitemid) {\n return draftItemIdsToName[draftitemid];\n }\n };\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/form\n */\n return {\n /**\n * Initialise the form JavaScript features.\n *\n * @param {Object} maxBgImageSize object with two properties: width and height.\n * @param {Object} maxDragImageSize object with two properties: width and height.\n */\n init: dragDropToImageForm.init\n };\n});\n"],"file":"form.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/form.js"],"names":["define","$","dragDrop","dragDropToImageForm","maxBgImageSize","maxDragImageSize","fp","init","filePickers","append","updateVisibilityOfFilePickers","setOptionsForDragItemSelectors","setupEventHandlers","waitForFilePickerToInitialise","file","href","setTimeout","M","util","js_pending","on","loadPreviewImage","one","afterPreviewImageLoaded","attr","bgImg","constrainImageSize","createDropZones","js_complete","img","maxSize","reduceby","Math","max","width","height","css","floor","addClass","dropZoneHolder","empty","bgimageurl","numDrops","form","getFormValue","dropNo","dragNo","group","label","imgUrl","waitForAllDropImagesToBeLoaded","notYetLoadedImages","not","i","imgNode","imageIsLoaded","length","updateDropZones","imgElement","complete","naturalHeight","dropBackgroundPosition","offset","drop","left","parseInt","top","is","html","numGroups","first","find","resizeAllDragsAndDropsInGroup","drops","maxWidth","maxHeight","each","ceil","offsetWidth","offsetHeight","round","e","input","target","closest","hasClass","dragStart","window","numDrags","picker","show","hide","dragItemOptions","toNameWithIndex","name","selector","selectedvalue","val","remove","value","hasOwnProperty","optionnode","isItemUsed","filter","selectNode","info","prepare","start","x","y","dragMove","dragEnd","backgroundImage","backgroundPosition","data","dropPosition","min","setFormValue","indexes","indexString","getEl","elements","el","type","checked","draftItemIdsToName","nameToParentNode","index","filepicker","parentNode","fileAnchor","get","innerHTML","draftitemid"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/form\n * @package qtype_ddimageortext\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/dragdrop'], function($, dragDrop) {\n\n \"use strict\";\n\n /**\n * Singleton object to handle progressive enhancement of the\n * drag-drop onto image question editing form.\n * @type {Object}\n */\n var dragDropToImageForm = {\n /**\n * @var {Object} with properties width and height.\n */\n maxBgImageSize: null,\n\n /**\n * @var {Object} with properties width and height.\n */\n maxDragImageSize: null,\n\n /**\n * @var {object} for interacting with the file pickers.\n */\n fp: null, // Object containing functions associated with the file picker.\n\n /**\n * Initialise the form javascript features.\n *\n * @param {Object} maxBgImageSize object with two properties: width and height.\n * @param {Object} maxDragImageSize object with two properties: width and height.\n */\n init: function(maxBgImageSize, maxDragImageSize) {\n dragDropToImageForm.maxBgImageSize = maxBgImageSize;\n dragDropToImageForm.maxDragImageSize = maxDragImageSize;\n dragDropToImageForm.fp = dragDropToImageForm.filePickers();\n\n $('#id_previewareaheader').append(\n '
' +\n '
' +\n ' ' +\n '
' +\n '
' +\n '
' +\n '
');\n\n dragDropToImageForm.updateVisibilityOfFilePickers();\n dragDropToImageForm.setOptionsForDragItemSelectors();\n dragDropToImageForm.setupEventHandlers();\n dragDropToImageForm.waitForFilePickerToInitialise();\n },\n\n /**\n * Waits for the file-pickers to be sufficiently ready before initialising the preview.\n */\n waitForFilePickerToInitialise: function() {\n if (dragDropToImageForm.fp.file('bgimage').href === null) {\n // It would be better to use an onload or onchange event rather than this timeout.\n // Unfortunately attempts to do this early are overwritten by filepicker during its loading.\n setTimeout(dragDropToImageForm.waitForFilePickerToInitialise, 1000);\n return;\n }\n M.util.js_pending('dragDropToImageForm');\n\n // From now on, when a new file gets loaded into the filepicker, update the preview.\n // This is not in the setupEventHandlers section as it needs to be delayed until\n // after filepicker's javascript has finished.\n $('form.mform').on('change', '.filepickerhidden', function() {\n M.util.js_pending('dragDropToImageForm');\n dragDropToImageForm.loadPreviewImage();\n });\n\n dragDropToImageForm.loadPreviewImage();\n },\n\n /**\n * Loads the preview background image.\n */\n loadPreviewImage: function() {\n $('fieldset#id_previewareaheader .dropbackground')\n .one('load', dragDropToImageForm.afterPreviewImageLoaded)\n .attr('src', dragDropToImageForm.fp.file('bgimage').href);\n },\n\n /**\n * After the background image is loaded, continue setting up the preview.\n */\n afterPreviewImageLoaded: function() {\n var bgImg = $('fieldset#id_previewareaheader .dropbackground');\n dragDropToImageForm.constrainImageSize(bgImg, dragDropToImageForm.maxBgImageSize);\n dragDropToImageForm.createDropZones();\n M.util.js_complete('dragDropToImageForm');\n },\n\n /**\n * Limits an image display size to the given maximums.\n *\n * @param {jQuery} img the image.\n * @param {Object} maxSize with width and height properties.\n */\n constrainImageSize: function(img, maxSize) {\n var reduceby = Math.max(img.width() / maxSize.width,\n img.height() / maxSize.height);\n if (reduceby > 1) {\n img.css('width', Math.floor(img.width() / reduceby));\n }\n img.addClass('constrained');\n },\n\n /**\n * Create, or recreate all the drop zones.\n */\n createDropZones: function() {\n var dropZoneHolder = $('.dropzones');\n dropZoneHolder.empty();\n\n var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;\n if (bgimageurl === null) {\n return; // There is not currently a valid preview to update.\n }\n\n var numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']);\n if (dragNo === '0') {\n continue;\n }\n dragNo = dragNo - 1;\n var group = dragDropToImageForm.form.getFormValue('drags', [dragNo, 'draggroup']),\n label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {\n var imgUrl = dragDropToImageForm.fp.file('dragitem[' + dragNo + ']').href;\n if (imgUrl === null) {\n continue;\n }\n // Althoug these are previews of drops, we also add the class name 'drag',\n dropZoneHolder.append('\"'');\n\n } else if (label !== '') {\n dropZoneHolder.append('
' + label + '
');\n }\n }\n\n dragDropToImageForm.waitForAllDropImagesToBeLoaded();\n },\n\n /**\n * This polls until all the drop-zone images have loaded, and then calls updateDropZones().\n */\n waitForAllDropImagesToBeLoaded: function() {\n var notYetLoadedImages = $('.dropzones img').not(function(i, imgNode) {\n return dragDropToImageForm.imageIsLoaded(imgNode);\n });\n\n if (notYetLoadedImages.length > 0) {\n setTimeout(function() {\n dragDropToImageForm.waitForAllDropImagesToBeLoaded();\n }, 100);\n return;\n }\n\n dragDropToImageForm.updateDropZones();\n },\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n imageIsLoaded: function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n },\n\n /**\n * Set the size and position of all the drop zones.\n */\n updateDropZones: function() {\n var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;\n if (bgimageurl === null) {\n return; // There is not currently a valid preview to update.\n }\n\n var dropBackgroundPosition = $('fieldset#id_previewareaheader .dropbackground').offset(),\n numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n\n // Move each drop to the right position and update the text.\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var drop = $('.dropzones .drop' + dropNo);\n if (drop.length === 0) {\n continue;\n }\n var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']) - 1;\n\n drop.offset({\n left: dropBackgroundPosition.left +\n parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'xleft'])),\n top: dropBackgroundPosition.top +\n parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'ytop']))\n });\n\n var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n if (drop.is('img')) {\n drop.attr('alt', label);\n } else {\n drop.html(label);\n }\n }\n\n // Resize them to the same size.\n $('.dropzones .droppreview').css('padding', '0');\n var numGroups = $('select.draggroup').first().find('option').length;\n for (var group = 1; group <= numGroups; group++) {\n dragDropToImageForm.resizeAllDragsAndDropsInGroup(group);\n }\n },\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n resizeAllDragsAndDropsInGroup: function(group) {\n var drops = $('.dropzones .droppreview.group' + group),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n drops.each(function(i, drop) {\n maxWidth = Math.max(maxWidth, Math.ceil(drop.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drop.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n drops.each(function(i, drop) {\n var left = Math.round((maxWidth - drop.offsetWidth) / 2),\n top = Math.floor((maxHeight - drop.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drop).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drop.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drop.offsetHeight - top) + 'px'\n });\n });\n },\n\n /**\n * Events linked to form actions.\n */\n setupEventHandlers: function() {\n // Changes to settings in the draggable items section.\n $('fieldset#id_draggableitemheader')\n .on('change input', 'input, select', function(e) {\n var input = $(e.target).closest('select, input');\n if (input.hasClass('dragitemtype')) {\n dragDropToImageForm.updateVisibilityOfFilePickers();\n }\n\n dragDropToImageForm.setOptionsForDragItemSelectors();\n\n if (input.is('.dragitemtype, .draggroup')) {\n dragDropToImageForm.createDropZones();\n } else if (input.is('.draglabel')) {\n dragDropToImageForm.updateDropZones();\n }\n });\n\n // Changes to Drop zones section: left, top and drag item.\n $('fieldset#id_dropzoneheader').on('change input', 'input, select', function(e) {\n var input = $(e.target).closest('select, input');\n if (input.is('select')) {\n dragDropToImageForm.createDropZones();\n } else {\n dragDropToImageForm.updateDropZones();\n }\n });\n\n // Moving drop zones in the preview.\n $('fieldset#id_previewareaheader').on('mousedown touchstart', '.droppreview', function(e) {\n dragDropToImageForm.dragStart(e);\n });\n\n $(window).on('resize', function() {\n dragDropToImageForm.updateDropZones();\n });\n },\n\n /**\n * Update all the drag item filepickers, so they are only shown for\n */\n updateVisibilityOfFilePickers: function() {\n var numDrags = dragDropToImageForm.form.getFormValue('noitems', []);\n for (var dragNo = 0; dragNo < numDrags; dragNo++) {\n var picker = $('input#id_dragitem_' + dragNo).closest('.fitem_ffilepicker');\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {\n picker.show();\n } else {\n picker.hide();\n }\n }\n },\n\n\n setOptionsForDragItemSelectors: function() {\n var dragItemOptions = {'0': ''},\n numDrags = dragDropToImageForm.form.getFormValue('noitems', []),\n numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n\n // Work out the list of options.\n for (var dragNo = 0; dragNo < numDrags; dragNo++) {\n var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n var file = dragDropToImageForm.fp.file(dragDropToImageForm.form.toNameWithIndex('dragitem', [dragNo]));\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype']) && file.name !== null) {\n dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label + ' (' + file.name + ')';\n } else if (label !== '') {\n dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label;\n }\n }\n\n // Initialise each select.\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var selector = $('#id_drops_' + dropNo + '_choice');\n\n var selectedvalue = selector.val();\n selector.find('option').remove();\n for (var value in dragItemOptions) {\n if (!dragItemOptions.hasOwnProperty(value)) {\n continue;\n }\n selector.append('');\n var optionnode = selector.find('option[value=\"' + value + '\"]');\n if (parseInt(value) === parseInt(selectedvalue)) {\n optionnode.attr('selected', true);\n } else if (dragDropToImageForm.isItemUsed(parseInt(value))) {\n optionnode.attr('disabled', true);\n }\n }\n }\n },\n\n /**\n * Checks if the specified drag option is already used somewhere.\n *\n * @param {Number} value of the drag item to check\n * @return {Boolean} true if item is allocated to dropzone\n */\n isItemUsed: function(value) {\n if (value === 0) {\n return false; // None option can always be selected.\n }\n\n if (dragDropToImageForm.form.getFormValue('drags', [value - 1, 'infinite'])) {\n return false; // Infinite, so can't be used up.\n }\n\n return $('fieldset#id_dropzoneheader select').filter(function(i, selectNode) {\n return parseInt($(selectNode).val()) === value;\n }).length !== 0;\n },\n\n /**\n * Handles when a dropzone in dragged in the preview.\n * @param {Object} e Event object\n */\n dragStart: function(e) {\n var drop = $(e.target).closest('.droppreview');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragDrop.start(e, drop, function(x, y, drop) {\n dragDropToImageForm.dragMove(drop);\n }, function() {\n dragDropToImageForm.dragEnd();\n });\n },\n\n /**\n * Handles update while a drop is being dragged.\n *\n * @param {jQuery} drop the drop preview being moved.\n */\n dragMove: function(drop) {\n var backgroundImage = $('fieldset#id_previewareaheader .dropbackground'),\n backgroundPosition = backgroundImage.offset(),\n dropNo = drop.data('dropNo'),\n dropPosition = drop.offset(),\n left = Math.round(dropPosition.left - backgroundPosition.left),\n top = Math.round(dropPosition.top - backgroundPosition.top);\n\n // Constrain coordinates to be inside the background.\n // The -10 here matches the +10 in resizeAllDragsAndDropsInGroup().\n left = Math.max(0, Math.min(left, backgroundImage.width() - drop.width() - 10));\n top = Math.max(0, Math.min(top, backgroundImage.height() - drop.height() - 10));\n\n // Update the form.\n dragDropToImageForm.form.setFormValue('drops', [dropNo, 'xleft'], left);\n dragDropToImageForm.form.setFormValue('drops', [dropNo, 'ytop'], top);\n },\n\n /**\n * Handles when the drag ends.\n */\n dragEnd: function() {\n // Redraw, in case the position was constrained.\n dragDropToImageForm.updateDropZones();\n },\n\n /**\n * Low level operations on form.\n */\n form: {\n toNameWithIndex: function(name, indexes) {\n var indexString = name;\n for (var i = 0; i < indexes.length; i++) {\n indexString = indexString + '[' + indexes[i] + ']';\n }\n return indexString;\n },\n\n getEl: function(name, indexes) {\n var form = $('form.mform')[0];\n return form.elements[this.toNameWithIndex(name, indexes)];\n },\n\n /**\n * Helper to get the value of a form elements with name like \"drops[0][xleft]\".\n *\n * @param {String} name the base name, e.g. 'drops'.\n * @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].\n * @return {String} the value of that field.\n */\n getFormValue: function(name, indexes) {\n var el = this.getEl(name, indexes);\n if (!el.type) {\n el = el[el.length - 1];\n }\n if (el.type === 'checkbox') {\n return el.checked;\n } else {\n return el.value;\n }\n },\n\n /**\n * Helper to get the value of a form elements with name like \"drops[0][xleft]\".\n *\n * @param {String} name the base name, e.g. 'drops'.\n * @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].\n * @param {String|Number} value the value to set.\n */\n setFormValue: function(name, indexes, value) {\n var el = this.getEl(name, indexes);\n if (el.type === 'checkbox') {\n el.checked = value;\n } else {\n el.value = value;\n }\n }\n },\n\n /**\n * Utility to get the file name and url from the filepicker.\n * @returns {Object} object containing functions {file, name}\n */\n filePickers: function() {\n var draftItemIdsToName;\n var nameToParentNode;\n\n if (draftItemIdsToName === undefined) {\n draftItemIdsToName = {};\n nameToParentNode = {};\n var fp = $('form.mform input.filepickerhidden');\n fp.each(function(index, filepicker) {\n draftItemIdsToName[filepicker.value] = filepicker.name;\n nameToParentNode[filepicker.name] = filepicker.parentNode;\n });\n }\n\n return {\n file: function(name) {\n var parentNode = $(nameToParentNode[name]);\n var fileAnchor = parentNode.find('div.filepicker-filelist a');\n if (fileAnchor.length) {\n return {href: fileAnchor.get(0).href, name: fileAnchor.get(0).innerHTML};\n } else {\n return {href: null, name: null};\n }\n },\n\n name: function(draftitemid) {\n return draftItemIdsToName[draftitemid];\n }\n };\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/form\n */\n return {\n /**\n * Initialise the form JavaScript features.\n *\n * @param {Object} maxBgImageSize object with two properties: width and height.\n * @param {Object} maxDragImageSize object with two properties: width and height.\n */\n init: dragDropToImageForm.init\n };\n});\n"],"file":"form.min.js"} \ No newline at end of file diff --git a/question/type/ddimageortext/amd/build/question.min.js b/question/type/ddimageortext/amd/build/question.min.js dissimilarity index 96% index ec81d0e046f..403463f2125 100644 --- a/question/type/ddimageortext/amd/build/question.min.js +++ b/question/type/ddimageortext/amd/build/question.min.js @@ -1,2 +1,2 @@ -define ("qtype_ddimageortext/question",["jquery","core/dragdrop","core/key_codes"],function(a,b,c){"use strict";function d(a,b,c){this.containerId=a;M.util.js_pending("qtype_ddimageortext-init-"+this.containerId);this.places=c;this.allImagesLoaded=!1;this.imageLoadingTimeoutId=null;if(b){this.getRoot().addClass("qtype_ddimageortext-readonly")}var d=this;this.getNotYetLoadedImages().one("load",function(){d.waitForAllImagesToBeLoaded()});this.waitForAllImagesToBeLoaded()}d.prototype.waitForAllImagesToBeLoaded=function(){var a=this;if(this.allImagesLoaded){return}if(null!==this.imageLoadingTimeoutId){clearTimeout(this.imageLoadingTimeoutId)}if(0 div").each(function(c,d){b.resizeAllDragsAndDropsInGroup(b.getClassnameNumericSuffix(a(d),"dragitemgroup"))})};d.prototype.resizeAllDragsAndDropsInGroup=function(b){var c=this.getRoot(),d=c.find(".dragitemgroup"+b+" .draghome"),e=0,f=0;d.each(function(a,b){e=Math.max(e,Math.ceil(b.offsetWidth));f=Math.max(f,Math.ceil(b.offsetHeight))});e+=10;f+=10;d.each(function(b,c){var d=Math.round((e-c.offsetWidth)/2),g=Math.floor((f-c.offsetHeight)/2);a(c).css({"padding-left":d+"px","padding-right":e-c.offsetWidth-d+"px","padding-top":g+"px","padding-bottom":f-c.offsetHeight-g+"px"})});for(var g in this.places){if(!this.places.hasOwnProperty(g)){continue}var h=this.places[g],i=h.text;if(parseInt(h.group)!==b){continue}if(""===i){i=M.util.get_string("blank","qtype_ddimageortext")}c.find(".dropzones").append("
"+i+" 
");c.find(".dropzone.place"+g).width(e-2).height(f-2)}};d.prototype.cloneDrags=function(){var b=this;this.getRoot().find(".ddarea .draghome").each(function(c,d){b.cloneDragsForOneChoice(a(d))})};d.prototype.cloneDragsForOneChoice=function(a){if(a.hasClass("infinite")){for(var b=this.noOfDropsInGroup(this.getGroup(a)),c=0;c=d.left&&a=d.top&&b div").each(function(c,d){b.resizeAllDragsAndDropsInGroup(b.getClassnameNumericSuffix(a(d),"dragitemgroup"))})};d.prototype.resizeAllDragsAndDropsInGroup=function(b){var c=this.getRoot(),d=c.find(".dragitemgroup"+b+" .draghome"),e=0,f=0;d.each(function(a,b){e=Math.max(e,Math.ceil(b.offsetWidth));f=Math.max(f,Math.ceil(b.offsetHeight))});e+=10;f+=10;d.each(function(b,c){var d=Math.round((e-c.offsetWidth)/2),g=Math.floor((f-c.offsetHeight)/2);a(c).css({"padding-left":d+"px","padding-right":e-c.offsetWidth-d+"px","padding-top":g+"px","padding-bottom":f-c.offsetHeight-g+"px"})});for(var g in this.places){if(!this.places.hasOwnProperty(g)){continue}var h=this.places[g],i=h.text;if(parseInt(h.group)!==b){continue}if(""===i){i=M.util.get_string("blank","qtype_ddimageortext")}c.find(".dropzones").append("
"+i+" 
");c.find(".dropzone.place"+g).width(e-2).height(f-2)}};d.prototype.cloneDrags=function(){var b=this;b.getRoot().find(".draghome").each(function(c,d){var e=a(d),f=e.clone();f.removeClass();f.addClass("draghome choice"+b.getChoice(e)+" group"+b.getGroup(e)+" dragplaceholder");e.before(f)})};d.prototype.cloneDragsForOneChoice=function(a){if(a.hasClass("infinite")){for(var b=this.noOfDropsInGroup(this.getGroup(a)),c=0;c=d.left&&a=d.top&&b=d.left&&a=d.top&&bb){b=e}});return b};var f={eventHandlersInitialised:!1,isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function init(a,b,c){f.questions[a]=new d(a,b,c);if(!f.eventHandlersInitialised){f.setupEventHandlers();f.eventHandlersInitialised=!0}},setupEventHandlers:function setupEventHandlers(){a("body").on("mousedown touchstart",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome",f.handleDragStart).on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone",f.handleKeyPress).on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)",f.handleKeyPress).on("dragmoved",f.handleDragMoved);a(window).on("resize",function(){f.handleWindowResize(!1)});window.addEventListener("beforeprint",function(){f.isPrinting=!0;f.handleWindowResize(f.isPrinting)});window.addEventListener("afterprint",function(){f.isPrinting=!1;f.handleWindowResize(f.isPrinting)});setTimeout(function(){f.fixLayoutIfThingsMoved()},100)},handleDragStart:function handleDragStart(a){a.preventDefault();var b=f.getQuestionForEvent(a);if(b){b.handleDragStart(a)}},handleKeyPress:function handleKeyPress(a){if(f.isKeyboardNavigation){return}f.isKeyboardNavigation=!0;var b=f.getQuestionForEvent(a);if(b){b.handleKeyPress(a)}},handleWindowResize:function handleWindowResize(a){for(var b in f.questions){if(f.questions.hasOwnProperty(b)){f.questions[b].isPrinting=a;f.questions[b].handleResize()}}},fixLayoutIfThingsMoved:function fixLayoutIfThingsMoved(){this.handleWindowResize(f.isPrinting);setTimeout(function(){f.fixLayoutIfThingsMoved(f.isPrinting)},100)},handleDragMoved:function handleDragMoved(a,b,c,d){b.removeClass("beingdragged").css("z-index","");b.css("top",c.position().top).css("left",c.position().left);c.after(b);c.removeClass("active");if("undefined"!=typeof b.data("unplaced")&&!0===b.data("unplaced")){b.removeClass("placed").addClass("unplaced");b.removeAttr("tabindex");b.removeData("unplaced");b.css("top","").css("left","").css("transform","");if(b.hasClass("infinite")&&1.\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/question\n * @package qtype_ddimageortext\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) {\n\n \"use strict\";\n\n /**\n * Initialise one drag-drop onto image question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Array} places Information about the drop places.\n * @constructor\n */\n function DragDropOntoImageQuestion(containerId, readOnly, places) {\n this.containerId = containerId;\n M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);\n this.places = places;\n this.allImagesLoaded = false;\n this.imageLoadingTimeoutId = null;\n if (readOnly) {\n this.getRoot().addClass('qtype_ddimageortext-readonly');\n }\n\n var thisQ = this;\n this.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n this.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n var thisQ = this;\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n thisQ.waitForAllImagesToBeLoaded();\n }, 100);\n return;\n }\n\n // We now have all images. Carry on, but only after giving the layout a chance to settle down.\n this.allImagesLoaded = true;\n thisQ.setupQuestion();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {\n var thisQ = this;\n return this.getRoot().find('.ddarea img').not(function(i, imgNode) {\n return thisQ.imageIsLoaded(imgNode);\n });\n };\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Set up the question, once all images have been loaded.\n */\n DragDropOntoImageQuestion.prototype.setupQuestion = function() {\n this.resizeAllDragsAndDrops();\n this.cloneDrags();\n this.positionDragsAndDrops();\n M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);\n };\n\n /**\n * In each group, resize all the items to be the same size.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {\n var thisQ = this;\n this.getRoot().find('.draghomes > div').each(function(i, node) {\n thisQ.resizeAllDragsAndDropsInGroup(\n thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));\n });\n };\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {\n var root = this.getRoot(),\n dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n dragHomes.each(function(i, drag) {\n maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n dragHomes.each(function(i, drag) {\n var left = Math.round((maxWidth - drag.offsetWidth) / 2),\n top = Math.floor((maxHeight - drag.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drag).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'\n });\n });\n\n // Create the drops and make them the right size.\n for (var i in this.places) {\n if (!this.places.hasOwnProperty((i))) {\n continue;\n }\n var place = this.places[i],\n label = place.text;\n if (parseInt(place.group) !== group) {\n continue;\n }\n if (label === '') {\n label = M.util.get_string('blank', 'qtype_ddimageortext');\n }\n root.find('.dropzones').append('
' +\n '' + label + ' 
');\n root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);\n }\n };\n\n /**\n * Invisible 'drag homes' are output by the renderer. These have the same properties\n * as the drag items but are invisible. We clone these invisible elements to make the\n * actual drag items.\n */\n DragDropOntoImageQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n this.getRoot().find('.ddarea .draghome').each(function(index, dragHome) {\n thisQ.cloneDragsForOneChoice($(dragHome));\n });\n };\n\n /**\n * Clone drag item for one choice.\n *\n * @param {jQuery} dragHome the drag home to clone.\n */\n DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {\n if (dragHome.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));\n for (var i = 0; i < noOfDrags; i++) {\n this.cloneDrag(dragHome);\n }\n } else {\n this.cloneDrag(dragHome);\n }\n };\n\n /**\n * Clone drag item.\n *\n * @param {jQuery} dragHome\n */\n DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {\n var drag = dragHome.clone();\n drag.removeClass('draghome')\n .addClass('drag unplaced moodle-has-zindex')\n .offset(dragHome.offset());\n this.getRoot().find('.dragitems').append(drag);\n };\n\n /**\n * Update the position of drags.\n */\n DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {\n var thisQ = this,\n root = this.getRoot(),\n bgPosition = this.bgImage().offset();\n\n // Move the drops into position.\n root.find('.ddarea .dropzone').each(function(i, dropNode) {\n var drop = $(dropNode),\n place = thisQ.places[thisQ.getPlace(drop)];\n // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation.\n drop.offset({\n left: bgPosition.left + parseInt(place.xy[0]),\n top: bgPosition.top + parseInt(place.xy[1])});\n });\n\n // First move all items back home.\n root.find('.ddarea .drag').each(function(i, dragNode) {\n var drag = $(dragNode),\n currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');\n drag.addClass('unplaced')\n .removeClass('placed')\n .offset(thisQ.getDragHome(thisQ.getGroup(drag), thisQ.getChoice(drag)).offset());\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n });\n\n // Then place the ones that should be placed.\n root.find('input.placeinput').each(function(i, inputNode) {\n var input = $(inputNode),\n choice = input.val();\n if (choice === '0') {\n // No item in this place.\n return;\n }\n\n var place = thisQ.getPlace(input);\n thisQ.getUnplacedChoice(thisQ.getGroup(input), choice)\n .removeClass('unplaced')\n .addClass('placed inplace' + place)\n .offset(root.find('.dropzone.place' + place).offset());\n });\n\n this.bgImage().data('prev-top', bgPosition.top).data('prev-left', bgPosition.left);\n };\n\n /**\n * Check to see if the background image has moved. If so, refresh the layout.\n */\n DragDropOntoImageQuestion.prototype.fixLayoutIfBackgroundMoved = function() {\n var bgImage = this.bgImage(),\n bgPosition = bgImage.offset(),\n prevTop = bgImage.data('prev-top'),\n prevLeft = bgImage.data('prev-left');\n if (prevLeft === undefined || prevTop === undefined) {\n // Question is not set up yet. Nothing to do.\n return;\n }\n if (prevTop === bgPosition.top && prevLeft === bgPosition.left) {\n // Things have not moved.\n return;\n }\n // We need to reposition things.\n this.positionDragsAndDrops();\n };\n\n /**\n * Handles the start of dragging an item.\n *\n * @param {Event} e the touch start or mouse down event.\n */\n DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n drag = $(e.target).closest('.drag');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n this.setInputValue(currentPlace, 0);\n drag.removeClass('inplace' + currentPlace);\n }\n\n drag.addClass('beingdragged');\n dragDrop.start(e, drag, function(x, y, drag) {\n thisQ.dragMove(x, y, drag);\n }, function(x, y, drag) {\n thisQ.dragEnd(x, y, drag);\n });\n };\n\n /**\n * Called whenever the currently dragged items moves.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {\n var thisQ = this;\n this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop)) {\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n };\n\n /**\n * Called when user drops a drag item.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {\n var thisQ = this,\n root = this.getRoot(),\n placed = false;\n root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (!thisQ.isPointInDrop(pageX, pageY, drop)) {\n // Not this drop.\n return true;\n }\n\n // Now put this drag into the drop.\n drop.removeClass('valid-drag-over-drop');\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n\n if (!placed) {\n this.sendDragHome(drag);\n }\n };\n\n /**\n * Animate a drag item into a given place (or back home).\n *\n * @param {jQuery|null} drag the item to place. If null, clear the place.\n * @param {jQuery} drop the place to put it.\n */\n DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {\n // Is there already a drag in this drop? if so, evict it.\n var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));\n if (oldDrag.length !== 0) {\n this.sendDragHome(oldDrag);\n }\n\n if (drag.length === 0) {\n this.setInputValue(this.getPlace(drop), 0);\n } else {\n this.setInputValue(this.getPlace(drop), this.getChoice(drag));\n drag.removeClass('unplaced')\n .addClass('placed inplace' + this.getPlace(drop));\n this.animateTo(drag, drop);\n }\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('placed').addClass('unplaced');\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n\n this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));\n };\n\n /**\n * Handles keyboard events on drops.\n *\n * Drops are focusable. Once focused, right/down/space switches to the next choice, and\n * left/up switches to the previous. Escape clear.\n *\n * @param {KeyboardEvent} e\n */\n DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {\n var drop = $(e.target).closest('.dropzone'),\n currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),\n nextDrag = $();\n\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.escape:\n break;\n\n default:\n return; // To avoid the preventDefault below.\n }\n\n e.preventDefault();\n this.sendDragToDrop(nextDrag, drop);\n };\n\n /**\n * Choose the next drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {\n var choice,\n numChoices = this.noOfChoicesInGroup(group);\n\n if (drag.length === 0) {\n choice = 1; // Was empty, so we want to select the first choice.\n } else {\n choice = this.getChoice(drag) + 1;\n }\n\n var next = this.getUnplacedChoice(group, choice);\n while (next.length === 0 && choice < numChoices) {\n choice++;\n next = this.getUnplacedChoice(group, choice);\n }\n\n return next;\n };\n\n /**\n * Choose the previous drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {\n var choice;\n\n if (drag.length === 0) {\n choice = this.noOfChoicesInGroup(group);\n } else {\n choice = this.getChoice(drag) - 1;\n }\n\n var previous = this.getUnplacedChoice(group, choice);\n while (previous.length === 0 && choice > 1) {\n choice--;\n previous = this.getUnplacedChoice(group, choice);\n }\n\n // Does this choice exist?\n return previous;\n };\n\n /**\n * Animate an object to the given destination.\n *\n * @param {jQuery} drag the element to be animated.\n * @param {jQuery} target element marking the place to move it to.\n */\n DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {\n var currentPos = drag.offset(),\n targetPos = target.offset();\n drag.addClass('beingdragged');\n\n // Animate works in terms of CSS position, whereas locating an object\n // on the page works best with jQuery offset() function. So, to get\n // the right target position, we work out the required change in\n // offset() and then add that to the current CSS position.\n drag.animate(\n {\n left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,\n top: parseInt(drag.css('top')) + targetPos.top - currentPos.top\n },\n {\n duration: 'fast',\n done: function() {\n drag.removeClass('beingdragged');\n // It seems that the animation sometimes leaves the drag\n // one pixel out of position. Put it in exactly the right place.\n drag.offset(targetPos);\n }\n }\n );\n };\n\n /**\n * Detect if a point is inside a given DOM node.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drop the node to check (typically a drop).\n * @return {boolean} whether the point is inside the node.\n */\n DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {\n var position = drop.offset();\n return pageX >= position.left && pageX < position.left + drop.width()\n && pageY >= position.top && pageY < position.top + drop.height();\n };\n\n /**\n * Set the value of the hidden input for a place, to record what is currently there.\n *\n * @param {int} place which place to set the input value for.\n * @param {int} choice the value to set.\n */\n DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {\n this.getRoot().find('input.placeinput.place' + place).val(choice);\n };\n\n /**\n * Get the outer div for this question.\n *\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DragDropOntoImageQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n /**\n * Get drag home for a given choice.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {\n return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice);\n };\n\n /**\n * Get an unplaced choice for a particular group.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.\n */\n DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {\n return this.getRoot().find('.ddarea .drag.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);\n };\n\n /**\n * Get the drag that is currently in a given place.\n *\n * @param {int} place the place number.\n * @return {jQuery} the current drag (or an empty jQuery if none).\n */\n DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {\n return this.getRoot().find('.ddarea .drag.inplace' + place);\n };\n\n /**\n * Return the number of blanks in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of drops.\n */\n DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {\n return this.getRoot().find('.dropzone.group' + group).length;\n };\n\n /**\n * Return the number of choices in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of choices.\n */\n DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {\n return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;\n };\n\n /**\n * Return the number at the end of the CSS class name with the given prefix.\n *\n * @param {jQuery} node\n * @param {String} prefix name prefix\n * @returns {Number|null} the suffix if found, else null.\n */\n DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = node.attr('class');\n if (classes !== '') {\n var classesArr = classes.split(' ');\n for (var index = 0; index < classesArr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesArr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesArr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Get the choice number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the choice number.\n */\n DragDropOntoImageQuestion.prototype.getChoice = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'choice');\n };\n\n /**\n * Given a DOM node that is significant to this question\n * (drag, drop, ...) get the group it belongs to.\n *\n * @param {jQuery} node a DOM node.\n * @returns {Number} the group it belongs to.\n */\n DragDropOntoImageQuestion.prototype.getGroup = function(node) {\n return this.getClassnameNumericSuffix(node, 'group');\n };\n\n /**\n * Get the place number of a drop, or its corresponding hidden input.\n *\n * @param {jQuery} node the DOM node.\n * @returns {Number} the place number.\n */\n DragDropOntoImageQuestion.prototype.getPlace = function(node) {\n return this.getClassnameNumericSuffix(node, 'place');\n };\n\n /**\n * Singleton object that handles all the DragDropOntoImageQuestions\n * on the page, and deals with event dispatching.\n * @type {Object}\n */\n var questionManager = {\n\n /**\n * {boolean} ensures that the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @param {String} containerId the id of the div.que that contains this question.\n * @param {boolean} readOnly whether the question is read-only.\n * @param {Array} places data.\n */\n init: function(containerId, readOnly, places) {\n questionManager.questions[containerId] =\n new DragDropOntoImageQuestion(containerId, readOnly, places);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $('body')\n .on('mousedown touchstart',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dragitems .drag',\n questionManager.handleDragStart)\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',\n questionManager.handleKeyPress);\n $(window).on('resize', questionManager.handleWindowResize);\n setTimeout(questionManager.fixLayoutIfThingsMoved, 100);\n },\n\n /**\n * Handle mouse down / touch start events on drags.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on drags.\n * @param {KeyboardEvent} e\n */\n handleKeyPress: function(e) {\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n */\n handleWindowResize: function() {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].positionDragsAndDrops();\n }\n }\n },\n\n /**\n * Sometimes, despite our best efforts, things change in a way that cannot\n * be specifically caught (e.g. dock expanding or collapsing in Boost).\n * Therefore, we need to periodically check everything is in the right position.\n */\n fixLayoutIfThingsMoved: function() {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].fixLayoutIfBackgroundMoved();\n }\n }\n\n // We use setTimeout after finishing work, rather than setInterval,\n // in case positioning things is slow. We want 100 ms gap\n // between executions, not what setInterval does.\n setTimeout(questionManager.fixLayoutIfThingsMoved, 100);\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');\n return questionManager.questions[containerId];\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/question\n */\n return {\n /**\n * Initialise one drag-drop onto image question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Array} Information about the drop places.\n */\n init: questionManager.init\n };\n});\n"],"file":"question.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/question.js"],"names":["define","$","dragDrop","keys","DragDropOntoImageQuestion","containerId","readOnly","places","M","util","js_pending","allImagesLoaded","imageLoadingTimeoutId","isPrinting","getRoot","addClass","thisQ","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","clearTimeout","length","setTimeout","setupQuestion","find","not","i","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","resizeAllDragsAndDrops","cloneDrags","positionDragsAndDrops","js_complete","each","node","resizeAllDragsAndDropsInGroup","getClassnameNumericSuffix","group","root","dragHomes","maxWidth","maxHeight","drag","Math","max","ceil","offsetWidth","offsetHeight","left","round","top","floor","css","hasOwnProperty","place","label","text","parseInt","get_string","append","width","height","index","dragHome","placeHolder","clone","removeClass","getChoice","getGroup","before","cloneDragsForOneChoice","hasClass","noOfDrags","noOfDropsInGroup","cloneDrag","offset","bgRatio","dropNode","drop","getPlace","xy","data","handleElementScale","dragNode","currentPlace","removeAttr","inputNode","input","choice","val","unplacedDrag","getUnplacedChoice","hiddenDrag","getDragClone","getInfiniteDragClones","after","sendDragToDrop","handleDragStart","e","target","closest","currentIndex","calculateZIndex","info","prepare","start","setInputValue","hiddenDrop","getDrop","x","y","dragMove","dragEnd","pageX","pageY","isPointInDrop","placed","placedNode","placedDrag","sendDragHome","oldDrag","getCurrentDragInPlace","focus","attr","animateTo","getDragHome","handleKeyPress","currentDrag","nextDrag","keyCode","space","arrowRight","arrowDown","getNextDrag","arrowLeft","arrowUp","getPreviousDrag","escape","questionManager","isKeyboardNavigation","preventDefault","numChoices","noOfChoicesInGroup","next","previous","currentPos","targetPos","animate","duration","done","trigger","position","outerWidth","outerHeight","document","getElementById","bgImage","is","slice","prefix","classes","classesArr","split","patt1","RegExp","test","patt2","match","exec","inHome","handleResize","parseFloat","key","bgImg","bgImgNaturalWidth","get","naturalWidth","bgImgClientWidth","element","type","zIndex","itemZIndex","eventHandlersInitialised","questions","init","setupEventHandlers","on","handleDragMoved","window","handleWindowResize","addEventListener","fixLayoutIfThingsMoved","question","getQuestionForEvent","removeData","first","remove","currentTarget"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/question\n * @package qtype_ddimageortext\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) {\n\n \"use strict\";\n\n /**\n * Initialise one drag-drop onto image question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Array} places Information about the drop places.\n * @constructor\n */\n function DragDropOntoImageQuestion(containerId, readOnly, places) {\n this.containerId = containerId;\n M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);\n this.places = places;\n this.allImagesLoaded = false;\n this.imageLoadingTimeoutId = null;\n this.isPrinting = false;\n if (readOnly) {\n this.getRoot().addClass('qtype_ddimageortext-readonly');\n }\n\n var thisQ = this;\n this.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n this.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n var thisQ = this;\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n thisQ.waitForAllImagesToBeLoaded();\n }, 100);\n return;\n }\n\n // We now have all images. Carry on, but only after giving the layout a chance to settle down.\n this.allImagesLoaded = true;\n thisQ.setupQuestion();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {\n var thisQ = this;\n return this.getRoot().find('.ddarea img').not(function(i, imgNode) {\n return thisQ.imageIsLoaded(imgNode);\n });\n };\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Set up the question, once all images have been loaded.\n */\n DragDropOntoImageQuestion.prototype.setupQuestion = function() {\n this.resizeAllDragsAndDrops();\n this.cloneDrags();\n this.positionDragsAndDrops();\n M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);\n };\n\n /**\n * In each group, resize all the items to be the same size.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {\n var thisQ = this;\n this.getRoot().find('.draghomes > div').each(function(i, node) {\n thisQ.resizeAllDragsAndDropsInGroup(\n thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));\n });\n };\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {\n var root = this.getRoot(),\n dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n dragHomes.each(function(i, drag) {\n maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n dragHomes.each(function(i, drag) {\n var left = Math.round((maxWidth - drag.offsetWidth) / 2),\n top = Math.floor((maxHeight - drag.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drag).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'\n });\n });\n\n // Create the drops and make them the right size.\n for (var i in this.places) {\n if (!this.places.hasOwnProperty((i))) {\n continue;\n }\n var place = this.places[i],\n label = place.text;\n if (parseInt(place.group) !== group) {\n continue;\n }\n if (label === '') {\n label = M.util.get_string('blank', 'qtype_ddimageortext');\n }\n root.find('.dropzones').append('
' +\n '' + label + ' 
');\n root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);\n }\n };\n\n /**\n * Invisible 'drag homes' are output by the renderer. These have the same properties\n * as the drag items but are invisible. We clone these invisible elements to make the\n * actual drag items.\n */\n DragDropOntoImageQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n thisQ.getRoot().find('.draghome').each(function(index, dragHome) {\n var drag = $(dragHome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('draghome choice' +\n thisQ.getChoice(drag) + ' group' +\n thisQ.getGroup(drag) + ' dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Clone drag item for one choice.\n *\n * @param {jQuery} dragHome the drag home to clone.\n */\n DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {\n if (dragHome.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));\n for (var i = 0; i < noOfDrags; i++) {\n this.cloneDrag(dragHome);\n }\n } else {\n this.cloneDrag(dragHome);\n }\n };\n\n /**\n * Clone drag item.\n *\n * @param {jQuery} dragHome\n */\n DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {\n var drag = dragHome.clone();\n drag.removeClass('draghome')\n .addClass('drag unplaced moodle-has-zindex')\n .offset(dragHome.offset());\n this.getRoot().find('.dragitems').append(drag);\n };\n\n /**\n * Update the position of drags.\n */\n DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {\n var thisQ = this,\n root = this.getRoot(),\n bgRatio = this.bgRatio();\n\n // Move the drops into position.\n root.find('.ddarea .dropzone').each(function(i, dropNode) {\n var drop = $(dropNode),\n place = thisQ.places[thisQ.getPlace(drop)];\n // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation.\n drop.css('left', parseInt(place.xy[0]) * bgRatio)\n .css('top', parseInt(place.xy[1]) * bgRatio);\n drop.data('originX', parseInt(place.xy[0]))\n .data('originY', parseInt(place.xy[1]));\n thisQ.handleElementScale(drop, 'left top');\n });\n\n // First move all items back home.\n root.find('.draghome').not('.dragplaceholder').each(function(i, dragNode) {\n var drag = $(dragNode),\n currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');\n drag.addClass('unplaced')\n .removeClass('placed');\n drag.removeAttr('tabindex');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n });\n\n // Then place the ones that should be placed.\n root.find('input.placeinput').each(function(i, inputNode) {\n var input = $(inputNode),\n choice = input.val();\n if (choice.length === 0 || (choice.length > 0 && choice === '0')) {\n // No item in this place.\n return;\n }\n\n var place = thisQ.getPlace(input);\n // Get the unplaced drag.\n var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);\n // Get the clone of the drag.\n var hiddenDrag = thisQ.getDragClone(unplacedDrag);\n if (hiddenDrag.length) {\n if (unplacedDrag.hasClass('infinite')) {\n var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));\n var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = unplacedDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n } else {\n hiddenDrag.addClass('active');\n }\n } else {\n hiddenDrag.addClass('active');\n }\n }\n\n // Send the drag to drop.\n var drop = root.find('.dropzone.place' + place);\n thisQ.sendDragToDrop(unplacedDrag, drop);\n });\n };\n\n /**\n * Handles the start of dragging an item.\n *\n * @param {Event} e the touch start or mouse down event.\n */\n DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n drag = $(e.target).closest('.draghome'),\n currentIndex = this.calculateZIndex(),\n newIndex = currentIndex + 2;\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n drag.addClass('beingdragged').css('transform', '').css('z-index', newIndex);\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n this.setInputValue(currentPlace, 0);\n drag.removeClass('inplace' + currentPlace);\n var hiddenDrop = thisQ.getDrop(drag, currentPlace);\n if (hiddenDrop.length) {\n hiddenDrop.addClass('active');\n drag.offset(hiddenDrop.offset());\n }\n } else {\n var hiddenDrag = thisQ.getDragClone(drag);\n if (hiddenDrag.length) {\n if (drag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(thisQ.getGroup(drag));\n var cloneDrags = this.getInfiniteDragClones(drag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = drag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n drag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n }\n }\n\n dragDrop.start(e, drag, function(x, y, drag) {\n thisQ.dragMove(x, y, drag);\n }, function(x, y, drag) {\n thisQ.dragEnd(x, y, drag);\n });\n };\n\n /**\n * Called whenever the currently dragged items moves.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {\n var thisQ = this;\n this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop)) {\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n this.getRoot().find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop)) {\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n };\n\n /**\n * Called when user drops a drag item.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {\n var thisQ = this,\n root = this.getRoot(),\n placed = false;\n root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (!thisQ.isPointInDrop(pageX, pageY, drop)) {\n // Not this drop.\n return true;\n }\n\n // Now put this drag into the drop.\n drop.removeClass('valid-drag-over-drop');\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n\n root.find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) {\n var placedDrag = $(placedNode);\n if (!thisQ.isPointInDrop(pageX, pageY, placedDrag)) {\n // Not this placed drag.\n return true;\n }\n\n // Now put this drag into the drop.\n placedDrag.removeClass('valid-drag-over-drop');\n var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace');\n var drop = thisQ.getDrop(drag, currentPlace);\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n\n if (!placed) {\n this.sendDragHome(drag);\n }\n };\n\n /**\n * Animate a drag item into a given place (or back home).\n *\n * @param {jQuery|null} drag the item to place. If null, clear the place.\n * @param {jQuery} drop the place to put it.\n */\n DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {\n // Is there already a drag in this drop? if so, evict it.\n var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));\n if (oldDrag.length !== 0) {\n oldDrag.addClass('beingdragged');\n oldDrag.offset(oldDrag.offset());\n var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');\n var hiddenDrop = this.getDrop(oldDrag, currentPlace);\n hiddenDrop.addClass('active');\n this.sendDragHome(oldDrag);\n }\n\n if (drag.length === 0) {\n this.setInputValue(this.getPlace(drop), 0);\n if (drop.data('isfocus')) {\n drop.focus();\n }\n } else {\n this.setInputValue(this.getPlace(drop), this.getChoice(drag));\n drag.removeClass('unplaced')\n .addClass('placed inplace' + this.getPlace(drop));\n drag.attr('tabindex', 0);\n this.animateTo(drag, drop);\n }\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n drag.data('unplaced', true);\n\n this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));\n };\n\n /**\n * Handles keyboard events on drops.\n *\n * Drops are focusable. Once focused, right/down/space switches to the next choice, and\n * left/up switches to the previous. Escape clear.\n *\n * @param {KeyboardEvent} e\n */\n DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {\n var drop = $(e.target).closest('.dropzone');\n if (drop.length === 0) {\n var placedDrag = $(e.target);\n var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');\n if (currentPlace !== null) {\n drop = this.getDrop(placedDrag, currentPlace);\n }\n }\n var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),\n nextDrag = $();\n\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.escape:\n break;\n\n default:\n questionManager.isKeyboardNavigation = false;\n return; // To avoid the preventDefault below.\n }\n\n if (nextDrag.length) {\n nextDrag.data('isfocus', true);\n nextDrag.addClass('beingdragged');\n var hiddenDrag = this.getDragClone(nextDrag);\n if (hiddenDrag.length) {\n if (nextDrag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));\n var cloneDrags = this.getInfiniteDragClones(nextDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = nextDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n nextDrag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n }\n } else {\n drop.data('isfocus', true);\n }\n\n e.preventDefault();\n this.sendDragToDrop(nextDrag, drop);\n };\n\n /**\n * Choose the next drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {\n var choice,\n numChoices = this.noOfChoicesInGroup(group);\n\n if (drag.length === 0) {\n choice = 1; // Was empty, so we want to select the first choice.\n } else {\n choice = this.getChoice(drag) + 1;\n }\n\n var next = this.getUnplacedChoice(group, choice);\n while (next.length === 0 && choice < numChoices) {\n choice++;\n next = this.getUnplacedChoice(group, choice);\n }\n\n return next;\n };\n\n /**\n * Choose the previous drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {\n var choice;\n\n if (drag.length === 0) {\n choice = this.noOfChoicesInGroup(group);\n } else {\n choice = this.getChoice(drag) - 1;\n }\n\n var previous = this.getUnplacedChoice(group, choice);\n while (previous.length === 0 && choice > 1) {\n choice--;\n previous = this.getUnplacedChoice(group, choice);\n }\n\n // Does this choice exist?\n return previous;\n };\n\n /**\n * Animate an object to the given destination.\n *\n * @param {jQuery} drag the element to be animated.\n * @param {jQuery} target element marking the place to move it to.\n */\n DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {\n var currentPos = drag.offset(),\n targetPos = target.offset(),\n thisQ = this;\n\n M.util.js_pending('qtype_ddimageortext-animate-' + thisQ.containerId);\n // Animate works in terms of CSS position, whereas locating an object\n // on the page works best with jQuery offset() function. So, to get\n // the right target position, we work out the required change in\n // offset() and then add that to the current CSS position.\n drag.animate(\n {\n left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,\n top: parseInt(drag.css('top')) + targetPos.top - currentPos.top\n },\n {\n duration: 'fast',\n done: function() {\n $('body').trigger('dragmoved', [drag, target, thisQ]);\n M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId);\n }\n }\n );\n };\n\n /**\n * Detect if a point is inside a given DOM node.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drop the node to check (typically a drop).\n * @return {boolean} whether the point is inside the node.\n */\n DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {\n var position = drop.offset();\n if (drop.hasClass('draghome')) {\n return pageX >= position.left && pageX < position.left + drop.outerWidth()\n && pageY >= position.top && pageY < position.top + drop.outerHeight();\n }\n return pageX >= position.left && pageX < position.left + drop.width()\n && pageY >= position.top && pageY < position.top + drop.height();\n };\n\n /**\n * Set the value of the hidden input for a place, to record what is currently there.\n *\n * @param {int} place which place to set the input value for.\n * @param {int} choice the value to set.\n */\n DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {\n this.getRoot().find('input.placeinput.place' + place).val(choice);\n };\n\n /**\n * Get the outer div for this question.\n *\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DragDropOntoImageQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n /**\n * Get drag home for a given choice.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {\n if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {\n return this.getRoot().find('.dragitemgroup' + group +\n ' .draghome.infinite' +\n '.choice' + choice +\n '.group' + group);\n }\n return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);\n };\n\n /**\n * Get an unplaced choice for a particular group.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.\n */\n DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {\n return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);\n };\n\n /**\n * Get the drag that is currently in a given place.\n *\n * @param {int} place the place number.\n * @return {jQuery} the current drag (or an empty jQuery if none).\n */\n DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {\n return this.getRoot().find('.ddarea .draghome.inplace' + place);\n };\n\n /**\n * Return the number of blanks in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of drops.\n */\n DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {\n return this.getRoot().find('.dropzone.group' + group).length;\n };\n\n /**\n * Return the number of choices in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of choices.\n */\n DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {\n return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;\n };\n\n /**\n * Return the number at the end of the CSS class name with the given prefix.\n *\n * @param {jQuery} node\n * @param {String} prefix name prefix\n * @returns {Number|null} the suffix if found, else null.\n */\n DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = node.attr('class');\n if (classes !== '') {\n var classesArr = classes.split(' ');\n for (var index = 0; index < classesArr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesArr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesArr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Get the choice number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the choice number.\n */\n DragDropOntoImageQuestion.prototype.getChoice = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'choice');\n };\n\n /**\n * Given a DOM node that is significant to this question\n * (drag, drop, ...) get the group it belongs to.\n *\n * @param {jQuery} node a DOM node.\n * @returns {Number} the group it belongs to.\n */\n DragDropOntoImageQuestion.prototype.getGroup = function(node) {\n return this.getClassnameNumericSuffix(node, 'group');\n };\n\n /**\n * Get the place number of a drop, or its corresponding hidden input.\n *\n * @param {jQuery} node the DOM node.\n * @returns {Number} the place number.\n */\n DragDropOntoImageQuestion.prototype.getPlace = function(node) {\n return this.getClassnameNumericSuffix(node, 'place');\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropOntoImageQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.dragplaceholder');\n };\n\n /**\n * Get infinite drag clones for given drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} inHome in the home area or not.\n * @returns {jQuery} the drag's clones.\n */\n DragDropOntoImageQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {\n if (inHome) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n }\n return this.getRoot().find('.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n };\n\n /**\n * Get drop for a given drag and place.\n *\n * @param {jQuery} drag the drag.\n * @param {Integer} currentPlace the current place of drag.\n * @returns {jQuery} the drop's clone.\n */\n DragDropOntoImageQuestion.prototype.getDrop = function(drag, currentPlace) {\n return this.getRoot().find('.dropzone.group' + this.getGroup(drag) + '.place' + currentPlace);\n };\n\n /**\n * Handle when the window is resized.\n */\n DragDropOntoImageQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('.ddarea .dropzone').each(function(i, dropNode) {\n $(dropNode)\n .css('left', parseInt($(dropNode).data('originX')) * parseFloat(bgRatio))\n .css('top', parseInt($(dropNode).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(dropNode, 'left top');\n });\n\n this.getRoot().find('div.droparea .draghome').not('.beingdragged').each(function(key, drag) {\n $(drag)\n .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio))\n .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(drag, 'left top');\n });\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropOntoImageQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DragDropOntoImageQuestion.prototype.handleElementScale = function(element, type) {\n var bgRatio = parseFloat(this.bgRatio());\n if (this.isPrinting) {\n bgRatio = 1;\n }\n $(element).css({\n '-webkit-transform': 'scale(' + bgRatio + ')',\n '-moz-transform': 'scale(' + bgRatio + ')',\n '-ms-transform': 'scale(' + bgRatio + ')',\n '-o-transform': 'scale(' + bgRatio + ')',\n 'transform': 'scale(' + bgRatio + ')',\n 'transform-origin': type\n });\n };\n\n /**\n * Calculate z-index value.\n *\n * @returns {number} z-index value\n */\n DragDropOntoImageQuestion.prototype.calculateZIndex = function() {\n var zIndex = 0;\n this.getRoot().find('.ddarea .dropzone, div.droparea .draghome').each(function(i, dropNode) {\n dropNode = $(dropNode);\n // Note that webkit browsers won't return the z-index value from the CSS stylesheet\n // if the element doesn't have a position specified. Instead it'll return \"auto\".\n var itemZIndex = dropNode.css('z-index') ? parseInt(dropNode.css('z-index')) : 0;\n\n if (itemZIndex > zIndex) {\n zIndex = itemZIndex;\n }\n });\n\n return zIndex;\n };\n\n /**\n * Singleton object that handles all the DragDropOntoImageQuestions\n * on the page, and deals with event dispatching.\n * @type {Object}\n */\n var questionManager = {\n\n /**\n * {boolean} ensures that the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation or not.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @param {String} containerId the id of the div.que that contains this question.\n * @param {boolean} readOnly whether the question is read-only.\n * @param {Array} places data.\n */\n init: function(containerId, readOnly, places) {\n questionManager.questions[containerId] =\n new DragDropOntoImageQuestion(containerId, readOnly, places);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $('body')\n .on('mousedown touchstart',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome',\n questionManager.handleDragStart)\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',\n questionManager.handleKeyPress)\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)',\n questionManager.handleKeyPress)\n .on('dragmoved', questionManager.handleDragMoved);\n $(window).on('resize', function() {\n questionManager.handleWindowResize(false);\n });\n window.addEventListener('beforeprint', function() {\n questionManager.isPrinting = true;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n window.addEventListener('afterprint', function() {\n questionManager.isPrinting = false;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved();\n }, 100);\n },\n\n /**\n * Handle mouse down / touch start events on drags.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on drags.\n * @param {KeyboardEvent} e\n */\n handleKeyPress: function(e) {\n if (questionManager.isKeyboardNavigation) {\n return;\n }\n questionManager.isKeyboardNavigation = true;\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * Sometimes, despite our best efforts, things change in a way that cannot\n * be specifically caught (e.g. dock expanding or collapsing in Boost).\n * Therefore, we need to periodically check everything is in the right position.\n */\n fixLayoutIfThingsMoved: function() {\n this.handleWindowResize(questionManager.isPrinting);\n // We use setTimeout after finishing work, rather than setInterval,\n // in case positioning things is slow. We want 100 ms gap\n // between executions, not what setInterval does.\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);\n }, 100);\n },\n\n /**\n * Handle when drag moved.\n *\n * @param {Event} e the event.\n * @param {jQuery} drag the drag\n * @param {jQuery} target the target\n * @param {DragDropOntoImageQuestion} thisQ the question.\n */\n handleDragMoved: function(e, drag, target, thisQ) {\n drag.removeClass('beingdragged').css('z-index', '');\n drag.css('top', target.position().top).css('left', target.position().left);\n target.after(drag);\n target.removeClass('active');\n if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {\n drag.removeClass('placed').addClass('unplaced');\n drag.removeAttr('tabindex');\n drag.removeData('unplaced');\n drag.css('top', '')\n .css('left', '')\n .css('transform', '');\n if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {\n thisQ.getInfiniteDragClones(drag, true).first().remove();\n }\n } else {\n drag.data('originX', target.data('originX')).data('originY', target.data('originY'));\n thisQ.handleElementScale(drag, 'left top');\n }\n if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {\n drag.focus();\n drag.removeData('isfocus');\n }\n if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {\n target.removeData('isfocus');\n }\n if (questionManager.isKeyboardNavigation) {\n questionManager.isKeyboardNavigation = false;\n }\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');\n return questionManager.questions[containerId];\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/question\n */\n return {\n /**\n * Initialise one drag-drop onto image question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Array} Information about the drop places.\n */\n init: questionManager.init\n };\n});\n"],"file":"question.min.js"} \ No newline at end of file diff --git a/question/type/ddimageortext/amd/src/form.js b/question/type/ddimageortext/amd/src/form.js index da384b73bf8..eca7993edfa 100644 --- a/question/type/ddimageortext/amd/src/form.js +++ b/question/type/ddimageortext/amd/src/form.js @@ -58,7 +58,7 @@ define(['jquery', 'core/dragdrop'], function($, dragDrop) { dragDropToImageForm.fp = dragDropToImageForm.filePickers(); $('#id_previewareaheader').append( - '
' + + '
' + '
' + ' ' + '
' + diff --git a/question/type/ddimageortext/amd/src/question.js b/question/type/ddimageortext/amd/src/question.js index a2327e93b47..9c7fd7b9fea 100644 --- a/question/type/ddimageortext/amd/src/question.js +++ b/question/type/ddimageortext/amd/src/question.js @@ -39,6 +39,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys this.places = places; this.allImagesLoaded = false; this.imageLoadingTimeoutId = null; + this.isPrinting = false; if (readOnly) { this.getRoot().addClass('qtype_ddimageortext-readonly'); } @@ -175,7 +176,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys if (label === '') { label = M.util.get_string('blank', 'qtype_ddimageortext'); } - root.find('.dropzones').append('
' + '' + label + ' 
'); root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2); @@ -189,8 +190,14 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys */ DragDropOntoImageQuestion.prototype.cloneDrags = function() { var thisQ = this; - this.getRoot().find('.ddarea .draghome').each(function(index, dragHome) { - thisQ.cloneDragsForOneChoice($(dragHome)); + thisQ.getRoot().find('.draghome').each(function(index, dragHome) { + var drag = $(dragHome); + var placeHolder = drag.clone(); + placeHolder.removeClass(); + placeHolder.addClass('draghome choice' + + thisQ.getChoice(drag) + ' group' + + thisQ.getGroup(drag) + ' dragplaceholder'); + drag.before(placeHolder); }); }; @@ -229,25 +236,27 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() { var thisQ = this, root = this.getRoot(), - bgPosition = this.bgImage().offset(); + bgRatio = this.bgRatio(); // Move the drops into position. root.find('.ddarea .dropzone').each(function(i, dropNode) { var drop = $(dropNode), place = thisQ.places[thisQ.getPlace(drop)]; // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation. - drop.offset({ - left: bgPosition.left + parseInt(place.xy[0]), - top: bgPosition.top + parseInt(place.xy[1])}); + drop.css('left', parseInt(place.xy[0]) * bgRatio) + .css('top', parseInt(place.xy[1]) * bgRatio); + drop.data('originX', parseInt(place.xy[0])) + .data('originY', parseInt(place.xy[1])); + thisQ.handleElementScale(drop, 'left top'); }); // First move all items back home. - root.find('.ddarea .drag').each(function(i, dragNode) { + root.find('.draghome').not('.dragplaceholder').each(function(i, dragNode) { var drag = $(dragNode), currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace'); drag.addClass('unplaced') - .removeClass('placed') - .offset(thisQ.getDragHome(thisQ.getGroup(drag), thisQ.getChoice(drag)).offset()); + .removeClass('placed'); + drag.removeAttr('tabindex'); if (currentPlace !== null) { drag.removeClass('inplace' + currentPlace); } @@ -257,39 +266,37 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys root.find('input.placeinput').each(function(i, inputNode) { var input = $(inputNode), choice = input.val(); - if (choice === '0') { + if (choice.length === 0 || (choice.length > 0 && choice === '0')) { // No item in this place. return; } var place = thisQ.getPlace(input); - thisQ.getUnplacedChoice(thisQ.getGroup(input), choice) - .removeClass('unplaced') - .addClass('placed inplace' + place) - .offset(root.find('.dropzone.place' + place).offset()); - }); - - this.bgImage().data('prev-top', bgPosition.top).data('prev-left', bgPosition.left); - }; + // Get the unplaced drag. + var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice); + // Get the clone of the drag. + var hiddenDrag = thisQ.getDragClone(unplacedDrag); + if (hiddenDrag.length) { + if (unplacedDrag.hasClass('infinite')) { + var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag)); + var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false); + if (cloneDrags.length < noOfDrags) { + var cloneDrag = unplacedDrag.clone(); + cloneDrag.removeClass('beingdragged'); + cloneDrag.removeAttr('tabindex'); + hiddenDrag.after(cloneDrag); + } else { + hiddenDrag.addClass('active'); + } + } else { + hiddenDrag.addClass('active'); + } + } - /** - * Check to see if the background image has moved. If so, refresh the layout. - */ - DragDropOntoImageQuestion.prototype.fixLayoutIfBackgroundMoved = function() { - var bgImage = this.bgImage(), - bgPosition = bgImage.offset(), - prevTop = bgImage.data('prev-top'), - prevLeft = bgImage.data('prev-left'); - if (prevLeft === undefined || prevTop === undefined) { - // Question is not set up yet. Nothing to do. - return; - } - if (prevTop === bgPosition.top && prevLeft === bgPosition.left) { - // Things have not moved. - return; - } - // We need to reposition things. - this.positionDragsAndDrops(); + // Send the drag to drop. + var drop = root.find('.dropzone.place' + place); + thisQ.sendDragToDrop(unplacedDrag, drop); + }); }; /** @@ -299,20 +306,48 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys */ DragDropOntoImageQuestion.prototype.handleDragStart = function(e) { var thisQ = this, - drag = $(e.target).closest('.drag'); + drag = $(e.target).closest('.draghome'), + currentIndex = this.calculateZIndex(), + newIndex = currentIndex + 2; var info = dragDrop.prepare(e); if (!info.start) { return; } + drag.addClass('beingdragged').css('transform', '').css('z-index', newIndex); var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace'); if (currentPlace !== null) { this.setInputValue(currentPlace, 0); drag.removeClass('inplace' + currentPlace); + var hiddenDrop = thisQ.getDrop(drag, currentPlace); + if (hiddenDrop.length) { + hiddenDrop.addClass('active'); + drag.offset(hiddenDrop.offset()); + } + } else { + var hiddenDrag = thisQ.getDragClone(drag); + if (hiddenDrag.length) { + if (drag.hasClass('infinite')) { + var noOfDrags = this.noOfDropsInGroup(thisQ.getGroup(drag)); + var cloneDrags = this.getInfiniteDragClones(drag, false); + if (cloneDrags.length < noOfDrags) { + var cloneDrag = drag.clone(); + cloneDrag.removeClass('beingdragged'); + cloneDrag.removeAttr('tabindex'); + hiddenDrag.after(cloneDrag); + drag.offset(cloneDrag.offset()); + } else { + hiddenDrag.addClass('active'); + drag.offset(hiddenDrag.offset()); + } + } else { + hiddenDrag.addClass('active'); + drag.offset(hiddenDrag.offset()); + } + } } - drag.addClass('beingdragged'); dragDrop.start(e, drag, function(x, y, drag) { thisQ.dragMove(x, y, drag); }, function(x, y, drag) { @@ -337,6 +372,14 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys drop.removeClass('valid-drag-over-drop'); } }); + this.getRoot().find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) { + var drop = $(dropNode); + if (thisQ.isPointInDrop(pageX, pageY, drop)) { + drop.addClass('valid-drag-over-drop'); + } else { + drop.removeClass('valid-drag-over-drop'); + } + }); }; /** @@ -364,6 +407,22 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys return false; // Stop the each() here. }); + root.find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) { + var placedDrag = $(placedNode); + if (!thisQ.isPointInDrop(pageX, pageY, placedDrag)) { + // Not this placed drag. + return true; + } + + // Now put this drag into the drop. + placedDrag.removeClass('valid-drag-over-drop'); + var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace'); + var drop = thisQ.getDrop(drag, currentPlace); + thisQ.sendDragToDrop(drag, drop); + placed = true; + return false; // Stop the each() here. + }); + if (!placed) { this.sendDragHome(drag); } @@ -379,15 +438,24 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys // Is there already a drag in this drop? if so, evict it. var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop)); if (oldDrag.length !== 0) { + oldDrag.addClass('beingdragged'); + oldDrag.offset(oldDrag.offset()); + var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace'); + var hiddenDrop = this.getDrop(oldDrag, currentPlace); + hiddenDrop.addClass('active'); this.sendDragHome(oldDrag); } if (drag.length === 0) { this.setInputValue(this.getPlace(drop), 0); + if (drop.data('isfocus')) { + drop.focus(); + } } else { this.setInputValue(this.getPlace(drop), this.getChoice(drag)); drag.removeClass('unplaced') .addClass('placed inplace' + this.getPlace(drop)); + drag.attr('tabindex', 0); this.animateTo(drag, drop); } }; @@ -398,11 +466,11 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys * @param {jQuery} drag the item being moved. */ DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) { - drag.removeClass('placed').addClass('unplaced'); var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace'); if (currentPlace !== null) { drag.removeClass('inplace' + currentPlace); } + drag.data('unplaced', true); this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag))); }; @@ -416,8 +484,15 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys * @param {KeyboardEvent} e */ DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) { - var drop = $(e.target).closest('.dropzone'), - currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)), + var drop = $(e.target).closest('.dropzone'); + if (drop.length === 0) { + var placedDrag = $(e.target); + var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace'); + if (currentPlace !== null) { + drop = this.getDrop(placedDrag, currentPlace); + } + } + var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)), nextDrag = $(); switch (e.keyCode) { @@ -436,9 +511,37 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys break; default: + questionManager.isKeyboardNavigation = false; return; // To avoid the preventDefault below. } + if (nextDrag.length) { + nextDrag.data('isfocus', true); + nextDrag.addClass('beingdragged'); + var hiddenDrag = this.getDragClone(nextDrag); + if (hiddenDrag.length) { + if (nextDrag.hasClass('infinite')) { + var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag)); + var cloneDrags = this.getInfiniteDragClones(nextDrag, false); + if (cloneDrags.length < noOfDrags) { + var cloneDrag = nextDrag.clone(); + cloneDrag.removeClass('beingdragged'); + cloneDrag.removeAttr('tabindex'); + hiddenDrag.after(cloneDrag); + nextDrag.offset(cloneDrag.offset()); + } else { + hiddenDrag.addClass('active'); + nextDrag.offset(hiddenDrag.offset()); + } + } else { + hiddenDrag.addClass('active'); + nextDrag.offset(hiddenDrag.offset()); + } + } + } else { + drop.data('isfocus', true); + } + e.preventDefault(); this.sendDragToDrop(nextDrag, drop); }; @@ -503,9 +606,10 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys */ DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) { var currentPos = drag.offset(), - targetPos = target.offset(); - drag.addClass('beingdragged'); + targetPos = target.offset(), + thisQ = this; + M.util.js_pending('qtype_ddimageortext-animate-' + thisQ.containerId); // Animate works in terms of CSS position, whereas locating an object // on the page works best with jQuery offset() function. So, to get // the right target position, we work out the required change in @@ -518,10 +622,8 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys { duration: 'fast', done: function() { - drag.removeClass('beingdragged'); - // It seems that the animation sometimes leaves the drag - // one pixel out of position. Put it in exactly the right place. - drag.offset(targetPos); + $('body').trigger('dragmoved', [drag, target, thisQ]); + M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId); } } ); @@ -537,6 +639,10 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys */ DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) { var position = drop.offset(); + if (drop.hasClass('draghome')) { + return pageX >= position.left && pageX < position.left + drop.outerWidth() + && pageY >= position.top && pageY < position.top + drop.outerHeight(); + } return pageX >= position.left && pageX < position.left + drop.width() && pageY >= position.top && pageY < position.top + drop.height(); }; @@ -576,7 +682,13 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys * @returns {jQuery} containing that div. */ DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) { - return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice); + if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) { + return this.getRoot().find('.dragitemgroup' + group + + ' .draghome.infinite' + + '.choice' + choice + + '.group' + group); + } + return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice); }; /** @@ -587,7 +699,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty. */ DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) { - return this.getRoot().find('.ddarea .drag.group' + group + '.choice' + choice + '.unplaced').slice(0, 1); + return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1); }; /** @@ -597,7 +709,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys * @return {jQuery} the current drag (or an empty jQuery if none). */ DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) { - return this.getRoot().find('.ddarea .drag.inplace' + place); + return this.getRoot().find('.ddarea .draghome.inplace' + place); }; /** @@ -675,6 +787,134 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys }; /** + * Get drag clone for a given drag. + * + * @param {jQuery} drag the drag. + * @returns {jQuery} the drag's clone. + */ + DragDropOntoImageQuestion.prototype.getDragClone = function(drag) { + return this.getRoot().find('.dragitemgroup' + + this.getGroup(drag) + + ' .draghome' + + '.choice' + this.getChoice(drag) + + '.group' + this.getGroup(drag) + + '.dragplaceholder'); + }; + + /** + * Get infinite drag clones for given drag. + * + * @param {jQuery} drag the drag. + * @param {Boolean} inHome in the home area or not. + * @returns {jQuery} the drag's clones. + */ + DragDropOntoImageQuestion.prototype.getInfiniteDragClones = function(drag, inHome) { + if (inHome) { + return this.getRoot().find('.dragitemgroup' + + this.getGroup(drag) + + ' .draghome' + + '.choice' + this.getChoice(drag) + + '.group' + this.getGroup(drag) + + '.infinite').not('.dragplaceholder'); + } + return this.getRoot().find('.draghome' + + '.choice' + this.getChoice(drag) + + '.group' + this.getGroup(drag) + + '.infinite').not('.dragplaceholder'); + }; + + /** + * Get drop for a given drag and place. + * + * @param {jQuery} drag the drag. + * @param {Integer} currentPlace the current place of drag. + * @returns {jQuery} the drop's clone. + */ + DragDropOntoImageQuestion.prototype.getDrop = function(drag, currentPlace) { + return this.getRoot().find('.dropzone.group' + this.getGroup(drag) + '.place' + currentPlace); + }; + + /** + * Handle when the window is resized. + */ + DragDropOntoImageQuestion.prototype.handleResize = function() { + var thisQ = this, + bgRatio = this.bgRatio(); + if (this.isPrinting) { + bgRatio = 1; + } + + this.getRoot().find('.ddarea .dropzone').each(function(i, dropNode) { + $(dropNode) + .css('left', parseInt($(dropNode).data('originX')) * parseFloat(bgRatio)) + .css('top', parseInt($(dropNode).data('originY')) * parseFloat(bgRatio)); + thisQ.handleElementScale(dropNode, 'left top'); + }); + + this.getRoot().find('div.droparea .draghome').not('.beingdragged').each(function(key, drag) { + $(drag) + .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio)) + .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio)); + thisQ.handleElementScale(drag, 'left top'); + }); + }; + + /** + * Return the background ratio. + * + * @returns {number} Background ratio. + */ + DragDropOntoImageQuestion.prototype.bgRatio = function() { + var bgImg = this.bgImage(); + var bgImgNaturalWidth = bgImg.get(0).naturalWidth; + var bgImgClientWidth = bgImg.width(); + + return bgImgClientWidth / bgImgNaturalWidth; + }; + + /** + * Scale the drag if needed. + * + * @param {jQuery} element the item to place. + * @param {String} type scaling type + */ + DragDropOntoImageQuestion.prototype.handleElementScale = function(element, type) { + var bgRatio = parseFloat(this.bgRatio()); + if (this.isPrinting) { + bgRatio = 1; + } + $(element).css({ + '-webkit-transform': 'scale(' + bgRatio + ')', + '-moz-transform': 'scale(' + bgRatio + ')', + '-ms-transform': 'scale(' + bgRatio + ')', + '-o-transform': 'scale(' + bgRatio + ')', + 'transform': 'scale(' + bgRatio + ')', + 'transform-origin': type + }); + }; + + /** + * Calculate z-index value. + * + * @returns {number} z-index value + */ + DragDropOntoImageQuestion.prototype.calculateZIndex = function() { + var zIndex = 0; + this.getRoot().find('.ddarea .dropzone, div.droparea .draghome').each(function(i, dropNode) { + dropNode = $(dropNode); + // Note that webkit browsers won't return the z-index value from the CSS stylesheet + // if the element doesn't have a position specified. Instead it'll return "auto". + var itemZIndex = dropNode.css('z-index') ? parseInt(dropNode.css('z-index')) : 0; + + if (itemZIndex > zIndex) { + zIndex = itemZIndex; + } + }); + + return zIndex; + }; + + /** * Singleton object that handles all the DragDropOntoImageQuestions * on the page, and deals with event dispatching. * @type {Object} @@ -687,6 +927,16 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys eventHandlersInitialised: false, /** + * {boolean} is printing or not. + */ + isPrinting: false, + + /** + * {boolean} is keyboard navigation or not. + */ + isKeyboardNavigation: false, + + /** * {Object} all the questions on this page, indexed by containerId (id on the .que div). */ questions: {}, // An object containing all the information about each question on the page. @@ -713,13 +963,29 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys setupEventHandlers: function() { $('body') .on('mousedown touchstart', - '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dragitems .drag', + '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome', questionManager.handleDragStart) .on('keydown', '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone', - questionManager.handleKeyPress); - $(window).on('resize', questionManager.handleWindowResize); - setTimeout(questionManager.fixLayoutIfThingsMoved, 100); + questionManager.handleKeyPress) + .on('keydown', + '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)', + questionManager.handleKeyPress) + .on('dragmoved', questionManager.handleDragMoved); + $(window).on('resize', function() { + questionManager.handleWindowResize(false); + }); + window.addEventListener('beforeprint', function() { + questionManager.isPrinting = true; + questionManager.handleWindowResize(questionManager.isPrinting); + }); + window.addEventListener('afterprint', function() { + questionManager.isPrinting = false; + questionManager.handleWindowResize(questionManager.isPrinting); + }); + setTimeout(function() { + questionManager.fixLayoutIfThingsMoved(); + }, 100); }, /** @@ -739,6 +1005,10 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys * @param {KeyboardEvent} e */ handleKeyPress: function(e) { + if (questionManager.isKeyboardNavigation) { + return; + } + questionManager.isKeyboardNavigation = true; var question = questionManager.getQuestionForEvent(e); if (question) { question.handleKeyPress(e); @@ -747,11 +1017,13 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys /** * Handle when the window is resized. + * @param {boolean} isPrinting */ - handleWindowResize: function() { + handleWindowResize: function(isPrinting) { for (var containerId in questionManager.questions) { if (questionManager.questions.hasOwnProperty(containerId)) { - questionManager.questions[containerId].positionDragsAndDrops(); + questionManager.questions[containerId].isPrinting = isPrinting; + questionManager.questions[containerId].handleResize(); } } }, @@ -762,16 +1034,52 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys * Therefore, we need to periodically check everything is in the right position. */ fixLayoutIfThingsMoved: function() { - for (var containerId in questionManager.questions) { - if (questionManager.questions.hasOwnProperty(containerId)) { - questionManager.questions[containerId].fixLayoutIfBackgroundMoved(); - } - } - + this.handleWindowResize(questionManager.isPrinting); // We use setTimeout after finishing work, rather than setInterval, // in case positioning things is slow. We want 100 ms gap // between executions, not what setInterval does. - setTimeout(questionManager.fixLayoutIfThingsMoved, 100); + setTimeout(function() { + questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting); + }, 100); + }, + + /** + * Handle when drag moved. + * + * @param {Event} e the event. + * @param {jQuery} drag the drag + * @param {jQuery} target the target + * @param {DragDropOntoImageQuestion} thisQ the question. + */ + handleDragMoved: function(e, drag, target, thisQ) { + drag.removeClass('beingdragged').css('z-index', ''); + drag.css('top', target.position().top).css('left', target.position().left); + target.after(drag); + target.removeClass('active'); + if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) { + drag.removeClass('placed').addClass('unplaced'); + drag.removeAttr('tabindex'); + drag.removeData('unplaced'); + drag.css('top', '') + .css('left', '') + .css('transform', ''); + if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) { + thisQ.getInfiniteDragClones(drag, true).first().remove(); + } + } else { + drag.data('originX', target.data('originX')).data('originY', target.data('originY')); + thisQ.handleElementScale(drag, 'left top'); + } + if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) { + drag.focus(); + drag.removeData('isfocus'); + } + if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) { + target.removeData('isfocus'); + } + if (questionManager.isKeyboardNavigation) { + questionManager.isKeyboardNavigation = false; + } }, /** diff --git a/question/type/ddimageortext/rendererbase.php b/question/type/ddimageortext/rendererbase.php index bdc15b43e5b..c7d348d6197 100644 --- a/question/type/ddimageortext/rendererbase.php +++ b/question/type/ddimageortext/rendererbase.php @@ -58,16 +58,23 @@ class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_rendere $questiontext = $question->format_questiontext($qa); - $output = html_writer::tag('div', $questiontext, array('class' => 'qtext')); + $dropareaclass = 'droparea'; + $draghomesclass = 'draghomes'; + if ($options->readonly) { + $dropareaclass .= ' readonly'; + $draghomesclass .= ' readonly'; + } - $bgimage = self::get_url_for_image($qa, 'bgimage'); + $output = html_writer::div($questiontext, 'qtext'); - $img = html_writer::empty_tag('img', array( - 'src' => $bgimage, 'class' => 'dropbackground', - 'alt' => get_string('dropbackground', 'qtype_ddimageortext'))); - $dropzones = html_writer::tag('div', '', array('class' => 'dropzones')); + $output .= html_writer::start_div('ddarea'); + $output .= html_writer::start_div($dropareaclass); + $output .= html_writer::img(self::get_url_for_image($qa, 'bgimage'), get_string('dropbackground', 'qtype_ddmarker'), + ['class' => 'dropbackground img-responsive img-fluid']); - $droparea = html_writer::tag('div', $img . $dropzones, array('class' => 'droparea')); + $output .= html_writer::div('', 'dropzones'); + $output .= html_writer::end_div(); + $output .= html_writer::start_div($draghomesclass); $dragimagehomes = ''; foreach ($question->choices as $groupno => $group) { @@ -75,51 +82,42 @@ class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_rendere $orderedgroup = $question->get_ordered_choices($groupno); foreach ($orderedgroup as $choiceno => $dragimage) { $dragimageurl = self::get_url_for_image($qa, 'dragimage', $dragimage->id); - $classes = array("group{$groupno}", - 'draghome', - "choice{$choiceno}"); + $classes = [ + 'group' . $groupno, + 'draghome', + 'choice' . $choiceno + ]; if ($dragimage->infinite) { $classes[] = 'infinite'; } if ($dragimageurl === null) { - $dragimagehomesgroup .= html_writer::tag('div', $dragimage->text, - array('src' => $dragimageurl, 'class' => join(' ', $classes))); + $dragimagehomesgroup .= html_writer::div($dragimage->text, join(' ', $classes), ['src' => $dragimageurl]); } else { - $dragimagehomesgroup .= html_writer::empty_tag('img', - array('src' => $dragimageurl, 'alt' => $dragimage->text, - 'class' => join(' ', $classes))); + $dragimagehomesgroup .= html_writer::img($dragimageurl, $dragimage->text, ['class' => join(' ', $classes)]); } } - $dragimagehomes .= html_writer::tag('div', $dragimagehomesgroup, - array('class' => 'dragitemgroup' . $groupno)); + $dragimagehomes .= html_writer::div($dragimagehomesgroup, 'dragitemgroup' . $groupno); } - $draghomes = html_writer::tag('div', $dragimagehomes, array('class' => 'draghomes')); - $dragitemsclass = 'dragitems'; - if ($options->readonly) { - $dragitemsclass .= ' readonly'; - } - $dragitems = html_writer::tag('div', '', array('class' => $dragitemsclass)); + $output .= $dragimagehomes; + $output .= html_writer::end_div(); - $hiddens = ''; foreach ($question->places as $placeno => $place) { $varname = $question->field($placeno); list($fieldname, $html) = $this->hidden_field_for_qt_var($qa, $varname, null, ['placeinput', 'place' . $placeno, 'group' . $place->group]); - $hiddens .= $html; + $output .= $html; $question->places[$placeno]->fieldname = $fieldname; } - $output .= html_writer::tag('div', - $droparea . $draghomes. $dragitems . $hiddens, array('class' => 'ddarea')); + + $output .= html_writer::end_div(); $this->page->requires->string_for_js('blank', 'qtype_ddimageortext'); $this->page->requires->js_call_amd('qtype_ddimageortext/question', 'init', [$qa->get_outer_question_div_unique_id(), $options->readonly, $question->places]); if ($qa->get_state() == question_state::$invalid) { - $output .= html_writer::nonempty_tag('div', - $question->get_validation_error($qa->get_last_qt_data()), - array('class' => 'validationerror')); + $output .= html_writer::div($question->get_validation_error($qa->get_last_qt_data()), 'validationerror'); } return $output; } diff --git a/question/type/ddimageortext/styles.css b/question/type/ddimageortext/styles.css index 8f5b95f0c1f..3b93673bd16 100644 --- a/question/type/ddimageortext/styles.css +++ b/question/type/ddimageortext/styles.css @@ -13,22 +13,51 @@ form.mform fieldset#id_previewareaheader div.ddarea { position: relative; } +.que.ddimageortext div.droparea { + display: inline-block; +} + +.que.ddimageortext div.droparea .draghome { + position: absolute; + cursor: move; + white-space: nowrap; +} + +.que.ddimageortext div.droparea .dropzones { + position: absolute; + top: 0; + left: 0; +} + .que.ddimageortext .dropbackground, form.mform fieldset#id_previewareaheader .dropbackground { border: 1px solid #000; - max-width: none; margin: 0 auto; } +form.mform fieldset#id_previewareaheader .dropbackground { + max-width: none; +} + +.que.ddimageortext .dropbackground.img-responsive.img-fluid { + width: 100%; +} + .que.ddimageortext .dropzone { + display: none; position: absolute; opacity: 0.5; border: 1px solid black; - z-index: 1; +} + +.que.ddimageortext .dropzone.active { + display: block; } .que.ddimageortext .dropzone:focus, -.que.ddimageortext .dropzone.valid-drag-over-drop { +.que.ddimageortext .droparea .draghome:focus, +.que.ddimageortext .dropzone.valid-drag-over-drop, +.que.ddimageortext .draghome.placed.valid-drag-over-drop { border-color: #0a0; box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1); outline: 0; @@ -42,12 +71,26 @@ form.mform fieldset#id_previewareaheader .droppreview { font: 13px/1.231 arial, helvetica, clean, sans-serif; } -.que.ddimageortext .draghome { +.que.ddimageortext .draghomes .draghome { vertical-align: top; margin: 5px; - visibility: hidden; height: auto; width: auto; + cursor: move; +} + +.que.ddimageortext .draghomes.readonly .draghome, +.que.ddimageortext .droparea.readonly .draghome { + cursor: auto; +} + +.que.ddimageortext .draghomes .draghome.dragplaceholder { + display: none; +} + +.que.ddimageortext .draghomes .draghome.dragplaceholder.active { + visibility: hidden; + display: inline-block; } .que.ddimageortext .dragitems, @@ -59,7 +102,6 @@ form.mform fieldset#id_previewareaheader .dragitems { form.mform fieldset#id_previewareaheader .droppreview { position: absolute; cursor: move; - z-index: 2; } .que.ddimageortext .dragitems.readonly .drag { @@ -67,11 +109,17 @@ form.mform fieldset#id_previewareaheader .droppreview { } form.mform fieldset#id_previewareaheader .drag.beingdragged, -.que.ddimageortext .drag.beingdragged { - z-index: 3; +.que.ddimageortext .drag.beingdragged, +.que.ddimageortext .draghomes .draghome.beingdragged, +.que.ddimageortext .droparea .draghome.beingdragged { box-shadow: 3px 3px 4px #000; } +.que.ddimageortext .draghomes .draghome.beingdragged, +.que.ddimageortext .droparea .draghome.beingdragged { + position: absolute; +} + .que.ddimageortext .group1, form.mform fieldset#id_previewareaheader .group1 { background-color: #fff; diff --git a/question/type/ddimageortext/tests/behat/behat_qtype_ddimageortext.php b/question/type/ddimageortext/tests/behat/behat_qtype_ddimageortext.php index ac893d482e0..ced73dcacf5 100644 --- a/question/type/ddimageortext/tests/behat/behat_qtype_ddimageortext.php +++ b/question/type/ddimageortext/tests/behat/behat_qtype_ddimageortext.php @@ -41,8 +41,8 @@ class behat_qtype_ddimageortext extends behat_base { * @return string the xpath expression. */ protected function drag_xpath($dragitem) { - return '//div[contains(concat(" ", @class, " "), " drag ") and ' . - 'contains(normalize-space(.), "' . $this->escape($dragitem) . '")]'; + return '//div[contains(concat(" ", @class, " "), " draghome ") and ' . + 'contains(normalize-space(.), "' . $this->escape($dragitem) . '") and not(contains(@class, "dragplaceholder"))]'; } /** @@ -84,6 +84,7 @@ class behat_qtype_ddimageortext extends behat_base { $node->keyDown($key); $node->keyPress($key); $node->keyUp($key); + $this->wait_for_pending_js(); } } } -- 2.11.4.GIT