MDL-63297 qtype_ddimageortext: re-implement JavaScript in AMD

this kills of a bunch of old YUI code.

Should also make this question type work a bit mroe robustly.
This commit is contained in:
Tim Hunt
2018-10-29 10:40:51 +00:00
parent 448bd578d8
commit ebf91776d7
22 changed files with 1387 additions and 2868 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+541
View File
@@ -0,0 +1,541 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/*
* JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.
*
* @module qtype_ddimageortext/form
* @package qtype_ddimageortext
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/dragdrop'], function($, dragDrop) {
"use strict";
/**
* Singleton object to handle progressive enhancement of the
* drag-drop onto image question editing form.
* @type {Object}
*/
var dragDropToImageForm = {
/**
* @var {Object} with properties width and height.
*/
maxBgImageSize: null,
/**
* @var {Object} with properties width and height.
*/
maxDragImageSize: null,
/**
* @var {object} for interacting with the file pickers.
*/
fp: null, // Object containing functions associated with the file picker.
/**
* Initialise the form javascript features.
*
* @param {Object} maxBgImageSize object with two properties: width and height.
* @param {Object} maxDragImageSize object with two properties: width and height.
*/
init: function(maxBgImageSize, maxDragImageSize) {
dragDropToImageForm.maxBgImageSize = maxBgImageSize;
dragDropToImageForm.maxDragImageSize = maxDragImageSize;
dragDropToImageForm.fp = dragDropToImageForm.filePickers();
$('#id_previewareaheader').append(
'<div class="ddarea">' +
' <div class="droparea">' +
' <img class="dropbackground" />' +
' <div class="dropzones"></div>' +
' </div>' +
' <div class="dragitems"></div>' +
'</div>');
dragDropToImageForm.updateVisibilityOfFilePickers();
dragDropToImageForm.setOptionsForDragItemSelectors();
dragDropToImageForm.setupEventHandlers();
dragDropToImageForm.waitForFilePickerToInitialise();
},
/**
* Waits for the file-pickers to be sufficiently ready before initialising the preview.
*/
waitForFilePickerToInitialise: function() {
if (dragDropToImageForm.fp.file('bgimage').href === null) {
// It would be better to use an onload or onchange event rather than this timeout.
// Unfortunately attempts to do this early are overwritten by filepicker during its loading.
setTimeout(dragDropToImageForm.waitForFilePickerToInitialise, 1000);
return;
}
M.util.js_pending('dragDropToImageForm');
// From now on, when a new file gets loaded into the filepicker, update the preview.
// This is not in the setupEventHandlers section as it needs to be delayed until
// after filepicker's javascript has finished.
$('form.mform').on('change', '.filepickerhidden', function() {
M.util.js_pending('dragDropToImageForm');
dragDropToImageForm.loadPreviewImage();
});
dragDropToImageForm.loadPreviewImage();
},
/**
* Loads the preview background image.
*/
loadPreviewImage: function() {
$('fieldset#id_previewareaheader .dropbackground')
.one('load', dragDropToImageForm.afterPreviewImageLoaded)
.attr('src', dragDropToImageForm.fp.file('bgimage').href);
},
/**
* After the background image is loaded, continue setting up the preview.
*/
afterPreviewImageLoaded: function() {
var bgImg = $('fieldset#id_previewareaheader .dropbackground');
dragDropToImageForm.constrainImageSize(bgImg, dragDropToImageForm.maxBgImageSize);
dragDropToImageForm.createDropZones();
M.util.js_complete('dragDropToImageForm');
},
/**
* Limits an image display size to the given maximums.
*
* @param {jQuery} img the image.
* @param {Object} maxSize with width and height properties.
*/
constrainImageSize: function(img, maxSize) {
var reduceby = Math.max(img.width() / maxSize.width,
img.height() / maxSize.height);
if (reduceby > 1) {
img.css('width', Math.floor(img.width() / reduceby));
}
img.addClass('constrained');
},
/**
* Create, or recreate all the drop zones.
*/
createDropZones: function() {
var dropZoneHolder = $('.dropzones');
dropZoneHolder.empty();
var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;
if (bgimageurl === null) {
return; // There is not currently a valid preview to update.
}
var numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);
for (var dropNo = 0; dropNo < numDrops; dropNo++) {
var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']);
if (dragNo === '0') {
continue;
}
dragNo = dragNo - 1;
var group = dragDropToImageForm.form.getFormValue('drags', [dragNo, 'draggroup']),
label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);
if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {
var imgUrl = dragDropToImageForm.fp.file('dragitem[' + dragNo + ']').href;
if (imgUrl === null) {
continue;
}
// Althoug these are previews of drops, we also add the class name 'drag',
dropZoneHolder.append('<img class="droppreview group' + group + ' drop' + dropNo +
'" src="' + imgUrl + '" alt="' + label + '" data-drop-no="' + dropNo + '">');
} else if (label !== '') {
dropZoneHolder.append('<div class="droppreview group' + group + ' drop' + dropNo +
'" data-drop-no="' + dropNo + '">' + label + '</div>');
}
}
dragDropToImageForm.waitForAllDropImagesToBeLoaded();
},
/**
* This polls until all the drop-zone images have loaded, and then calls updateDropZones().
*/
waitForAllDropImagesToBeLoaded: function() {
var notYetLoadedImages = $('.dropzones img').not(function(i, imgNode) {
return dragDropToImageForm.imageIsLoaded(imgNode);
});
if (notYetLoadedImages.length > 0) {
setTimeout(function() {
dragDropToImageForm.waitForAllDropImagesToBeLoaded();
}, 100);
return;
}
dragDropToImageForm.updateDropZones();
},
/**
* Check if an image has loaded without errors.
*
* @param {HTMLImageElement} imgElement an image.
* @returns {boolean} true if this image has loaded without errors.
*/
imageIsLoaded: function(imgElement) {
return imgElement.complete && imgElement.naturalHeight !== 0;
},
/**
* Set the size and position of all the drop zones.
*/
updateDropZones: function() {
var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;
if (bgimageurl === null) {
return; // There is not currently a valid preview to update.
}
var dropBackgroundPosition = $('fieldset#id_previewareaheader .dropbackground').offset(),
numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);
// Move each drop to the right position and update the text.
for (var dropNo = 0; dropNo < numDrops; dropNo++) {
var drop = $('.dropzones .drop' + dropNo);
if (drop.length === 0) {
continue;
}
var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']) - 1;
drop.offset({
left: dropBackgroundPosition.left +
parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'xleft'])),
top: dropBackgroundPosition.top +
parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'ytop']))
});
var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);
if (drop.is('img')) {
drop.attr('alt', label);
} else {
drop.html(label);
}
}
// Resize them to the same size.
$('.dropzones .droppreview').css('padding', '0');
var numGroups = $('select.draggroup').first().find('option').length;
for (var group = 1; group <= numGroups; group++) {
dragDropToImageForm.resizeAllDragsAndDropsInGroup(group);
}
},
/**
* In a given group, set all the drags and drops to be the same size.
*
* @param {int} group the group number.
*/
resizeAllDragsAndDropsInGroup: function(group) {
var drops = $('.dropzones .droppreview.group' + group),
maxWidth = 0,
maxHeight = 0;
// Find the maximum size of any drag in this groups.
drops.each(function(i, drop) {
maxWidth = Math.max(maxWidth, Math.ceil(drop.offsetWidth));
maxHeight = Math.max(maxHeight, Math.ceil(drop.offsetHeight));
});
// The size we will want to set is a bit bigger than this.
maxWidth += 10;
maxHeight += 10;
// Set each drag home to that size.
drops.each(function(i, drop) {
var left = Math.round((maxWidth - drop.offsetWidth) / 2),
top = Math.floor((maxHeight - drop.offsetHeight) / 2);
// Set top and left padding so the item is centred.
$(drop).css({
'padding-left': left + 'px',
'padding-right': (maxWidth - drop.offsetWidth - left) + 'px',
'padding-top': top + 'px',
'padding-bottom': (maxHeight - drop.offsetHeight - top) + 'px'
});
});
},
/**
* Events linked to form actions.
*/
setupEventHandlers: function() {
// Changes to settings in the draggable items section.
$('fieldset#id_draggableitemheader')
.on('change input', 'input, select', function(e) {
var input = $(e.target).closest('select, input');
if (input.hasClass('dragitemtype')) {
dragDropToImageForm.updateVisibilityOfFilePickers();
}
dragDropToImageForm.setOptionsForDragItemSelectors();
if (input.is('.dragitemtype, .draggroup')) {
dragDropToImageForm.createDropZones();
} else if (input.is('.draglabel')) {
dragDropToImageForm.updateDropZones();
}
});
// Changes to Drop zones section: left, top and drag item.
$('fieldset#id_dropzoneheader').on('change input', 'input, select', function(e) {
var input = $(e.target).closest('select, input');
if (input.is('select')) {
dragDropToImageForm.createDropZones();
} else {
dragDropToImageForm.updateDropZones();
}
});
// Moving drop zones in the preview.
$('fieldset#id_previewareaheader').on('mousedown touchstart', '.droppreview', function(e) {
dragDropToImageForm.dragStart(e);
});
$(window).on('resize', function() {
dragDropToImageForm.updateDropZones();
});
},
/**
* Update all the drag item filepickers, so they are only shown for
*/
updateVisibilityOfFilePickers: function() {
var numDrags = dragDropToImageForm.form.getFormValue('noitems', []);
for (var dragNo = 0; dragNo < numDrags; dragNo++) {
var picker = $('input#id_dragitem_' + dragNo).closest('.fitem_ffilepicker');
if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {
picker.show();
} else {
picker.hide();
}
}
},
setOptionsForDragItemSelectors: function() {
var dragItemOptions = {'0': ''},
numDrags = dragDropToImageForm.form.getFormValue('noitems', []),
numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);
// Work out the list of options.
for (var dragNo = 0; dragNo < numDrags; dragNo++) {
var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);
var file = dragDropToImageForm.fp.file(dragDropToImageForm.form.toNameWithIndex('dragitem', [dragNo]));
if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype']) && file.name !== null) {
dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label + ' (' + file.name + ')';
} else if (label !== '') {
dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label;
}
}
// Initialise each select.
for (var dropNo = 0; dropNo < numDrops; dropNo++) {
var selector = $('#id_drops_' + dropNo + '_choice');
var selectedvalue = selector.val();
selector.find('option').remove();
for (var value in dragItemOptions) {
if (!dragItemOptions.hasOwnProperty(value)) {
continue;
}
selector.append('<option value="' + value + '">' + dragItemOptions[value] + '</option>');
var optionnode = selector.find('option[value="' + value + '"]');
if (parseInt(value) === parseInt(selectedvalue)) {
optionnode.attr('selected', true);
} else if (dragDropToImageForm.isItemUsed(parseInt(value))) {
optionnode.attr('disabled', true);
}
}
}
},
/**
* Checks if the specified drag option is already used somewhere.
*
* @param {Number} value of the drag item to check
* @return {Boolean} true if item is allocated to dropzone
*/
isItemUsed: function(value) {
if (value === 0) {
return false; // None option can always be selected.
}
if (dragDropToImageForm.form.getFormValue('drags', [value - 1, 'infinite'])) {
return false; // Infinite, so can't be used up.
}
return $('fieldset#id_dropzoneheader select').filter(function(i, selectNode) {
return parseInt($(selectNode).val()) === value;
}).length !== 0;
},
/**
* Handles when a dropzone in dragged in the preview.
* @param {Object} e Event object
*/
dragStart: function(e) {
var drop = $(e.target).closest('.droppreview');
var info = dragDrop.prepare(e);
if (!info.start) {
return;
}
dragDrop.start(e, drop, function(x, y, drop) {
dragDropToImageForm.dragMove(drop);
}, function() {
dragDropToImageForm.dragEnd();
});
},
/**
* Handles update while a drop is being dragged.
*
* @param {jQuery} drop the drop preview being moved.
*/
dragMove: function(drop) {
var backgroundImage = $('fieldset#id_previewareaheader .dropbackground'),
backgroundPosition = backgroundImage.offset(),
dropNo = drop.data('dropNo'),
dropPosition = drop.offset(),
left = Math.round(dropPosition.left - backgroundPosition.left),
top = Math.round(dropPosition.top - backgroundPosition.top);
// Constrain coordinates to be inside the background.
// The -10 here matches the +10 in resizeAllDragsAndDropsInGroup().
left = Math.max(0, Math.min(left, backgroundImage.width() - drop.width() - 10));
top = Math.max(0, Math.min(top, backgroundImage.height() - drop.height() - 10));
// Update the form.
dragDropToImageForm.form.setFormValue('drops', [dropNo, 'xleft'], left);
dragDropToImageForm.form.setFormValue('drops', [dropNo, 'ytop'], top);
},
/**
* Handles when the drag ends.
*/
dragEnd: function() {
// Redraw, in case the position was constrained.
dragDropToImageForm.updateDropZones();
},
/**
* Low level operations on form.
*/
form: {
toNameWithIndex: function(name, indexes) {
var indexString = name;
for (var i = 0; i < indexes.length; i++) {
indexString = indexString + '[' + indexes[i] + ']';
}
return indexString;
},
getEl: function(name, indexes) {
var form = document.getElementById('mform1');
return form.elements[this.toNameWithIndex(name, indexes)];
},
/**
* Helper to get the value of a form elements with name like "drops[0][xleft]".
*
* @param {String} name the base name, e.g. 'drops'.
* @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].
* @return {String} the value of that field.
*/
getFormValue: function(name, indexes) {
var el = this.getEl(name, indexes);
if (!el.type) {
el = el[el.length - 1];
}
if (el.type === 'checkbox') {
return el.checked;
} else {
return el.value;
}
},
/**
* Helper to get the value of a form elements with name like "drops[0][xleft]".
*
* @param {String} name the base name, e.g. 'drops'.
* @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].
* @param {String|Number} value the value to set.
*/
setFormValue: function(name, indexes, value) {
var el = this.getEl(name, indexes);
if (el.type === 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
}
},
/**
* Utility to get the file name and url from the filepicker.
* @returns {Object} object containing functions {file, name}
*/
filePickers: function() {
var draftItemIdsToName;
var nameToParentNode;
if (draftItemIdsToName === undefined) {
draftItemIdsToName = {};
nameToParentNode = {};
var fp = $('form.mform input.filepickerhidden');
fp.each(function(index, filepicker) {
draftItemIdsToName[filepicker.value] = filepicker.name;
nameToParentNode[filepicker.name] = filepicker.parentNode;
});
}
return {
file: function(name) {
var parentNode = $(nameToParentNode[name]);
var fileAnchor = parentNode.find('div.filepicker-filelist a');
if (fileAnchor.length) {
return {href: fileAnchor.get(0).href, name: fileAnchor.get(0).innerHTML};
} else {
return {href: null, name: null};
}
},
name: function(draftitemid) {
return draftItemIdsToName[draftitemid];
}
};
}
};
/**
* @alias module:qtype_ddimageortext/form
*/
return {
/**
* Initialise the form JavaScript features.
*
* @param {Object} maxBgImageSize object with two properties: width and height.
* @param {Object} maxDragImageSize object with two properties: width and height.
*/
init: dragDropToImageForm.init
};
});
@@ -0,0 +1,760 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/*
* JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.
*
* @module qtype_ddimageortext/question
* @package qtype_ddimageortext
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) {
"use strict";
/**
* Initialise one drag-drop onto image question.
*
* @param {String} containerId id of the outer div for this question.
* @param {boolean} readOnly whether the question is being displayed read-only.
* @param {Array} places Information about the drop places.
* @constructor
*/
function DragDropOntoImageQuestion(containerId, readOnly, places) {
this.containerId = containerId;
M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);
this.places = places;
this.allImagesLoaded = false;
this.imageLoadingTimeoutId = null;
if (readOnly) {
this.getRoot().addClass('qtype_ddimageortext-readonly');
}
var thisQ = this;
this.getNotYetLoadedImages().one('load', function() {
thisQ.waitForAllImagesToBeLoaded();
});
this.waitForAllImagesToBeLoaded();
}
/**
* Waits until all images are loaded before calling setupQuestion().
*
* This function is called from the onLoad of each image, and also polls with
* a time-out, because image on-loads are allegedly unreliable.
*/
DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {
var thisQ = this;
// This method may get called multiple times (via image on-loads or timeouts.
// If we are already done, don't do it again.
if (this.allImagesLoaded) {
return;
}
// Clear any current timeout, if set.
if (this.imageLoadingTimeoutId !== null) {
clearTimeout(this.imageLoadingTimeoutId);
}
// If we have not yet loaded all images, set a timeout to
// call ourselves again, since apparently images on-load
// events are flakey.
if (this.getNotYetLoadedImages().length > 0) {
this.imageLoadingTimeoutId = setTimeout(function() {
thisQ.waitForAllImagesToBeLoaded();
}, 100);
return;
}
// We now have all images. Carry on, but only after giving the layout a chance to settle down.
this.allImagesLoaded = true;
thisQ.setupQuestion();
};
/**
* Get any of the images in the drag-drop area that are not yet fully loaded.
*
* @returns {jQuery} those images.
*/
DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {
var thisQ = this;
return this.getRoot().find('.ddarea img').not(function(i, imgNode) {
return thisQ.imageIsLoaded(imgNode);
});
};
/**
* Check if an image has loaded without errors.
*
* @param {HTMLImageElement} imgElement an image.
* @returns {boolean} true if this image has loaded without errors.
*/
DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {
return imgElement.complete && imgElement.naturalHeight !== 0;
};
/**
* Set up the question, once all images have been loaded.
*/
DragDropOntoImageQuestion.prototype.setupQuestion = function() {
this.resizeAllDragsAndDrops();
this.cloneDrags();
this.positionDragsAndDrops();
M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);
};
/**
* In each group, resize all the items to be the same size.
*/
DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {
var thisQ = this;
this.getRoot().find('.draghomes > div').each(function(i) {
thisQ.resizeAllDragsAndDropsInGroup(i + 1);
});
};
/**
* In a given group, set all the drags and drops to be the same size.
*
* @param {int} group the group number.
*/
DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {
var root = this.getRoot(),
dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),
maxWidth = 0,
maxHeight = 0;
// Find the maximum size of any drag in this groups.
dragHomes.each(function(i, drag) {
maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));
maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));
});
// The size we will want to set is a bit bigger than this.
maxWidth += 10;
maxHeight += 10;
// Set each drag home to that size.
dragHomes.each(function(i, drag) {
var left = Math.round((maxWidth - drag.offsetWidth) / 2),
top = Math.floor((maxHeight - drag.offsetHeight) / 2);
// Set top and left padding so the item is centred.
$(drag).css({
'padding-left': left + 'px',
'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',
'padding-top': top + 'px',
'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'
});
});
// Create the drops and make them the right size.
for (var i in this.places) {
if (!this.places.hasOwnProperty((i))) {
continue;
}
var place = this.places[i],
label = place.text;
if (parseInt(place.group) !== group) {
continue;
}
if (label === '') {
label = M.util.get_string('blank', 'qtype_ddimageortext');
}
root.find('.dropzones').append('<div class="dropzone group' + place.group +
' place' + i + '" tabindex="0">' +
'<span class="accesshide">' + label + '</span>&nbsp;</div>');
root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);
}
};
/**
* Invisible 'drag homes' are output by the renderer. These have the same properties
* as the drag items but are invisible. We clone these invisible elements to make the
* actual drag items.
*/
DragDropOntoImageQuestion.prototype.cloneDrags = function() {
var thisQ = this;
this.getRoot().find('.ddarea .draghome').each(function(index, dragHome) {
thisQ.cloneDragsForOneChoice($(dragHome));
});
};
/**
* Clone drag item for one choice.
*
* @param {jQuery} dragHome the drag home to clone.
*/
DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {
if (dragHome.hasClass('infinite')) {
var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));
for (var i = 0; i < noOfDrags; i++) {
this.cloneDrag(dragHome);
}
} else {
this.cloneDrag(dragHome);
}
};
/**
* Clone drag item.
*
* @param {jQuery} dragHome
*/
DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {
var drag = dragHome.clone();
drag.removeClass('draghome')
.addClass('drag unplaced moodle-has-zindex')
.offset(dragHome.offset());
this.getRoot().find('.dragitems').append(drag);
};
/**
* Update the position of drags.
*/
DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {
var thisQ = this,
root = this.getRoot(),
bgPosition = this.bgImage().offset();
// 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])});
});
// First move all items back home.
root.find('.ddarea .drag').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());
if (currentPlace !== null) {
drag.removeClass('inplace' + currentPlace);
}
});
// Then place the ones that should be placed.
root.find('input.placeinput').each(function(i, inputNode) {
var input = $(inputNode),
choice = input.val();
if (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());
});
};
/**
* Handles the start of dragging an item.
*
* @param {Event} e the touch start or mouse down event.
*/
DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {
var thisQ = this,
drag = $(e.target).closest('.drag');
var info = dragDrop.prepare(e);
if (!info.start) {
return;
}
var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
if (currentPlace !== null) {
this.setInputValue(currentPlace, 0);
drag.removeClass('inplace' + currentPlace);
}
drag.addClass('beingdragged');
dragDrop.start(e, drag, function(x, y, drag) {
thisQ.dragMove(x, y, drag);
}, function(x, y, drag) {
thisQ.dragEnd(x, y, drag);
});
};
/**
* Called whenever the currently dragged items moves.
*
* @param {Number} pageX the x position.
* @param {Number} pageY the y position.
* @param {jQuery} drag the item being moved.
*/
DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {
var thisQ = this;
this.getRoot().find('.dropzone.group' + this.getGroup(drag)).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');
}
});
};
/**
* Called when user drops a drag item.
*
* @param {Number} pageX the x position.
* @param {Number} pageY the y position.
* @param {jQuery} drag the item being moved.
*/
DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {
var thisQ = this,
root = this.getRoot(),
placed = false;
root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {
var drop = $(dropNode);
if (!thisQ.isPointInDrop(pageX, pageY, drop)) {
// Not this drop.
return true;
}
// Now put this drag into the drop.
drop.removeClass('valid-drag-over-drop');
thisQ.sendDragToDrop(drag, drop);
placed = true;
return false; // Stop the each() here.
});
if (!placed) {
this.sendDragHome(drag);
}
};
/**
* Animate a drag item into a given place (or back home).
*
* @param {jQuery|null} drag the item to place. If null, clear the place.
* @param {jQuery} drop the place to put it.
*/
DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {
// Is there already a drag in this drop? if so, evict it.
var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));
if (oldDrag.length !== 0) {
this.sendDragHome(oldDrag);
}
if (drag.length === 0) {
this.setInputValue(this.getPlace(drop), 0);
} else {
this.setInputValue(this.getPlace(drop), this.getChoice(drag));
drag.removeClass('unplaced')
.addClass('placed inplace' + this.getPlace(drop));
this.animateTo(drag, drop);
}
};
/**
* Animate a drag back to its home.
*
* @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);
}
this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));
};
/**
* Handles keyboard events on drops.
*
* Drops are focusable. Once focused, right/down/space switches to the next choice, and
* left/up switches to the previous. Escape clear.
*
* @param {KeyboardEvent} e
*/
DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {
var drop = $(e.target).closest('.dropzone'),
currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),
nextDrag = $();
switch (e.keyCode) {
case keys.space:
case keys.arrowRight:
case keys.arrowDown:
nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);
break;
case keys.arrowLeft:
case keys.arrowUp:
nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);
break;
case keys.escape:
break;
default:
return; // To avoid the preventDefault below.
}
e.preventDefault();
this.sendDragToDrop(nextDrag, drop);
};
/**
* Choose the next drag in a group.
*
* @param {int} group which group.
* @param {jQuery} drag current choice (empty jQuery if there isn't one).
* @return {jQuery} the next drag in that group, or null if there wasn't one.
*/
DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {
var choice,
numChoices = this.noOfChoicesInGroup(group);
if (drag.length === 0) {
choice = 1; // Was empty, so we want to select the first choice.
} else {
choice = this.getChoice(drag) + 1;
}
var next = this.getUnplacedChoice(group, choice);
while (next.length === 0 && choice < numChoices) {
choice++;
next = this.getUnplacedChoice(group, choice);
}
return next;
};
/**
* Choose the previous drag in a group.
*
* @param {int} group which group.
* @param {jQuery} drag current choice (empty jQuery if there isn't one).
* @return {jQuery} the next drag in that group, or null if there wasn't one.
*/
DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {
var choice;
if (drag.length === 0) {
choice = this.noOfChoicesInGroup(group);
} else {
choice = this.getChoice(drag) - 1;
}
var previous = this.getUnplacedChoice(group, choice);
while (previous.length === 0 && choice > 1) {
choice--;
previous = this.getUnplacedChoice(group, choice);
}
// Does this choice exist?
return previous;
};
/**
* Animate an object to the given destination.
*
* @param {jQuery} drag the element to be animated.
* @param {jQuery} target element marking the place to move it to.
*/
DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {
var currentPos = drag.offset(),
targetPos = target.offset();
drag.addClass('beingdragged');
// 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
// offset() and then add that to the current CSS position.
drag.animate(
{
left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,
top: parseInt(drag.css('top')) + targetPos.top - currentPos.top
},
{
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);
}
}
);
};
/**
* Detect if a point is inside a given DOM node.
*
* @param {Number} pageX the x position.
* @param {Number} pageY the y position.
* @param {jQuery} drop the node to check (typically a drop).
* @return {boolean} whether the point is inside the node.
*/
DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {
var position = drop.offset();
return pageX >= position.left && pageX < position.left + drop.width()
&& pageY >= position.top && pageY < position.top + drop.height();
};
/**
* Set the value of the hidden input for a place, to record what is currently there.
*
* @param {int} place which place to set the input value for.
* @param {int} choice the value to set.
*/
DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {
this.getRoot().find('input.placeinput.place' + place).val(choice);
};
/**
* Get the outer div for this question.
*
* @returns {jQuery} containing that div.
*/
DragDropOntoImageQuestion.prototype.getRoot = function() {
return $(document.getElementById(this.containerId));
};
/**
* Get the img that is the background image.
* @returns {jQuery} containing that img.
*/
DragDropOntoImageQuestion.prototype.bgImage = function() {
return this.getRoot().find('img.dropbackground');
};
/**
* Get drag home for a given choice.
*
* @param {int} group the group.
* @param {int} choice the choice number.
* @returns {jQuery} containing that div.
*/
DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {
return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice);
};
/**
* Get an unplaced choice for a particular group.
*
* @param {int} group the group.
* @param {int} choice the choice number.
* @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);
};
/**
* Get the drag that is currently in a given place.
*
* @param {int} place the place number.
* @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 the number of blanks in a given group.
*
* @param {int} group the group number.
* @returns {int} the number of drops.
*/
DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {
return this.getRoot().find('.dropzone.group' + group).length;
};
/**
* Return the number of choices in a given group.
*
* @param {int} group the group number.
* @returns {int} the number of choices.
*/
DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {
return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;
};
/**
* Return the number at the end of the CSS class name with the given prefix.
*
* @param {jQuery} node
* @param {String} prefix name prefix
* @returns {Number|null} the suffix if found, else null.
*/
DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {
var classes = node.attr('class');
if (classes !== '') {
var classesArr = classes.split(' ');
for (var index = 0; index < classesArr.length; index++) {
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
if (patt1.test(classesArr[index])) {
var patt2 = new RegExp('([0-9])+$');
var match = patt2.exec(classesArr[index]);
return Number(match[0]);
}
}
}
return null;
};
/**
* Get the choice number of a drag.
*
* @param {jQuery} drag the drag.
* @returns {Number} the choice number.
*/
DragDropOntoImageQuestion.prototype.getChoice = function(drag) {
return this.getClassnameNumericSuffix(drag, 'choice');
};
/**
* Given a DOM node that is significant to this question
* (drag, drop, ...) get the group it belongs to.
*
* @param {jQuery} node a DOM node.
* @returns {Number} the group it belongs to.
*/
DragDropOntoImageQuestion.prototype.getGroup = function(node) {
return this.getClassnameNumericSuffix(node, 'group');
};
/**
* Get the place number of a drop, or its corresponding hidden input.
*
* @param {jQuery} node the DOM node.
* @returns {Number} the place number.
*/
DragDropOntoImageQuestion.prototype.getPlace = function(node) {
return this.getClassnameNumericSuffix(node, 'place');
};
/**
* Singleton object that handles all the DragDropOntoImageQuestions
* on the page, and deals with event dispatching.
* @type {Object}
*/
var questionManager = {
/**
* {boolean} ensures that the event handlers are only initialised once per page.
*/
eventHandlersInitialised: 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.
/**
* Initialise one question.
*
* @param {String} containerId the id of the div.que that contains this question.
* @param {boolean} readOnly whether the question is read-only.
* @param {Array} places data.
*/
init: function(containerId, readOnly, places) {
questionManager.questions[containerId] =
new DragDropOntoImageQuestion(containerId, readOnly, places);
if (!questionManager.eventHandlersInitialised) {
questionManager.setupEventHandlers();
questionManager.eventHandlersInitialised = true;
}
},
/**
* Set up the event handlers that make this question type work. (Done once per page.)
*/
setupEventHandlers: function() {
$('body')
.on('mousedown touchstart',
'.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dragitems .drag',
questionManager.handleDragStart)
.on('keydown',
'.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',
questionManager.handleKeyPress);
$(window).on('resize', questionManager.handleWindowResize);
},
/**
* Handle mouse down / touch start events on drags.
* @param {Event} e the DOM event.
*/
handleDragStart: function(e) {
e.preventDefault();
var question = questionManager.getQuestionForEvent(e);
if (question) {
question.handleDragStart(e);
}
},
/**
* Handle key down / press events on drags.
* @param {KeyboardEvent} e
*/
handleKeyPress: function(e) {
var question = questionManager.getQuestionForEvent(e);
if (question) {
question.handleKeyPress(e);
}
},
/**
* Handle when the window is resized.
*/
handleWindowResize: function() {
for (var containerId in questionManager.questions) {
if (questionManager.questions.hasOwnProperty(containerId)) {
questionManager.questions[containerId].positionDragsAndDrops();
}
}
},
/**
* Given an event, work out which question it effects.
* @param {Event} e the event.
* @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.
*/
getQuestionForEvent: function(e) {
var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');
return questionManager.questions[containerId];
}
};
/**
* @alias module:qtype_ddimageortext/question
*/
return {
/**
* Initialise one drag-drop onto image question.
*
* @param {String} containerId id of the outer div for this question.
* @param {boolean} readOnly whether the question is being displayed read-only.
* @param {Array} Information about the drop places.
*/
init: questionManager.init
};
});
@@ -108,20 +108,17 @@ class qtype_ddimageortext_edit_form extends qtype_ddtoimage_edit_form_base {
public function js_call() {
global $PAGE;
$maxsizes = new stdClass();
$maxsizes->bgimage = new stdClass();
$maxsizes->bgimage->width = QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH;
$maxsizes->bgimage->height = QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT;
$maxsizes->dragimage = new stdClass();
$maxsizes->dragimage->width = QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH;
$maxsizes->dragimage->height = QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT;
$params = array('maxsizes' => $maxsizes,
'topnode' => 'fieldset#id_previewareaheader');
$PAGE->requires->yui_module('moodle-qtype_ddimageortext-form',
'M.qtype_ddimageortext.init_form',
array($params));
$maxbgimagesize = [
'width' => QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH,
'height' => QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT
];
$maxdragimagesize = [
'width' => QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH,
'height' => QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT
];
$PAGE->requires->js_call_amd('qtype_ddimageortext/form', 'init',
[$maxbgimagesize, $maxdragimagesize]);
}
// Drag items.
@@ -166,7 +163,7 @@ class qtype_ddimageortext_edit_form extends qtype_ddtoimage_edit_form_base {
$draggableimageitem[] = $mform->createElement('text', 'draglabel',
get_string('label', 'qtype_ddimageortext'),
array('size' => 30, 'class' => 'tweakcss'));
array('size' => 30, 'class' => 'tweakcss draglabel'));
$mform->setType('draglabel', PARAM_RAW); // These are validated manually.
return $draggableimageitem;
}
+9 -14
View File
@@ -66,8 +66,9 @@ class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_rendere
$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'));
$droparea = html_writer::tag('div', $img, array('class' => 'droparea'));
$droparea = html_writer::tag('div', $img . $dropzones, array('class' => 'droparea'));
$dragimagehomes = '';
foreach ($question->choices as $groupno => $group) {
@@ -77,13 +78,11 @@ class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_rendere
$dragimageurl = self::get_url_for_image($qa, 'dragimage', $dragimage->id);
$classes = array("group{$groupno}",
'draghome',
"dragitemhomes{$dragimage->no}",
"choice{$choiceno}");
if ($dragimage->infinite) {
$classes[] = 'infinite';
}
if ($dragimageurl === null) {
$classes[] = 'yui3-cssfonts';
$dragimagehomesgroup .= html_writer::tag('div', $dragimage->text,
array('src' => $dragimageurl, 'class' => join(' ', $classes)));
} else {
@@ -96,31 +95,27 @@ class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_rendere
array('class' => 'dragitemgroup' . $groupno));
}
$draghomes = html_writer::tag('div', $dragimagehomes, array('class' => 'draghomes'));
$dragitemsclass = 'dragitems';
if ($options->readonly) {
$dragitemsclass .= ' readonly';
}
$dragitems = html_writer::tag('div', $dragimagehomes, array('class' => $dragitemsclass));
$dropzones = html_writer::tag('div', '', array('class' => 'dropzones'));
$dragitems = html_writer::tag('div', '', array('class' => $dragitemsclass));
$hiddens = '';
foreach ($question->places as $placeno => $place) {
$varname = $question->field($placeno);
list($fieldname, $html) = $this->hidden_field_for_qt_var($qa, $varname);
list($fieldname, $html) = $this->hidden_field_for_qt_var($qa, $varname, null,
['placeinput', 'place' . $placeno, 'group' . $place->group]);
$hiddens .= $html;
$question->places[$placeno]->fieldname = $fieldname;
}
$output .= html_writer::tag('div',
$droparea . $dragitems . $dropzones . $hiddens, array('class' => 'ddarea'));
$topnode = 'div#q'.$qa->get_slot().' div.ddarea';
$params = array('drops' => $question->places,
'topnode' => $topnode,
'readonly' => $options->readonly);
$droparea . $draghomes. $dragitems . $hiddens, array('class' => 'ddarea'));
$PAGE->requires->string_for_js('blank', 'qtype_ddimageortext');
$PAGE->requires->yui_module('moodle-qtype_ddimageortext-dd',
'M.qtype_ddimageortext.init_question',
array($params));
$PAGE->requires->js_call_amd('qtype_ddimageortext/question', 'init',
['q' . $qa->get_slot(), $options->readonly, $question->places]);
if ($qa->get_state() == question_state::$invalid) {
$output .= html_writer::nonempty_tag('div',
+57 -57
View File
@@ -3,28 +3,73 @@
display: block;
}
.que.ddimageortext div.droparea img,
form.mform fieldset#id_previewareaheader div.droparea img {
.que.ddimageortext div.ddarea,
form.mform fieldset#id_previewareaheader div.ddarea {
text-align: center;
}
.que.ddimageortext div.droparea,
form.mform fieldset#id_previewareaheader div.ddarea {
position: relative;
}
.que.ddimageortext .dropbackground,
form.mform fieldset#id_previewareaheader .dropbackground {
border: 1px solid #000;
max-width: none;
margin: 0 auto;
}
.que.ddimageortext .dropzone {
position: absolute;
opacity: 0.5;
border: 1px solid black;
z-index: 1;
}
.que.ddimageortext .dropzone:focus,
.que.ddimageortext .dropzone.valid-drag-over-drop {
border-color: #0a0;
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
outline: 0;
}
.que.ddimageortext .draghome,
form.mform fieldset#id_previewareaheader .draghome {
.que.ddimageortext .drag,
form.mform fieldset#id_previewareaheader .droppreview {
border: 1px solid black;
display: inline-block;
font: 13px/1.231 arial, helvetica, clean, sans-serif;
}
.que.ddimageortext .draghome {
vertical-align: top;
margin: 5px;
visibility: hidden;
}
.que.ddimageortext div.draghome,
form.mform fieldset#id_previewareaheader div.draghome {
border: 1px solid black;
cursor: move;
background-color: #b0c4de;
display: inline-block;
height: auto;
width: auto;
zoom: 1;
}
.que.ddimageortext .dragitems,
form.mform fieldset#id_previewareaheader .dragitems {
height: 0;
}
.que.ddimageortext .drag,
form.mform fieldset#id_previewareaheader .droppreview {
position: absolute;
cursor: move;
z-index: 2;
}
.que.ddimageortext .dragitems.readonly .drag {
cursor: auto;
}
form.mform fieldset#id_previewareaheader .drag.beingdragged,
.que.ddimageortext .drag.beingdragged {
z-index: 3;
box-shadow: 3px 3px 4px #000;
}
.que.ddimageortext .group1,
@@ -67,51 +112,6 @@ form.mform fieldset#id_previewareaheader .group8 {
background-color: #f0e68c;
}
.que.ddimageortext .drag,
form.mform fieldset#id_previewareaheader .drag {
border: 1px solid black;
cursor: move;
z-index: 2;
}
.que.ddimageortext .dragitems.readonly .drag {
cursor: auto;
}
.que.ddimageortext div.ddarea,
form.mform fieldset#id_previewareaheader div.ddarea {
text-align: center;
}
.que.ddimageortext .dropbackground,
form.mform fieldset#id_previewareaheader .dropbackground {
margin: 0 auto;
}
.que.ddimageortext .dropzone {
border: 1px solid black;
position: absolute;
z-index: 1;
}
.que.ddimageortext .dropzone:focus,
.que.ddimageortext .dropzone.yui3-dd-drop-over.yui3-dd-drop-active-valid {
border-color: #0a0;
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
}
.que.ddimageortext div.dragitems div.draghome,
.que.ddimageortext div.dragitems div.drag,
form.mform fieldset#id_previewareaheader div.draghome,
form.mform fieldset#id_previewareaheader div.drag {
font: 13px/1.231 arial, helvetica, clean, sans-serif;
}
form.mform fieldset#id_previewareaheader div.drag.yui3-dd-dragging,
.que.ddimageortext div.drag.yui3-dd-dragging {
z-index: 3;
box-shadow: 3px 3px 4px #000;
}
/* Editing form. Style repeated elements*/
/*Top*/
body#page-question-type-ddimageortext div[id^=fgroup_id_][id*=drags_] {
@@ -41,7 +41,8 @@ class behat_qtype_ddimageortext extends behat_base {
* @return string the xpath expression.
*/
protected function drag_xpath($dragitem) {
return '//div[contains(@class, " drag ") and contains(normalize-space(.), "' . $this->escape($dragitem) . '")]';
return '//div[contains(concat(" ", @class, " "), " drag ") and ' .
'contains(normalize-space(.), "' . $this->escape($dragitem) . '")]';
}
/**
@@ -50,7 +51,8 @@ class behat_qtype_ddimageortext extends behat_base {
* @return string the xpath expression.
*/
protected function drop_xpath($placenumber) {
return '//div[contains(@class, "dropzone ") and contains(@class, "place' . $placenumber . ' ")]';
return '//div[contains(concat(" ", @class, " "), " dropzone ") and ' .
'contains(concat(" ", @class, " "), " place' . $placenumber . ' ")]';
}
/**
@@ -30,7 +30,7 @@ Feature: Preview a drag-drop onto image question
And I switch to "questionpreview" window
# Increase window size and wait 2 seconds to ensure elements are placed properly by js.
# Keep window large else drag will scroll the window to find element.
And I change window size to "large"
And I change window size to "medium"
And I wait "2" seconds
# Odd, but the <br>s go to nothing, not a space.
And I drag "mountainbelt" to place "1" in the drag and drop onto image question
@@ -51,7 +51,7 @@ Feature: Preview a drag-drop onto image question
When I click on "Preview" "link" in the "Drag onto image" "table_row"
And I switch to "questionpreview" window
# Increase window size and wait 2 seconds to ensure elements are placed properly by js.
And I change window size to "large"
And I change window size to "medium"
And I wait "2" seconds
And I type " " on place "1" in the drag and drop onto image question
And I type " " on place "2" in the drag and drop onto image question
@@ -46,8 +46,7 @@ class qtype_ddimageortext_walkthrough_test extends qbehaviour_walkthrough_test_b
* @return question_contains_tag_with_attributes the required expectation.
*/
protected function get_contains_drag_image_home_expectation($dragitemno, $choice, $group) {
$class = 'group' . $group;
$class .= ' draghome dragitemhomes' . $dragitemno. ' choice'.$choice.' yui3-cssfonts';
$class = 'group' . $group . ' draghome choice' . $choice;
$expectedattrs = array();
$expectedattrs['class'] = $class;
@@ -1,546 +0,0 @@
YUI.add('moodle-qtype_ddimageortext-dd', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
var DDIMAGEORTEXT_DD = function() {
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
};
/**
* This is the base class for the question rendering and question editing form code.
*/
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
doc: null,
polltimer: null,
afterimageloaddone: false,
poll_for_image_load: function(e, waitforimageconstrain, pause, doafterwords) {
if (this.afterimageloaddone) {
return;
}
var bgdone = this.doc.bg_img().get('complete');
if (waitforimageconstrain) {
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
}
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome) {
// in 'some' loop returning true breaks the loop and is passed as return value from
// 'some' else returns false. Can be though of as equivalent to ||.
if (dragitemhome.get('tagName') !== 'IMG') {
return false;
}
var done = (dragitemhome.get('complete'));
if (waitforimageconstrain) {
done = done && dragitemhome.hasClass('constrained');
}
return !done;
});
if (bgdone && alldragsloaded) {
if (this.polltimer !== null) {
this.polltimer.cancel();
this.polltimer = null;
}
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
this.doc.bg_img().detach('load', this.poll_for_image_load);
if (pause !== 0) {
Y.later(pause, this, doafterwords);
} else {
doafterwords.call(this);
}
this.afterimageloaddone = true;
} else if (this.polltimer === null) {
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
this.polltimer =
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
}
},
/**
* Object to encapsulate operations on dd area.
*/
doc_structure: function(mainobj) {
var topnode = Y.one(this.get('topnode'));
var dragitemsarea = topnode.one('div.dragitems');
var dropbgarea = topnode.one('div.droparea');
return {
top_node: function() {
return topnode;
},
drag_items: function() {
return dragitemsarea.all('.drag');
},
drop_zones: function() {
return topnode.all('div.dropzones div.dropzone');
},
drop_zone_group: function(groupno) {
return topnode.all('div.dropzones div.group' + groupno);
},
drag_items_cloned_from: function(dragitemno) {
return dragitemsarea.all('.dragitems' + dragitemno);
},
drag_item: function(draginstanceno) {
return dragitemsarea.one('.draginstance' + draginstanceno);
},
drag_items_in_group: function(groupno) {
return dragitemsarea.all('.drag.group' + groupno);
},
drag_item_homes: function() {
return dragitemsarea.all('.draghome');
},
bg_img: function() {
return topnode.one('.dropbackground');
},
load_bg_img: function(url) {
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
},
add_or_update_drag_item_home: function(dragitemno, url, alt, group) {
var oldhome = this.drag_item_home(dragitemno);
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
if (oldhome === null) {
if (url) {
dragitemsarea.append(imghtml);
} else if (alt !== '') {
dragitemsarea.append(divhtml);
}
} else {
if (url) {
dragitemsarea.insert(imghtml, oldhome);
} else if (alt !== '') {
dragitemsarea.insert(divhtml, oldhome);
}
oldhome.remove(true);
}
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
if (newlycreated !== null) {
newlycreated.setData('groupno', group);
newlycreated.setData('dragitemno', dragitemno);
}
},
drag_item_home: function(dragitemno) {
return dragitemsarea.one('.dragitemhomes' + dragitemno);
},
get_classname_numeric_suffix: function(node, prefix) {
var classes = node.getAttribute('class');
if (classes !== '') {
var classesarr = classes.split(' ');
for (var index = 0; index < classesarr.length; index++) {
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
if (patt1.test(classesarr[index])) {
var patt2 = new RegExp('([0-9])+$');
var match = patt2.exec(classesarr[index]);
return +match[0];
}
}
}
throw 'Prefix "' + prefix + '" not found in class names.';
},
clone_new_drag_item: function(draginstanceno, dragitemno) {
var draghome = this.drag_item_home(dragitemno);
if (draghome === null) {
return null;
}
var drag = draghome.cloneNode(true);
drag.removeClass('dragitemhomes' + dragitemno);
drag.addClass('dragitems' + dragitemno);
drag.addClass('draginstance' + draginstanceno);
drag.removeClass('draghome');
drag.addClass('drag');
drag.setStyles({'visibility': 'visible', 'position': 'absolute'});
drag.setData('draginstanceno', draginstanceno);
drag.setData('dragitemno', dragitemno);
draghome.get('parentNode').appendChild(drag);
return drag;
},
draggable_for_question: function(drag, group, choice) {
new Y.DD.Drag({
node: drag,
dragMode: 'point',
groups: [group]
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
drag.setData('group', group);
drag.setData('choice', choice);
},
draggable_for_form: function(drag) {
var dd = new Y.DD.Drag({
node: drag,
dragMode: 'point'
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
dd.on('drag:end', function(e) {
var dragnode = e.target.get('node');
var draginstanceno = dragnode.getData('draginstanceno');
var gooddrop = dragnode.getData('gooddrop');
if (!gooddrop) {
mainobj.reset_drag_xy(draginstanceno);
} else {
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
}
}, this);
dd.on('drag:start', function(e) {
var drag = e.target;
drag.get('node').setData('gooddrop', false);
}, this);
}
};
},
update_padding_sizes_all: function() {
for (var groupno = 1; groupno <= 8; groupno++) {
this.update_padding_size_for_group(groupno);
}
},
update_padding_size_for_group: function(groupno) {
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
if (groupitems.size() !== 0) {
var maxwidth = 0;
var maxheight = 0;
groupitems.each(function(item) {
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
maxheight = Math.max(maxheight, item.get('clientHeight'));
}, this);
groupitems.each(function(item) {
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
margintopbottom + 'px ' + marginleftright + 'px');
}, this);
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
'height': maxheight + 10});
}
},
convert_to_window_xy: function(bgimgxy) {
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
}
}, {
NAME: DDIMAGEORTEXTDDNAME,
ATTRS: {
drops: {value: null},
readonly: {value: false},
topnode: {value: null}
}
});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
var DDIMAGEORTEXT_QUESTION = function() {
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
};
/**
* This is the code for question rendering.
*/
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
passiveSupported: false,
pendingid: '',
initializer: function() {
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.doc = this.doc_structure(this);
this.poll_for_image_load(null, false, 10, this.create_all_drag_and_drops);
this.doc.bg_img().after('load', this.poll_for_image_load, this,
false, 10, this.create_all_drag_and_drops);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
false, 10, this.create_all_drag_and_drops);
if (!this.get('readonly')) {
Y.later(500, this, this.reposition_drags_for_question, true);
} else {
Y.one('window').on('resize', function() {
this.reposition_drags_for_question();
}, this);
}
this.checkPassiveSupported();
},
/**
* prevent_touchmove_from_scrolling allows users of touch screen devices to
* use drag and drop and normal scrolling at the same time. I.e. when
* touching and dragging a draggable item, the screen does not scroll, but
* you can scroll by touching other area of the screen apart from the
* draggable items.
*/
prevent_touchmove_from_scrolling: function(drag) {
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
var eventHandler = function(event) {
event.preventDefault();
};
var dragId = drag.get('id');
var el = document.getElementById(dragId);
// Note do not dynamically add events within another event, as this causes issues on iOS11.3.
// See https://github.com/atlassian/react-beautiful-dnd/issues/413 and
// https://bugs.webkit.org/show_bug.cgi?id=184250 for fuller explanation.
el.addEventListener(touchmove, eventHandler, this.passiveSupported ? {passive: false, capture: true} : false);
},
/**
* Some older browsers do not support passing an options object to addEventListener.
* This is a check from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener.
*/
checkPassiveSupported: function() {
try {
var options = Object.defineProperty({}, 'passive', {
get: function() {
this.passiveSupported = true;
}.bind(this)
});
window.addEventListener('test', options, options);
window.removeEventListener('test', options, options);
} catch (err) {
this.passiveSupported = false;
}
},
create_all_drag_and_drops: function() {
this.init_drops();
this.update_padding_sizes_all();
var i = 0;
this.doc.drag_item_homes().each(function(dragitemhome) {
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
var choice = +this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
var group = +this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
var groupsize = this.doc.drop_zone_group(group).size();
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
if (dragnode.hasClass('infinite')) {
var dragstocreate = groupsize - 1;
while (dragstocreate > 0) {
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
dragstocreate--;
}
}
}, this);
this.reposition_drags_for_question();
if (!this.get('readonly')) {
this.doc.drop_zones().set('tabIndex', 0);
this.doc.drop_zones().each(
function(v) {
v.on('dragchange', this.drop_zone_key_press, this);
}, this);
}
M.util.js_complete(this.pendingid);
},
drop_zone_key_press: function(e) {
switch (e.direction) {
case 'next' :
this.place_next_drag_in(e.target);
break;
case 'previous' :
this.place_previous_drag_in(e.target);
break;
case 'remove' :
this.remove_drag_from_drop(e.target);
break;
}
e.preventDefault();
this.reposition_drags_for_question();
},
place_next_drag_in: function(drop) {
this.search_for_unplaced_drop_choice(drop, 1);
},
place_previous_drag_in: function(drop) {
this.search_for_unplaced_drop_choice(drop, -1);
},
search_for_unplaced_drop_choice: function(drop, direction) {
var next;
var current = this.current_drag_in_drop(drop);
if ('' === current) {
if (direction === 1) {
next = 1;
} else {
next = 1;
var groupno = drop.getData('group');
this.doc.drag_items_in_group(groupno).each(function(drag) {
next = Math.max(next, drag.getData('choice'));
}, this);
}
} else {
next = +current + direction;
}
var drag;
do {
if (this.get_choices_for_drop(next, drop).size() === 0) {
this.remove_drag_from_drop(drop);
return;
} else {
drag = this.get_unplaced_choice_for_drop(next, drop);
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_drag_in_drop: function(drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
return inputnode.get('value');
},
remove_drag_from_drop: function(drop) {
this.place_drag_in_drop(null, drop);
},
place_drag_in_drop: function(drag, drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
if (drag !== null) {
inputnode.set('value', drag.getData('choice'));
} else {
inputnode.set('value', '');
}
},
reposition_drags_for_question: function(dotimeout) {
this.doc.drag_items().removeClass('placed');
this.doc.drag_items().each(function(dragitem) {
if (dragitem.dd !== undefined) {
dragitem.dd.detachAll('drag:start');
}
}, this);
this.doc.drop_zones().each(function(dropzone) {
var relativexy = dropzone.getData('xy');
dropzone.setXY(this.convert_to_window_xy(relativexy));
var inputcss = 'input#' + dropzone.getData('inputid');
var input = this.doc.top_node().one(inputcss);
var choice = input.get('value');
if (choice !== "") {
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
if (dragitem !== null) {
dragitem.setXY(dropzone.getXY());
dragitem.addClass('placed');
if (dragitem.dd !== undefined) {
dragitem.dd.once('drag:start', function(e, input) {
input.set('value', '');
e.target.get('node').removeClass('placed');
}, this, input);
}
}
}
}, this);
this.doc.drag_items().each(function(dragitem) {
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
dragitem.setXY(dragitemhome.getXY());
}
}, this);
if (dotimeout) {
Y.later(500, this, this.reposition_drags_for_question, true);
}
},
get_choices_for_drop: function(choice, drop) {
var group = drop.getData('group');
return this.doc.top_node().all(
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
},
get_unplaced_choice_for_drop: function(choice, drop) {
var dragitems = this.get_choices_for_drop(choice, drop);
var dragitem = null;
dragitems.some(function(d) {
if (this.get('readonly') || (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging'))) {
dragitem = d;
return true;
} else {
return false;
}
});
return dragitem;
},
init_drops: function() {
var dropareas = this.doc.top_node().one('div.dropzones');
var groupnodes = {};
for (var groupno = 1; groupno <= 8; groupno++) {
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
dropareas.append(groupnode);
groupnodes[groupno] = groupnode;
}
var drop_hit_handler = function(e) {
var drag = e.drag.get('node');
var drop = e.drop.get('node');
if (Number(drop.getData('group')) === drag.getData('group')) {
this.place_drag_in_drop(drag, drop);
}
};
for (var dropno in this.get('drops')) {
var drop = this.get('drops')[dropno];
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
var title = drop.text.replace('"', '\"');
if (!title) {
title = M.util.get_string('blank', 'qtype_ddimageortext');
}
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '">' +
'<span class="accesshide">' + title + '</span>&nbsp;</div>';
var dropnode = Y.Node.create(dropnodehtml);
groupnodes[drop.group].append(dropnode);
dropnode.setStyles({'opacity': 0.5});
dropnode.setData('xy', drop.xy);
dropnode.setData('place', dropno);
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
dropnode.setData('group', drop.group);
var dropdd = new Y.DD.Drop({
node: dropnode, groups: [drop.group]});
dropdd.on('drop:hit', drop_hit_handler, this);
}
}
}, {NAME: DDIMAGEORTEXTQUESTIONNAME, ATTRS: {}});
Y.Event.define('dragchange', {
// Webkit and IE repeat keydown when you hold down arrow keys.
// Opera links keypress to page scroll; others keydown.
// Firefox prevents page scroll via preventDefault() on either
// keydown or keypress.
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
_keys: {
'32': 'next', // Space
'37': 'previous', // Left arrow
'38': 'previous', // Up arrow
'39': 'next', // Right arrow
'40': 'next', // Down arrow
'27': 'remove' // Escape
},
_keyHandler: function(e, notifier) {
if (this._keys[e.keyCode]) {
e.direction = this._keys[e.keyCode];
notifier.fire(e);
}
},
on: function(node, sub, notifier) {
sub._detacher = node.on(this._event, this._keyHandler,
this, notifier);
}
});
M.qtype_ddimageortext.init_question = function(config) {
return new DDIMAGEORTEXT_QUESTION(config);
};
}, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});
File diff suppressed because one or more lines are too long
@@ -1,546 +0,0 @@
YUI.add('moodle-qtype_ddimageortext-dd', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
var DDIMAGEORTEXT_DD = function() {
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
};
/**
* This is the base class for the question rendering and question editing form code.
*/
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
doc: null,
polltimer: null,
afterimageloaddone: false,
poll_for_image_load: function(e, waitforimageconstrain, pause, doafterwords) {
if (this.afterimageloaddone) {
return;
}
var bgdone = this.doc.bg_img().get('complete');
if (waitforimageconstrain) {
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
}
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome) {
// in 'some' loop returning true breaks the loop and is passed as return value from
// 'some' else returns false. Can be though of as equivalent to ||.
if (dragitemhome.get('tagName') !== 'IMG') {
return false;
}
var done = (dragitemhome.get('complete'));
if (waitforimageconstrain) {
done = done && dragitemhome.hasClass('constrained');
}
return !done;
});
if (bgdone && alldragsloaded) {
if (this.polltimer !== null) {
this.polltimer.cancel();
this.polltimer = null;
}
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
this.doc.bg_img().detach('load', this.poll_for_image_load);
if (pause !== 0) {
Y.later(pause, this, doafterwords);
} else {
doafterwords.call(this);
}
this.afterimageloaddone = true;
} else if (this.polltimer === null) {
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
this.polltimer =
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
}
},
/**
* Object to encapsulate operations on dd area.
*/
doc_structure: function(mainobj) {
var topnode = Y.one(this.get('topnode'));
var dragitemsarea = topnode.one('div.dragitems');
var dropbgarea = topnode.one('div.droparea');
return {
top_node: function() {
return topnode;
},
drag_items: function() {
return dragitemsarea.all('.drag');
},
drop_zones: function() {
return topnode.all('div.dropzones div.dropzone');
},
drop_zone_group: function(groupno) {
return topnode.all('div.dropzones div.group' + groupno);
},
drag_items_cloned_from: function(dragitemno) {
return dragitemsarea.all('.dragitems' + dragitemno);
},
drag_item: function(draginstanceno) {
return dragitemsarea.one('.draginstance' + draginstanceno);
},
drag_items_in_group: function(groupno) {
return dragitemsarea.all('.drag.group' + groupno);
},
drag_item_homes: function() {
return dragitemsarea.all('.draghome');
},
bg_img: function() {
return topnode.one('.dropbackground');
},
load_bg_img: function(url) {
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
},
add_or_update_drag_item_home: function(dragitemno, url, alt, group) {
var oldhome = this.drag_item_home(dragitemno);
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
if (oldhome === null) {
if (url) {
dragitemsarea.append(imghtml);
} else if (alt !== '') {
dragitemsarea.append(divhtml);
}
} else {
if (url) {
dragitemsarea.insert(imghtml, oldhome);
} else if (alt !== '') {
dragitemsarea.insert(divhtml, oldhome);
}
oldhome.remove(true);
}
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
if (newlycreated !== null) {
newlycreated.setData('groupno', group);
newlycreated.setData('dragitemno', dragitemno);
}
},
drag_item_home: function(dragitemno) {
return dragitemsarea.one('.dragitemhomes' + dragitemno);
},
get_classname_numeric_suffix: function(node, prefix) {
var classes = node.getAttribute('class');
if (classes !== '') {
var classesarr = classes.split(' ');
for (var index = 0; index < classesarr.length; index++) {
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
if (patt1.test(classesarr[index])) {
var patt2 = new RegExp('([0-9])+$');
var match = patt2.exec(classesarr[index]);
return +match[0];
}
}
}
throw 'Prefix "' + prefix + '" not found in class names.';
},
clone_new_drag_item: function(draginstanceno, dragitemno) {
var draghome = this.drag_item_home(dragitemno);
if (draghome === null) {
return null;
}
var drag = draghome.cloneNode(true);
drag.removeClass('dragitemhomes' + dragitemno);
drag.addClass('dragitems' + dragitemno);
drag.addClass('draginstance' + draginstanceno);
drag.removeClass('draghome');
drag.addClass('drag');
drag.setStyles({'visibility': 'visible', 'position': 'absolute'});
drag.setData('draginstanceno', draginstanceno);
drag.setData('dragitemno', dragitemno);
draghome.get('parentNode').appendChild(drag);
return drag;
},
draggable_for_question: function(drag, group, choice) {
new Y.DD.Drag({
node: drag,
dragMode: 'point',
groups: [group]
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
drag.setData('group', group);
drag.setData('choice', choice);
},
draggable_for_form: function(drag) {
var dd = new Y.DD.Drag({
node: drag,
dragMode: 'point'
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
dd.on('drag:end', function(e) {
var dragnode = e.target.get('node');
var draginstanceno = dragnode.getData('draginstanceno');
var gooddrop = dragnode.getData('gooddrop');
if (!gooddrop) {
mainobj.reset_drag_xy(draginstanceno);
} else {
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
}
}, this);
dd.on('drag:start', function(e) {
var drag = e.target;
drag.get('node').setData('gooddrop', false);
}, this);
}
};
},
update_padding_sizes_all: function() {
for (var groupno = 1; groupno <= 8; groupno++) {
this.update_padding_size_for_group(groupno);
}
},
update_padding_size_for_group: function(groupno) {
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
if (groupitems.size() !== 0) {
var maxwidth = 0;
var maxheight = 0;
groupitems.each(function(item) {
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
maxheight = Math.max(maxheight, item.get('clientHeight'));
}, this);
groupitems.each(function(item) {
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
margintopbottom + 'px ' + marginleftright + 'px');
}, this);
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
'height': maxheight + 10});
}
},
convert_to_window_xy: function(bgimgxy) {
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
}
}, {
NAME: DDIMAGEORTEXTDDNAME,
ATTRS: {
drops: {value: null},
readonly: {value: false},
topnode: {value: null}
}
});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
var DDIMAGEORTEXT_QUESTION = function() {
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
};
/**
* This is the code for question rendering.
*/
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
passiveSupported: false,
pendingid: '',
initializer: function() {
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.doc = this.doc_structure(this);
this.poll_for_image_load(null, false, 10, this.create_all_drag_and_drops);
this.doc.bg_img().after('load', this.poll_for_image_load, this,
false, 10, this.create_all_drag_and_drops);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
false, 10, this.create_all_drag_and_drops);
if (!this.get('readonly')) {
Y.later(500, this, this.reposition_drags_for_question, true);
} else {
Y.one('window').on('resize', function() {
this.reposition_drags_for_question();
}, this);
}
this.checkPassiveSupported();
},
/**
* prevent_touchmove_from_scrolling allows users of touch screen devices to
* use drag and drop and normal scrolling at the same time. I.e. when
* touching and dragging a draggable item, the screen does not scroll, but
* you can scroll by touching other area of the screen apart from the
* draggable items.
*/
prevent_touchmove_from_scrolling: function(drag) {
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
var eventHandler = function(event) {
event.preventDefault();
};
var dragId = drag.get('id');
var el = document.getElementById(dragId);
// Note do not dynamically add events within another event, as this causes issues on iOS11.3.
// See https://github.com/atlassian/react-beautiful-dnd/issues/413 and
// https://bugs.webkit.org/show_bug.cgi?id=184250 for fuller explanation.
el.addEventListener(touchmove, eventHandler, this.passiveSupported ? {passive: false, capture: true} : false);
},
/**
* Some older browsers do not support passing an options object to addEventListener.
* This is a check from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener.
*/
checkPassiveSupported: function() {
try {
var options = Object.defineProperty({}, 'passive', {
get: function() {
this.passiveSupported = true;
}.bind(this)
});
window.addEventListener('test', options, options);
window.removeEventListener('test', options, options);
} catch (err) {
this.passiveSupported = false;
}
},
create_all_drag_and_drops: function() {
this.init_drops();
this.update_padding_sizes_all();
var i = 0;
this.doc.drag_item_homes().each(function(dragitemhome) {
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
var choice = +this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
var group = +this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
var groupsize = this.doc.drop_zone_group(group).size();
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
if (dragnode.hasClass('infinite')) {
var dragstocreate = groupsize - 1;
while (dragstocreate > 0) {
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
dragstocreate--;
}
}
}, this);
this.reposition_drags_for_question();
if (!this.get('readonly')) {
this.doc.drop_zones().set('tabIndex', 0);
this.doc.drop_zones().each(
function(v) {
v.on('dragchange', this.drop_zone_key_press, this);
}, this);
}
M.util.js_complete(this.pendingid);
},
drop_zone_key_press: function(e) {
switch (e.direction) {
case 'next' :
this.place_next_drag_in(e.target);
break;
case 'previous' :
this.place_previous_drag_in(e.target);
break;
case 'remove' :
this.remove_drag_from_drop(e.target);
break;
}
e.preventDefault();
this.reposition_drags_for_question();
},
place_next_drag_in: function(drop) {
this.search_for_unplaced_drop_choice(drop, 1);
},
place_previous_drag_in: function(drop) {
this.search_for_unplaced_drop_choice(drop, -1);
},
search_for_unplaced_drop_choice: function(drop, direction) {
var next;
var current = this.current_drag_in_drop(drop);
if ('' === current) {
if (direction === 1) {
next = 1;
} else {
next = 1;
var groupno = drop.getData('group');
this.doc.drag_items_in_group(groupno).each(function(drag) {
next = Math.max(next, drag.getData('choice'));
}, this);
}
} else {
next = +current + direction;
}
var drag;
do {
if (this.get_choices_for_drop(next, drop).size() === 0) {
this.remove_drag_from_drop(drop);
return;
} else {
drag = this.get_unplaced_choice_for_drop(next, drop);
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_drag_in_drop: function(drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
return inputnode.get('value');
},
remove_drag_from_drop: function(drop) {
this.place_drag_in_drop(null, drop);
},
place_drag_in_drop: function(drag, drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
if (drag !== null) {
inputnode.set('value', drag.getData('choice'));
} else {
inputnode.set('value', '');
}
},
reposition_drags_for_question: function(dotimeout) {
this.doc.drag_items().removeClass('placed');
this.doc.drag_items().each(function(dragitem) {
if (dragitem.dd !== undefined) {
dragitem.dd.detachAll('drag:start');
}
}, this);
this.doc.drop_zones().each(function(dropzone) {
var relativexy = dropzone.getData('xy');
dropzone.setXY(this.convert_to_window_xy(relativexy));
var inputcss = 'input#' + dropzone.getData('inputid');
var input = this.doc.top_node().one(inputcss);
var choice = input.get('value');
if (choice !== "") {
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
if (dragitem !== null) {
dragitem.setXY(dropzone.getXY());
dragitem.addClass('placed');
if (dragitem.dd !== undefined) {
dragitem.dd.once('drag:start', function(e, input) {
input.set('value', '');
e.target.get('node').removeClass('placed');
}, this, input);
}
}
}
}, this);
this.doc.drag_items().each(function(dragitem) {
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
dragitem.setXY(dragitemhome.getXY());
}
}, this);
if (dotimeout) {
Y.later(500, this, this.reposition_drags_for_question, true);
}
},
get_choices_for_drop: function(choice, drop) {
var group = drop.getData('group');
return this.doc.top_node().all(
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
},
get_unplaced_choice_for_drop: function(choice, drop) {
var dragitems = this.get_choices_for_drop(choice, drop);
var dragitem = null;
dragitems.some(function(d) {
if (this.get('readonly') || (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging'))) {
dragitem = d;
return true;
} else {
return false;
}
});
return dragitem;
},
init_drops: function() {
var dropareas = this.doc.top_node().one('div.dropzones');
var groupnodes = {};
for (var groupno = 1; groupno <= 8; groupno++) {
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
dropareas.append(groupnode);
groupnodes[groupno] = groupnode;
}
var drop_hit_handler = function(e) {
var drag = e.drag.get('node');
var drop = e.drop.get('node');
if (Number(drop.getData('group')) === drag.getData('group')) {
this.place_drag_in_drop(drag, drop);
}
};
for (var dropno in this.get('drops')) {
var drop = this.get('drops')[dropno];
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
var title = drop.text.replace('"', '\"');
if (!title) {
title = M.util.get_string('blank', 'qtype_ddimageortext');
}
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '">' +
'<span class="accesshide">' + title + '</span>&nbsp;</div>';
var dropnode = Y.Node.create(dropnodehtml);
groupnodes[drop.group].append(dropnode);
dropnode.setStyles({'opacity': 0.5});
dropnode.setData('xy', drop.xy);
dropnode.setData('place', dropno);
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
dropnode.setData('group', drop.group);
var dropdd = new Y.DD.Drop({
node: dropnode, groups: [drop.group]});
dropdd.on('drop:hit', drop_hit_handler, this);
}
}
}, {NAME: DDIMAGEORTEXTQUESTIONNAME, ATTRS: {}});
Y.Event.define('dragchange', {
// Webkit and IE repeat keydown when you hold down arrow keys.
// Opera links keypress to page scroll; others keydown.
// Firefox prevents page scroll via preventDefault() on either
// keydown or keypress.
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
_keys: {
'32': 'next', // Space
'37': 'previous', // Left arrow
'38': 'previous', // Up arrow
'39': 'next', // Right arrow
'40': 'next', // Down arrow
'27': 'remove' // Escape
},
_keyHandler: function(e, notifier) {
if (this._keys[e.keyCode]) {
e.direction = this._keys[e.keyCode];
notifier.fire(e);
}
},
on: function(node, sub, notifier) {
sub._detacher = node.on(this._event, this._keyHandler,
this, notifier);
}
});
M.qtype_ddimageortext.init_question = function(config) {
return new DDIMAGEORTEXT_QUESTION(config);
};
}, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});
@@ -1,369 +0,0 @@
YUI.add('moodle-qtype_ddimageortext-form', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This is the question editing form code.
*/
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
var DDIMAGEORTEXT_FORM = function() {
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
};
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
pendingid: '',
fp: null,
initializer: function() {
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.fp = this.file_pickers();
var tn = Y.one(this.get('topnode'));
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
'<div class="dropzones"></div></div>');
this.doc = this.doc_structure(this);
this.draw_dd_area();
},
draw_dd_area: function() {
var bgimageurl = this.fp.file('bgimage').href;
this.stop_selector_events();
this.set_options_for_drag_item_selectors();
if (bgimageurl !== null) {
this.doc.load_bg_img(bgimageurl);
this.load_drag_homes();
var drop = new Y.DD.Drop({
node: this.doc.bg_img()
});
// Listen for a drop:hit on the background image
drop.on('drop:hit', function(e) {
e.drag.get('node').setData('gooddrop', true);
});
this.afterimageloaddone = false;
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
this.doc.drag_item_homes()
.on('load', this.constrain_image_size, this, 'dragimage');
this.doc.bg_img().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
} else {
this.setup_form_events();
M.util.js_complete(this.pendingid);
}
this.update_visibility_of_file_pickers();
},
after_all_images_loaded: function() {
this.update_padding_sizes_all();
this.update_drag_instances();
this.reposition_drags_for_form();
this.set_options_for_drag_item_selectors();
this.setup_form_events();
Y.later(500, this, this.reposition_drags_for_form, [], true);
},
constrain_image_size: function(e, imagetype) {
var maxsize = this.get('maxsizes')[imagetype];
var reduceby = Math.max(e.target.get('width') / maxsize.width,
e.target.get('height') / maxsize.height);
if (reduceby > 1) {
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
}
e.target.addClass('constrained');
e.target.detach('load', this.constrain_image_size);
},
load_drag_homes: function() {
// Set up drag items homes.
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
this.load_drag_home(i);
}
},
load_drag_home: function(dragitemno) {
var url = null;
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
}
this.doc.add_or_update_drag_item_home(dragitemno, url,
this.form.get_form_value('draglabel', [dragitemno]),
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
},
update_drag_instances: function() {
// Set up drop zones.
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
if (drag !== null) {
this.doc.draggable_for_form(drag);
}
}
}
},
set_options_for_drag_item_selectors: function() {
var dragitemsoptions = {'0': ''};
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
var label = this.form.get_form_value('draglabel', [i]);
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
&& file.name !== null) {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
} else if (label !== '') {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
}
}
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var selector = Y.one('#id_drops_' + i + '_choice');
var selectedvalue = selector.get('value');
selector.all('option').remove(true);
for (var value in dragitemsoptions) {
value = +value;
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
selector.append(option);
var optionnode = selector.one('option[value="' + value + '"]');
if (value === +selectedvalue) {
optionnode.set('selected', true);
} else {
if (value !== 0) { // No item option is always selectable.
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
if (cbel && !cbel.get('checked')) {
if (this.item_is_allocated_to_dropzone(value)) {
optionnode.set('disabled', true);
}
}
}
}
}
}
},
stop_selector_events: function() {
Y.all('fieldset#id_dropzoneheader select').detachAll();
},
/**
* Checks if the specified drag item is allocated to a dropzone.
*
* @method item_is_allocated_to_dropzone
* @param {Number} value of the drag item to check
* @return {Boolean} true if item is allocated to dropzone
*/
item_is_allocated_to_dropzone: function(itemvalue) {
return Y.all('fieldset#id_dropzoneheader select').some(function(selectNode) {
return Number(selectNode.get('value')) === itemvalue;
});
},
setup_form_events: function() {
// Events triggered by changes to form data.
// X and y coordinates.
Y.all('fieldset#id_dropzoneheader input').on('blur', function(e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
}, this);
// Change in selected item.
Y.all('fieldset#id_dropzoneheader select').on('change', function(e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var old = this.doc.drag_item(draginstanceno);
if (old !== null) {
old.remove(true);
}
this.draw_dd_area();
}, this);
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
// Change to group selector.
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on('change', this.redraw_dd_area, this);
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on('change', this.redraw_dd_area, this);
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
.on('blur', this.set_options_for_drag_item_selectors, this);
// Change to infinite checkbox.
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
.on('change', this.set_options_for_drag_item_selectors, this);
}
// Event on file picker new file selection.
Y.after(function(e) {
var name = this.fp.name(e.id);
if (name !== 'bgimage') {
this.doc.drag_items().remove(true);
}
this.draw_dd_area();
}, M.form_filepicker, 'callback', this);
},
/**
* Redraws drag and drop preview area.
*
* @method redraw_dd_area
*/
redraw_dd_area: function() {
this.doc.drag_items().remove(true);
this.draw_dd_area();
},
update_visibility_of_file_pickers: function() {
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'block');
} else {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'none');
}
}
},
reposition_drags_for_form: function() {
this.doc.drag_items().each(function(drag) {
var draginstanceno = drag.getData('draginstanceno');
this.reposition_drag_for_form(draginstanceno);
}, this);
M.util.js_complete(this.pendingid);
},
reposition_drag_for_form: function(draginstanceno) {
var drag = this.doc.drag_item(draginstanceno);
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
if (fromform[0] === '' && fromform[1] === '') {
var dragitemno = drag.getData('dragitemno');
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
} else {
drag.setXY(this.convert_to_window_xy(fromform));
}
}
},
set_drag_xy: function(draginstanceno, xy) {
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
},
reset_drag_xy: function(draginstanceno) {
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
},
// make sure xy value is not out of bounds of bg image
constrain_xy: function(draginstanceno, bgimgxy) {
var drag = this.doc.drag_item(draginstanceno);
var xleftconstrained =
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
var ytopconstrained =
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
xleftconstrained = Math.max(xleftconstrained, 0);
ytopconstrained = Math.max(ytopconstrained, 0);
return [xleftconstrained, ytopconstrained];
},
convert_to_bg_img_xy: function(windowxy) {
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
},
/**
* Low level operations on form.
*/
form: {
to_name_with_index: function(name, indexes) {
var indexstring = name;
for (var i = 0; i < indexes.length; i++) {
indexstring = indexstring + '[' + indexes[i] + ']';
}
return indexstring;
},
get_el: function(name, indexes) {
var form = document.getElementById('mform1');
return form.elements[this.to_name_with_index(name, indexes)];
},
get_form_value: function(name, indexes) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
return el.checked;
} else {
return el.value;
}
},
set_form_value: function(name, indexes, value) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
},
from_name_with_index: function(name) {
var toreturn = {};
toreturn.indexes = [];
var bracket = name.indexOf('[');
toreturn.name = name.substring(0, bracket);
while (bracket !== -1) {
var end = name.indexOf(']', bracket + 1);
toreturn.indexes.push(name.substring(bracket + 1, end));
bracket = name.indexOf('[', end + 1);
}
return toreturn;
}
},
file_pickers: function() {
var draftitemidstoname;
var nametoparentnode;
if (draftitemidstoname === undefined) {
draftitemidstoname = {};
nametoparentnode = {};
var filepickers = Y.all('form.mform input.filepickerhidden');
filepickers.each(function(filepicker) {
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
}, this);
}
var toreturn = {
file: function(name) {
var parentnode = nametoparentnode[name];
var fileanchor = parentnode.one('div.filepicker-filelist a');
if (fileanchor) {
return {href: fileanchor.get('href'), name: fileanchor.get('innerHTML')};
} else {
return {href: null, name: null};
}
},
name: function(draftitemid) {
return draftitemidstoname[draftitemid];
}
};
return toreturn;
}
}, {NAME: DDIMAGEORTEXTFORMNAME, ATTRS: {maxsizes: {value: null}}});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.init_form = function(config) {
return new DDIMAGEORTEXT_FORM(config);
};
}, '@VERSION@', {"requires": ["moodle-qtype_ddimageortext-dd", "form_filepicker"]});
File diff suppressed because one or more lines are too long
@@ -1,369 +0,0 @@
YUI.add('moodle-qtype_ddimageortext-form', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This is the question editing form code.
*/
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
var DDIMAGEORTEXT_FORM = function() {
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
};
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
pendingid: '',
fp: null,
initializer: function() {
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.fp = this.file_pickers();
var tn = Y.one(this.get('topnode'));
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
'<div class="dropzones"></div></div>');
this.doc = this.doc_structure(this);
this.draw_dd_area();
},
draw_dd_area: function() {
var bgimageurl = this.fp.file('bgimage').href;
this.stop_selector_events();
this.set_options_for_drag_item_selectors();
if (bgimageurl !== null) {
this.doc.load_bg_img(bgimageurl);
this.load_drag_homes();
var drop = new Y.DD.Drop({
node: this.doc.bg_img()
});
// Listen for a drop:hit on the background image
drop.on('drop:hit', function(e) {
e.drag.get('node').setData('gooddrop', true);
});
this.afterimageloaddone = false;
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
this.doc.drag_item_homes()
.on('load', this.constrain_image_size, this, 'dragimage');
this.doc.bg_img().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
} else {
this.setup_form_events();
M.util.js_complete(this.pendingid);
}
this.update_visibility_of_file_pickers();
},
after_all_images_loaded: function() {
this.update_padding_sizes_all();
this.update_drag_instances();
this.reposition_drags_for_form();
this.set_options_for_drag_item_selectors();
this.setup_form_events();
Y.later(500, this, this.reposition_drags_for_form, [], true);
},
constrain_image_size: function(e, imagetype) {
var maxsize = this.get('maxsizes')[imagetype];
var reduceby = Math.max(e.target.get('width') / maxsize.width,
e.target.get('height') / maxsize.height);
if (reduceby > 1) {
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
}
e.target.addClass('constrained');
e.target.detach('load', this.constrain_image_size);
},
load_drag_homes: function() {
// Set up drag items homes.
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
this.load_drag_home(i);
}
},
load_drag_home: function(dragitemno) {
var url = null;
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
}
this.doc.add_or_update_drag_item_home(dragitemno, url,
this.form.get_form_value('draglabel', [dragitemno]),
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
},
update_drag_instances: function() {
// Set up drop zones.
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
if (drag !== null) {
this.doc.draggable_for_form(drag);
}
}
}
},
set_options_for_drag_item_selectors: function() {
var dragitemsoptions = {'0': ''};
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
var label = this.form.get_form_value('draglabel', [i]);
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
&& file.name !== null) {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
} else if (label !== '') {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
}
}
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var selector = Y.one('#id_drops_' + i + '_choice');
var selectedvalue = selector.get('value');
selector.all('option').remove(true);
for (var value in dragitemsoptions) {
value = +value;
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
selector.append(option);
var optionnode = selector.one('option[value="' + value + '"]');
if (value === +selectedvalue) {
optionnode.set('selected', true);
} else {
if (value !== 0) { // No item option is always selectable.
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
if (cbel && !cbel.get('checked')) {
if (this.item_is_allocated_to_dropzone(value)) {
optionnode.set('disabled', true);
}
}
}
}
}
}
},
stop_selector_events: function() {
Y.all('fieldset#id_dropzoneheader select').detachAll();
},
/**
* Checks if the specified drag item is allocated to a dropzone.
*
* @method item_is_allocated_to_dropzone
* @param {Number} value of the drag item to check
* @return {Boolean} true if item is allocated to dropzone
*/
item_is_allocated_to_dropzone: function(itemvalue) {
return Y.all('fieldset#id_dropzoneheader select').some(function(selectNode) {
return Number(selectNode.get('value')) === itemvalue;
});
},
setup_form_events: function() {
// Events triggered by changes to form data.
// X and y coordinates.
Y.all('fieldset#id_dropzoneheader input').on('blur', function(e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
}, this);
// Change in selected item.
Y.all('fieldset#id_dropzoneheader select').on('change', function(e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var old = this.doc.drag_item(draginstanceno);
if (old !== null) {
old.remove(true);
}
this.draw_dd_area();
}, this);
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
// Change to group selector.
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on('change', this.redraw_dd_area, this);
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on('change', this.redraw_dd_area, this);
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
.on('blur', this.set_options_for_drag_item_selectors, this);
// Change to infinite checkbox.
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
.on('change', this.set_options_for_drag_item_selectors, this);
}
// Event on file picker new file selection.
Y.after(function(e) {
var name = this.fp.name(e.id);
if (name !== 'bgimage') {
this.doc.drag_items().remove(true);
}
this.draw_dd_area();
}, M.form_filepicker, 'callback', this);
},
/**
* Redraws drag and drop preview area.
*
* @method redraw_dd_area
*/
redraw_dd_area: function() {
this.doc.drag_items().remove(true);
this.draw_dd_area();
},
update_visibility_of_file_pickers: function() {
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'block');
} else {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'none');
}
}
},
reposition_drags_for_form: function() {
this.doc.drag_items().each(function(drag) {
var draginstanceno = drag.getData('draginstanceno');
this.reposition_drag_for_form(draginstanceno);
}, this);
M.util.js_complete(this.pendingid);
},
reposition_drag_for_form: function(draginstanceno) {
var drag = this.doc.drag_item(draginstanceno);
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
if (fromform[0] === '' && fromform[1] === '') {
var dragitemno = drag.getData('dragitemno');
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
} else {
drag.setXY(this.convert_to_window_xy(fromform));
}
}
},
set_drag_xy: function(draginstanceno, xy) {
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
},
reset_drag_xy: function(draginstanceno) {
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
},
// make sure xy value is not out of bounds of bg image
constrain_xy: function(draginstanceno, bgimgxy) {
var drag = this.doc.drag_item(draginstanceno);
var xleftconstrained =
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
var ytopconstrained =
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
xleftconstrained = Math.max(xleftconstrained, 0);
ytopconstrained = Math.max(ytopconstrained, 0);
return [xleftconstrained, ytopconstrained];
},
convert_to_bg_img_xy: function(windowxy) {
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
},
/**
* Low level operations on form.
*/
form: {
to_name_with_index: function(name, indexes) {
var indexstring = name;
for (var i = 0; i < indexes.length; i++) {
indexstring = indexstring + '[' + indexes[i] + ']';
}
return indexstring;
},
get_el: function(name, indexes) {
var form = document.getElementById('mform1');
return form.elements[this.to_name_with_index(name, indexes)];
},
get_form_value: function(name, indexes) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
return el.checked;
} else {
return el.value;
}
},
set_form_value: function(name, indexes, value) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
},
from_name_with_index: function(name) {
var toreturn = {};
toreturn.indexes = [];
var bracket = name.indexOf('[');
toreturn.name = name.substring(0, bracket);
while (bracket !== -1) {
var end = name.indexOf(']', bracket + 1);
toreturn.indexes.push(name.substring(bracket + 1, end));
bracket = name.indexOf('[', end + 1);
}
return toreturn;
}
},
file_pickers: function() {
var draftitemidstoname;
var nametoparentnode;
if (draftitemidstoname === undefined) {
draftitemidstoname = {};
nametoparentnode = {};
var filepickers = Y.all('form.mform input.filepickerhidden');
filepickers.each(function(filepicker) {
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
}, this);
}
var toreturn = {
file: function(name) {
var parentnode = nametoparentnode[name];
var fileanchor = parentnode.one('div.filepicker-filelist a');
if (fileanchor) {
return {href: fileanchor.get('href'), name: fileanchor.get('innerHTML')};
} else {
return {href: null, name: null};
}
},
name: function(draftitemid) {
return draftitemidstoname[draftitemid];
}
};
return toreturn;
}
}, {NAME: DDIMAGEORTEXTFORMNAME, ATTRS: {maxsizes: {value: null}}});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.init_form = function(config) {
return new DDIMAGEORTEXT_FORM(config);
};
}, '@VERSION@', {"requires": ["moodle-qtype_ddimageortext-dd", "form_filepicker"]});
@@ -1,10 +0,0 @@
{
"name": "moodle-qtype_ddimageortext-dd",
"builds": {
"moodle-qtype_ddimageortext-dd": {
"jsfiles": [
"ddimageortext.js"
]
}
}
}
@@ -1,541 +0,0 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
var DDIMAGEORTEXT_DD = function() {
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
};
/**
* This is the base class for the question rendering and question editing form code.
*/
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
doc: null,
polltimer: null,
afterimageloaddone: false,
poll_for_image_load: function(e, waitforimageconstrain, pause, doafterwords) {
if (this.afterimageloaddone) {
return;
}
var bgdone = this.doc.bg_img().get('complete');
if (waitforimageconstrain) {
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
}
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome) {
// in 'some' loop returning true breaks the loop and is passed as return value from
// 'some' else returns false. Can be though of as equivalent to ||.
if (dragitemhome.get('tagName') !== 'IMG') {
return false;
}
var done = (dragitemhome.get('complete'));
if (waitforimageconstrain) {
done = done && dragitemhome.hasClass('constrained');
}
return !done;
});
if (bgdone && alldragsloaded) {
if (this.polltimer !== null) {
this.polltimer.cancel();
this.polltimer = null;
}
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
this.doc.bg_img().detach('load', this.poll_for_image_load);
if (pause !== 0) {
Y.later(pause, this, doafterwords);
} else {
doafterwords.call(this);
}
this.afterimageloaddone = true;
} else if (this.polltimer === null) {
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
this.polltimer =
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
}
},
/**
* Object to encapsulate operations on dd area.
*/
doc_structure: function(mainobj) {
var topnode = Y.one(this.get('topnode'));
var dragitemsarea = topnode.one('div.dragitems');
var dropbgarea = topnode.one('div.droparea');
return {
top_node: function() {
return topnode;
},
drag_items: function() {
return dragitemsarea.all('.drag');
},
drop_zones: function() {
return topnode.all('div.dropzones div.dropzone');
},
drop_zone_group: function(groupno) {
return topnode.all('div.dropzones div.group' + groupno);
},
drag_items_cloned_from: function(dragitemno) {
return dragitemsarea.all('.dragitems' + dragitemno);
},
drag_item: function(draginstanceno) {
return dragitemsarea.one('.draginstance' + draginstanceno);
},
drag_items_in_group: function(groupno) {
return dragitemsarea.all('.drag.group' + groupno);
},
drag_item_homes: function() {
return dragitemsarea.all('.draghome');
},
bg_img: function() {
return topnode.one('.dropbackground');
},
load_bg_img: function(url) {
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
},
add_or_update_drag_item_home: function(dragitemno, url, alt, group) {
var oldhome = this.drag_item_home(dragitemno);
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
if (oldhome === null) {
if (url) {
dragitemsarea.append(imghtml);
} else if (alt !== '') {
dragitemsarea.append(divhtml);
}
} else {
if (url) {
dragitemsarea.insert(imghtml, oldhome);
} else if (alt !== '') {
dragitemsarea.insert(divhtml, oldhome);
}
oldhome.remove(true);
}
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
if (newlycreated !== null) {
newlycreated.setData('groupno', group);
newlycreated.setData('dragitemno', dragitemno);
}
},
drag_item_home: function(dragitemno) {
return dragitemsarea.one('.dragitemhomes' + dragitemno);
},
get_classname_numeric_suffix: function(node, prefix) {
var classes = node.getAttribute('class');
if (classes !== '') {
var classesarr = classes.split(' ');
for (var index = 0; index < classesarr.length; index++) {
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
if (patt1.test(classesarr[index])) {
var patt2 = new RegExp('([0-9])+$');
var match = patt2.exec(classesarr[index]);
return +match[0];
}
}
}
throw 'Prefix "' + prefix + '" not found in class names.';
},
clone_new_drag_item: function(draginstanceno, dragitemno) {
var draghome = this.drag_item_home(dragitemno);
if (draghome === null) {
return null;
}
var drag = draghome.cloneNode(true);
drag.removeClass('dragitemhomes' + dragitemno);
drag.addClass('dragitems' + dragitemno);
drag.addClass('draginstance' + draginstanceno);
drag.removeClass('draghome');
drag.addClass('drag');
drag.setStyles({'visibility': 'visible', 'position': 'absolute'});
drag.setData('draginstanceno', draginstanceno);
drag.setData('dragitemno', dragitemno);
draghome.get('parentNode').appendChild(drag);
return drag;
},
draggable_for_question: function(drag, group, choice) {
new Y.DD.Drag({
node: drag,
dragMode: 'point',
groups: [group]
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
drag.setData('group', group);
drag.setData('choice', choice);
},
draggable_for_form: function(drag) {
var dd = new Y.DD.Drag({
node: drag,
dragMode: 'point'
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
dd.on('drag:end', function(e) {
var dragnode = e.target.get('node');
var draginstanceno = dragnode.getData('draginstanceno');
var gooddrop = dragnode.getData('gooddrop');
if (!gooddrop) {
mainobj.reset_drag_xy(draginstanceno);
} else {
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
}
}, this);
dd.on('drag:start', function(e) {
var drag = e.target;
drag.get('node').setData('gooddrop', false);
}, this);
}
};
},
update_padding_sizes_all: function() {
for (var groupno = 1; groupno <= 8; groupno++) {
this.update_padding_size_for_group(groupno);
}
},
update_padding_size_for_group: function(groupno) {
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
if (groupitems.size() !== 0) {
var maxwidth = 0;
var maxheight = 0;
groupitems.each(function(item) {
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
maxheight = Math.max(maxheight, item.get('clientHeight'));
}, this);
groupitems.each(function(item) {
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
margintopbottom + 'px ' + marginleftright + 'px');
}, this);
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
'height': maxheight + 10});
}
},
convert_to_window_xy: function(bgimgxy) {
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
}
}, {
NAME: DDIMAGEORTEXTDDNAME,
ATTRS: {
drops: {value: null},
readonly: {value: false},
topnode: {value: null}
}
});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
var DDIMAGEORTEXT_QUESTION = function() {
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
};
/**
* This is the code for question rendering.
*/
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
passiveSupported: false,
pendingid: '',
initializer: function() {
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.doc = this.doc_structure(this);
this.poll_for_image_load(null, false, 10, this.create_all_drag_and_drops);
this.doc.bg_img().after('load', this.poll_for_image_load, this,
false, 10, this.create_all_drag_and_drops);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
false, 10, this.create_all_drag_and_drops);
if (!this.get('readonly')) {
Y.later(500, this, this.reposition_drags_for_question, true);
} else {
Y.one('window').on('resize', function() {
this.reposition_drags_for_question();
}, this);
}
this.checkPassiveSupported();
},
/**
* prevent_touchmove_from_scrolling allows users of touch screen devices to
* use drag and drop and normal scrolling at the same time. I.e. when
* touching and dragging a draggable item, the screen does not scroll, but
* you can scroll by touching other area of the screen apart from the
* draggable items.
*/
prevent_touchmove_from_scrolling: function(drag) {
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
var eventHandler = function(event) {
event.preventDefault();
};
var dragId = drag.get('id');
var el = document.getElementById(dragId);
// Note do not dynamically add events within another event, as this causes issues on iOS11.3.
// See https://github.com/atlassian/react-beautiful-dnd/issues/413 and
// https://bugs.webkit.org/show_bug.cgi?id=184250 for fuller explanation.
el.addEventListener(touchmove, eventHandler, this.passiveSupported ? {passive: false, capture: true} : false);
},
/**
* Some older browsers do not support passing an options object to addEventListener.
* This is a check from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener.
*/
checkPassiveSupported: function() {
try {
var options = Object.defineProperty({}, 'passive', {
get: function() {
this.passiveSupported = true;
}.bind(this)
});
window.addEventListener('test', options, options);
window.removeEventListener('test', options, options);
} catch (err) {
this.passiveSupported = false;
}
},
create_all_drag_and_drops: function() {
this.init_drops();
this.update_padding_sizes_all();
var i = 0;
this.doc.drag_item_homes().each(function(dragitemhome) {
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
var choice = +this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
var group = +this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
var groupsize = this.doc.drop_zone_group(group).size();
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
if (dragnode.hasClass('infinite')) {
var dragstocreate = groupsize - 1;
while (dragstocreate > 0) {
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
dragstocreate--;
}
}
}, this);
this.reposition_drags_for_question();
if (!this.get('readonly')) {
this.doc.drop_zones().set('tabIndex', 0);
this.doc.drop_zones().each(
function(v) {
v.on('dragchange', this.drop_zone_key_press, this);
}, this);
}
M.util.js_complete(this.pendingid);
},
drop_zone_key_press: function(e) {
switch (e.direction) {
case 'next' :
this.place_next_drag_in(e.target);
break;
case 'previous' :
this.place_previous_drag_in(e.target);
break;
case 'remove' :
this.remove_drag_from_drop(e.target);
break;
}
e.preventDefault();
this.reposition_drags_for_question();
},
place_next_drag_in: function(drop) {
this.search_for_unplaced_drop_choice(drop, 1);
},
place_previous_drag_in: function(drop) {
this.search_for_unplaced_drop_choice(drop, -1);
},
search_for_unplaced_drop_choice: function(drop, direction) {
var next;
var current = this.current_drag_in_drop(drop);
if ('' === current) {
if (direction === 1) {
next = 1;
} else {
next = 1;
var groupno = drop.getData('group');
this.doc.drag_items_in_group(groupno).each(function(drag) {
next = Math.max(next, drag.getData('choice'));
}, this);
}
} else {
next = +current + direction;
}
var drag;
do {
if (this.get_choices_for_drop(next, drop).size() === 0) {
this.remove_drag_from_drop(drop);
return;
} else {
drag = this.get_unplaced_choice_for_drop(next, drop);
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_drag_in_drop: function(drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
return inputnode.get('value');
},
remove_drag_from_drop: function(drop) {
this.place_drag_in_drop(null, drop);
},
place_drag_in_drop: function(drag, drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
if (drag !== null) {
inputnode.set('value', drag.getData('choice'));
} else {
inputnode.set('value', '');
}
},
reposition_drags_for_question: function(dotimeout) {
this.doc.drag_items().removeClass('placed');
this.doc.drag_items().each(function(dragitem) {
if (dragitem.dd !== undefined) {
dragitem.dd.detachAll('drag:start');
}
}, this);
this.doc.drop_zones().each(function(dropzone) {
var relativexy = dropzone.getData('xy');
dropzone.setXY(this.convert_to_window_xy(relativexy));
var inputcss = 'input#' + dropzone.getData('inputid');
var input = this.doc.top_node().one(inputcss);
var choice = input.get('value');
if (choice !== "") {
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
if (dragitem !== null) {
dragitem.setXY(dropzone.getXY());
dragitem.addClass('placed');
if (dragitem.dd !== undefined) {
dragitem.dd.once('drag:start', function(e, input) {
input.set('value', '');
e.target.get('node').removeClass('placed');
}, this, input);
}
}
}
}, this);
this.doc.drag_items().each(function(dragitem) {
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
dragitem.setXY(dragitemhome.getXY());
}
}, this);
if (dotimeout) {
Y.later(500, this, this.reposition_drags_for_question, true);
}
},
get_choices_for_drop: function(choice, drop) {
var group = drop.getData('group');
return this.doc.top_node().all(
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
},
get_unplaced_choice_for_drop: function(choice, drop) {
var dragitems = this.get_choices_for_drop(choice, drop);
var dragitem = null;
dragitems.some(function(d) {
if (this.get('readonly') || (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging'))) {
dragitem = d;
return true;
} else {
return false;
}
});
return dragitem;
},
init_drops: function() {
var dropareas = this.doc.top_node().one('div.dropzones');
var groupnodes = {};
for (var groupno = 1; groupno <= 8; groupno++) {
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
dropareas.append(groupnode);
groupnodes[groupno] = groupnode;
}
var drop_hit_handler = function(e) {
var drag = e.drag.get('node');
var drop = e.drop.get('node');
if (Number(drop.getData('group')) === drag.getData('group')) {
this.place_drag_in_drop(drag, drop);
}
};
for (var dropno in this.get('drops')) {
var drop = this.get('drops')[dropno];
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
var title = drop.text.replace('"', '\"');
if (!title) {
title = M.util.get_string('blank', 'qtype_ddimageortext');
}
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '">' +
'<span class="accesshide">' + title + '</span>&nbsp;</div>';
var dropnode = Y.Node.create(dropnodehtml);
groupnodes[drop.group].append(dropnode);
dropnode.setStyles({'opacity': 0.5});
dropnode.setData('xy', drop.xy);
dropnode.setData('place', dropno);
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
dropnode.setData('group', drop.group);
var dropdd = new Y.DD.Drop({
node: dropnode, groups: [drop.group]});
dropdd.on('drop:hit', drop_hit_handler, this);
}
}
}, {NAME: DDIMAGEORTEXTQUESTIONNAME, ATTRS: {}});
Y.Event.define('dragchange', {
// Webkit and IE repeat keydown when you hold down arrow keys.
// Opera links keypress to page scroll; others keydown.
// Firefox prevents page scroll via preventDefault() on either
// keydown or keypress.
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
_keys: {
'32': 'next', // Space
'37': 'previous', // Left arrow
'38': 'previous', // Up arrow
'39': 'next', // Right arrow
'40': 'next', // Down arrow
'27': 'remove' // Escape
},
_keyHandler: function(e, notifier) {
if (this._keys[e.keyCode]) {
e.direction = this._keys[e.keyCode];
notifier.fire(e);
}
},
on: function(node, sub, notifier) {
sub._detacher = node.on(this._event, this._keyHandler,
this, notifier);
}
});
M.qtype_ddimageortext.init_question = function(config) {
return new DDIMAGEORTEXT_QUESTION(config);
};
@@ -1,10 +0,0 @@
{
"moodle-qtype_ddimageortext-dd": {
"requires": [
"node",
"dd",
"dd-drop",
"dd-constrain"
]
}
}
@@ -1,10 +0,0 @@
{
"name": "moodle-qtype_ddimageortext-form",
"builds": {
"moodle-qtype_ddimageortext-form": {
"jsfiles": [
"form.js"
]
}
}
}
-364
View File
@@ -1,364 +0,0 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This is the question editing form code.
*/
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
var DDIMAGEORTEXT_FORM = function() {
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
};
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
pendingid: '',
fp: null,
initializer: function() {
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.fp = this.file_pickers();
var tn = Y.one(this.get('topnode'));
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
'<div class="dropzones"></div></div>');
this.doc = this.doc_structure(this);
this.draw_dd_area();
},
draw_dd_area: function() {
var bgimageurl = this.fp.file('bgimage').href;
this.stop_selector_events();
this.set_options_for_drag_item_selectors();
if (bgimageurl !== null) {
this.doc.load_bg_img(bgimageurl);
this.load_drag_homes();
var drop = new Y.DD.Drop({
node: this.doc.bg_img()
});
// Listen for a drop:hit on the background image
drop.on('drop:hit', function(e) {
e.drag.get('node').setData('gooddrop', true);
});
this.afterimageloaddone = false;
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
this.doc.drag_item_homes()
.on('load', this.constrain_image_size, this, 'dragimage');
this.doc.bg_img().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
} else {
this.setup_form_events();
M.util.js_complete(this.pendingid);
}
this.update_visibility_of_file_pickers();
},
after_all_images_loaded: function() {
this.update_padding_sizes_all();
this.update_drag_instances();
this.reposition_drags_for_form();
this.set_options_for_drag_item_selectors();
this.setup_form_events();
Y.later(500, this, this.reposition_drags_for_form, [], true);
},
constrain_image_size: function(e, imagetype) {
var maxsize = this.get('maxsizes')[imagetype];
var reduceby = Math.max(e.target.get('width') / maxsize.width,
e.target.get('height') / maxsize.height);
if (reduceby > 1) {
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
}
e.target.addClass('constrained');
e.target.detach('load', this.constrain_image_size);
},
load_drag_homes: function() {
// Set up drag items homes.
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
this.load_drag_home(i);
}
},
load_drag_home: function(dragitemno) {
var url = null;
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
}
this.doc.add_or_update_drag_item_home(dragitemno, url,
this.form.get_form_value('draglabel', [dragitemno]),
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
},
update_drag_instances: function() {
// Set up drop zones.
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
if (drag !== null) {
this.doc.draggable_for_form(drag);
}
}
}
},
set_options_for_drag_item_selectors: function() {
var dragitemsoptions = {'0': ''};
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
var label = this.form.get_form_value('draglabel', [i]);
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
&& file.name !== null) {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
} else if (label !== '') {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
}
}
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var selector = Y.one('#id_drops_' + i + '_choice');
var selectedvalue = selector.get('value');
selector.all('option').remove(true);
for (var value in dragitemsoptions) {
value = +value;
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
selector.append(option);
var optionnode = selector.one('option[value="' + value + '"]');
if (value === +selectedvalue) {
optionnode.set('selected', true);
} else {
if (value !== 0) { // No item option is always selectable.
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
if (cbel && !cbel.get('checked')) {
if (this.item_is_allocated_to_dropzone(value)) {
optionnode.set('disabled', true);
}
}
}
}
}
}
},
stop_selector_events: function() {
Y.all('fieldset#id_dropzoneheader select').detachAll();
},
/**
* Checks if the specified drag item is allocated to a dropzone.
*
* @method item_is_allocated_to_dropzone
* @param {Number} value of the drag item to check
* @return {Boolean} true if item is allocated to dropzone
*/
item_is_allocated_to_dropzone: function(itemvalue) {
return Y.all('fieldset#id_dropzoneheader select').some(function(selectNode) {
return Number(selectNode.get('value')) === itemvalue;
});
},
setup_form_events: function() {
// Events triggered by changes to form data.
// X and y coordinates.
Y.all('fieldset#id_dropzoneheader input').on('blur', function(e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
}, this);
// Change in selected item.
Y.all('fieldset#id_dropzoneheader select').on('change', function(e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var old = this.doc.drag_item(draginstanceno);
if (old !== null) {
old.remove(true);
}
this.draw_dd_area();
}, this);
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
// Change to group selector.
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on('change', this.redraw_dd_area, this);
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on('change', this.redraw_dd_area, this);
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
.on('blur', this.set_options_for_drag_item_selectors, this);
// Change to infinite checkbox.
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
.on('change', this.set_options_for_drag_item_selectors, this);
}
// Event on file picker new file selection.
Y.after(function(e) {
var name = this.fp.name(e.id);
if (name !== 'bgimage') {
this.doc.drag_items().remove(true);
}
this.draw_dd_area();
}, M.form_filepicker, 'callback', this);
},
/**
* Redraws drag and drop preview area.
*
* @method redraw_dd_area
*/
redraw_dd_area: function() {
this.doc.drag_items().remove(true);
this.draw_dd_area();
},
update_visibility_of_file_pickers: function() {
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'block');
} else {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'none');
}
}
},
reposition_drags_for_form: function() {
this.doc.drag_items().each(function(drag) {
var draginstanceno = drag.getData('draginstanceno');
this.reposition_drag_for_form(draginstanceno);
}, this);
M.util.js_complete(this.pendingid);
},
reposition_drag_for_form: function(draginstanceno) {
var drag = this.doc.drag_item(draginstanceno);
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
if (fromform[0] === '' && fromform[1] === '') {
var dragitemno = drag.getData('dragitemno');
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
} else {
drag.setXY(this.convert_to_window_xy(fromform));
}
}
},
set_drag_xy: function(draginstanceno, xy) {
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
},
reset_drag_xy: function(draginstanceno) {
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
},
// make sure xy value is not out of bounds of bg image
constrain_xy: function(draginstanceno, bgimgxy) {
var drag = this.doc.drag_item(draginstanceno);
var xleftconstrained =
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
var ytopconstrained =
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
xleftconstrained = Math.max(xleftconstrained, 0);
ytopconstrained = Math.max(ytopconstrained, 0);
return [xleftconstrained, ytopconstrained];
},
convert_to_bg_img_xy: function(windowxy) {
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
},
/**
* Low level operations on form.
*/
form: {
to_name_with_index: function(name, indexes) {
var indexstring = name;
for (var i = 0; i < indexes.length; i++) {
indexstring = indexstring + '[' + indexes[i] + ']';
}
return indexstring;
},
get_el: function(name, indexes) {
var form = document.getElementById('mform1');
return form.elements[this.to_name_with_index(name, indexes)];
},
get_form_value: function(name, indexes) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
return el.checked;
} else {
return el.value;
}
},
set_form_value: function(name, indexes, value) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
},
from_name_with_index: function(name) {
var toreturn = {};
toreturn.indexes = [];
var bracket = name.indexOf('[');
toreturn.name = name.substring(0, bracket);
while (bracket !== -1) {
var end = name.indexOf(']', bracket + 1);
toreturn.indexes.push(name.substring(bracket + 1, end));
bracket = name.indexOf('[', end + 1);
}
return toreturn;
}
},
file_pickers: function() {
var draftitemidstoname;
var nametoparentnode;
if (draftitemidstoname === undefined) {
draftitemidstoname = {};
nametoparentnode = {};
var filepickers = Y.all('form.mform input.filepickerhidden');
filepickers.each(function(filepicker) {
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
}, this);
}
var toreturn = {
file: function(name) {
var parentnode = nametoparentnode[name];
var fileanchor = parentnode.one('div.filepicker-filelist a');
if (fileanchor) {
return {href: fileanchor.get('href'), name: fileanchor.get('innerHTML')};
} else {
return {href: null, name: null};
}
},
name: function(draftitemid) {
return draftitemidstoname[draftitemid];
}
};
return toreturn;
}
}, {NAME: DDIMAGEORTEXTFORMNAME, ATTRS: {maxsizes: {value: null}}});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.init_form = function(config) {
return new DDIMAGEORTEXT_FORM(config);
};
@@ -1,8 +0,0 @@
{
"moodle-qtype_ddimageortext-form": {
"requires": [
"moodle-qtype_ddimageortext-dd",
"form_filepicker"
]
}
}