1
0
mirror of https://github.com/moodle/moodle.git synced 2025-04-11 11:23:52 +02:00

MDL-68382 Drag and drop marker questions: responsive support

These changes also make it possible to print these questions.
This commit is contained in:
Huong Nguyen 2020-04-08 17:25:41 +07:00
parent 9df4a4de18
commit d2fa9e7981
14 changed files with 535 additions and 319 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -51,7 +51,8 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes'], function($, dragDro
if (this.shape.getCoordinates() === coordinates) {
return;
}
if (!this.shape.parse(coordinates)) {
// We don't need to scale the shape for editing form.
if (!this.shape.parse(coordinates, 1)) {
// Invalid coordinates. Don't update the preview.
return;
}
@ -70,6 +71,8 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes'], function($, dragDro
// Simple update.
this.updateSvgEl();
}
// Update the rounded coordinates if needed.
this.setCoordinatesInForm();
};
/**

@ -30,56 +30,44 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
* Object to handle one drag-drop markers question.
*
* @param {String} containerId id of the outer div for this question.
* @param {String} bgImgUrl the URL of the background image.
* @param {boolean} readOnly whether the question is being displayed read-only.
* @param {Object[]} visibleDropZones the geometry of any drop-zones to show.
* Objects have fields shape, coords and markertext.
* @constructor
*/
function DragDropMarkersQuestion(containerId, bgImgUrl, readOnly, visibleDropZones) {
function DragDropMarkersQuestion(containerId, readOnly, visibleDropZones) {
var thisQ = this;
this.containerId = containerId;
this.visibleDropZones = visibleDropZones;
this.shapes = [];
this.shapeSVGs = [];
this.isPrinting = false;
if (readOnly) {
this.getRoot().addClass('qtype_ddmarker-readonly');
}
this.loadImage(bgImgUrl);
thisQ.cloneDrags();
thisQ.repositionDrags();
thisQ.drawDropzones();
}
/**
* Load the background image is loaded, then do the rest of the display.
*
* @param {String} bgImgUrl the URL of the background image.
*/
DragDropMarkersQuestion.prototype.loadImage = function(bgImgUrl) {
var thisQ = this;
this.getRoot().find('.dropbackground')
.one('load', function() {
if (thisQ.visibleDropZones.length > 0) {
thisQ.drawDropzones();
}
thisQ.repositionDrags();
})
.attr('src', bgImgUrl)
.css({'border': '1px solid #000', 'max-width': 'none'});
};
/**
* Draws the svg shapes of any drop zones that should be visible for feedback purposes.
*/
DragDropMarkersQuestion.prototype.drawDropzones = function() {
var bgImage = this.getRoot().find('img.dropbackground');
if (this.visibleDropZones.length > 0) {
var bgImage = this.bgImage();
this.getRoot().find('div.dropzones').html('<svg xmlns="http://www.w3.org/2000/svg" class="dropzones" ' +
'width="' + bgImage.outerWidth() + '" ' +
'height="' + bgImage.outerHeight() + '"></svg>');
var svg = this.getRoot().find('svg.dropzones');
svg.css('position', 'absolute');
this.getRoot().find('div.dropzones').html('<svg xmlns="http://www.w3.org/2000/svg" class="dropzones" ' +
'width="' + bgImage.outerWidth() + '" ' +
'height="' + bgImage.outerHeight() + '"></svg>');
var svg = this.getRoot().find('svg.dropzones');
var nextColourIndex = 0;
for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {
var colourClass = 'color' + nextColourIndex;
nextColourIndex = (nextColourIndex + 1) % 8;
this.addDropzone(svg, dropZoneNo, colourClass);
var nextColourIndex = 0;
for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {
var colourClass = 'color' + nextColourIndex;
nextColourIndex = (nextColourIndex + 1) % 8;
this.addDropzone(svg, dropZoneNo, colourClass);
}
}
};
@ -93,8 +81,9 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {
var dropZone = this.visibleDropZones[dropZoneNo],
shape = Shapes.make(dropZone.shape, ''),
existingmarkertext;
if (!shape.parse(dropZone.coords)) {
existingmarkertext,
bgRatio = this.bgRatio();
if (!shape.parse(dropZone.coords, bgRatio)) {
return;
}
@ -109,40 +98,26 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
var classnames = 'markertext markertext' + dropZoneNo;
this.getRoot().find('div.markertexts').append('<span class="' + classnames + '">' +
dropZone.markertext + '</span>');
var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);
if (markerspan.length) {
var handles = shape.getHandlePositions();
var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;
var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);
markerspan
.css('left', positionLeft)
.css('top', positionTop);
markerspan
.data('originX', markerspan.position().left / bgRatio)
.data('originY', markerspan.position().top / bgRatio);
this.handleElementScale(markerspan, 'center');
}
}
var shapeSVG = shape.makeSvg(svg[0]);
shapeSVG.setAttribute('class', 'dropzone ' + colourClass);
};
/**
* Draws the drag items on the page (and drop zones if required).
* The idea is to re-draw all the drags and drops whenever there is a change
* like a widow resize or an item dropped in place.
*/
DragDropMarkersQuestion.prototype.repositionDropZones = function() {
var svg = this.getRoot().find('svg.dropzones');
if (svg.length === 0) {
return;
}
var bgPosition = this.convertToWindowXY(new Shapes.Point(-1, 0));
svg.offset({'left': bgPosition.x, 'top': bgPosition.y});
for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {
var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);
if (markerspan.length === 0) {
continue;
}
var dropZone = this.visibleDropZones[dropZoneNo],
shape = Shapes.make(dropZone.shape, '');
if (!shape.parse(dropZone.coords)) {
continue;
}
var handles = shape.getHandlePositions(),
textPos = this.convertToWindowXY(handles.moveHandle.offset(
-markerspan.outerWidth() / 2, -markerspan.outerHeight() / 2));
markerspan.offset({'left': textPos.x - 4, 'top': textPos.y});
}
this.shapes[this.shapes.length] = shape;
this.shapeSVGs[this.shapeSVGs.length] = shapeSVG;
};
/**
@ -154,37 +129,25 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
var root = this.getRoot(),
thisQ = this;
root.find('div.dragitems .dragitem').each(function(key, item) {
root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {
$(item).addClass('unneeded');
});
root.find('input.choices').each(function(key, input) {
var choiceNo = thisQ.getChoiceNoFromElement(input),
coords = thisQ.getCoords(input),
dragHome = thisQ.dragHome(choiceNo);
for (var i = 0; i < coords.length; i++) {
var drag = thisQ.dragItem(choiceNo, i);
if (!drag.length || drag.hasClass('beingdragged')) {
drag = thisQ.cloneNewDragItem(dragHome, i);
} else {
drag.removeClass('unneeded');
coords = thisQ.getCoords(input);
if (coords.length) {
var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');
drag.remove();
for (var i = 0; i < coords.length; i++) {
var dragInDrop = drag.clone();
dragInDrop.data('pagex', coords[i].x).data('pagey', coords[i].y);
thisQ.sendDragToDrop(dragInDrop, false);
}
drag.offset({'left': coords[i].x, 'top': coords[i].y});
thisQ.getDragClone(drag).addClass('active');
thisQ.cloneDragIfNeeded(drag);
}
});
root.find('div.dragitems .dragitem').each(function(key, itm) {
var item = $(itm);
if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
item.remove();
}
});
this.repositionDropZones();
var bgImage = this.bgImage(),
bgPosition = bgImage.offset();
bgImage.data('prev-top', bgPosition.top).data('prev-left', bgPosition.left);
};
/**
@ -197,11 +160,7 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
* @returns {Point[]} coordinates of however many copies of the drag item should be shown.
*/
DragDropMarkersQuestion.prototype.getCoords = function(inputNode) {
var root = this.getRoot(),
choiceNo = this.getChoiceNoFromElement(inputNode),
noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),
dragging = root.find('span.dragitem.beingdragged.choice' + choiceNo).length > 0,
coords = [],
var coords = [],
val = $(inputNode).val();
if (val !== '') {
var coordsStrings = val.split(';');
@ -209,10 +168,6 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
coords[i] = this.convertToWindowXY(Shapes.Point.parse(coordsStrings[i]));
}
}
var displayeddrags = coords.length + (dragging ? 1 : 0);
if ($(inputNode).hasClass('infinite') || (displayeddrags < noOfDrags)) {
coords[coords.length] = this.dragHomeXY(choiceNo);
}
return coords;
};
@ -252,19 +207,10 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
*/
DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {
var bgImage = this.bgImage();
return point.x > 0 && point.x <= bgImage.width() &&
point.y > 0 && point.y <= bgImage.height();
};
var bgPosition = bgImage.offset();
/**
* Returns coordinates for the home position of a choice.
*
* @param {Number} choiceNo
* @returns {Point} coordinates
*/
DragDropMarkersQuestion.prototype.dragHomeXY = function(choiceNo) {
var dragItemHome = this.dragHome(choiceNo);
return new Shapes.Point(dragItemHome.offset().left, dragItemHome.offset().top);
return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()
&& point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();
};
/**
@ -283,52 +229,28 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
return this.getRoot().find('img.dropbackground');
};
/**
* Return the DOM node for this choice's home position.
* @param {Number} choiceNo
* @returns {jQuery} containing the home.
*/
DragDropMarkersQuestion.prototype.dragHome = function(choiceNo) {
return this.getRoot().find('div.dragitems span.draghome.choice' + choiceNo);
};
/**
* Return the DOM node for a particular instance of a particular choice.
* @param {Number} choiceNo
* @param {Number} itemNo
* @returns {jQuery} containing the item.
*/
DragDropMarkersQuestion.prototype.dragItem = function(choiceNo, itemNo) {
return this.getRoot().find('div.dragitems span.dragitem.choice' + choiceNo + '.item' + itemNo);
};
/**
* Create a draggable copy of the drag item.
*
* @param {jQuery} dragHome to clone
* @param {Number} itemNo new item number
* @return {jQuery} drag
*/
DragDropMarkersQuestion.prototype.cloneNewDragItem = function(dragHome, itemNo) {
var drag = dragHome.clone(true);
drag.removeClass('draghome').addClass('dragitem').addClass('item' + itemNo);
dragHome.after(drag);
drag.attr('tabIndex', 0);
return drag;
};
DragDropMarkersQuestion.prototype.handleDragStart = function(e) {
var thisQ = this,
dragged = $(e.target).closest('.dragitem');
dragged = $(e.target).closest('.marker');
var info = dragDrop.prepare(e);
if (!info.start) {
return;
}
dragged.addClass('beingdragged');
dragged.addClass('beingdragged').css('transform', '');
var placed = !dragged.hasClass('unneeded');
if (!placed) {
var hiddenDrag = thisQ.getDragClone(dragged);
if (hiddenDrag.length) {
hiddenDrag.addClass('active');
dragged.offset(hiddenDrag.offset());
}
}
dragDrop.start(e, dragged, function() {
void (1); // Nothing to do, but we need a function.
void (1);
}, function(x, y, dragged) {
thisQ.dragEnd(dragged);
});
@ -339,52 +261,56 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
* @param {jQuery} dragged the marker that was dragged.
*/
DragDropMarkersQuestion.prototype.dragEnd = function(dragged) {
dragged.removeClass('beingdragged');
var choiceNo = this.getChoiceNoFromElement(dragged);
this.saveCoordsForChoice(choiceNo, dragged);
this.repositionDrags();
var placed = false,
choiceNo = this.getChoiceNoFromElement(dragged),
bgRatio = this.bgRatio(),
dragXY;
dragged.data('pagex', dragged.offset().left).data('pagey', dragged.offset().top);
dragXY = new Shapes.Point(dragged.data('pagex'), dragged.data('pagey'));
if (this.coordsInBgImg(dragXY)) {
this.sendDragToDrop(dragged, true);
placed = true;
// It seems that the dragdrop sometimes leaves the drag
// one pixel out of position. Put it in exactly the right place.
var bgImgXY = this.convertToBgImgXY(dragXY);
bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);
dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);
}
if (!placed) {
this.sendDragHome(dragged);
this.removeDragIfNeeded(dragged);
} else {
this.cloneDragIfNeeded(dragged);
}
this.saveCoordsForChoice(choiceNo);
};
/**
* Save the coordinates for a dropped item in the form field.
* @param {Number} choiceNo which copy of the choice this was.
* @param {jQuery} dropped the choice that was dropped here.
*/
DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo, dropped) {
DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) {
var coords = [],
numItems = this.getRoot().find('span.dragitem.choice' + choiceNo).length,
bgImgXY,
addme = true;
items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),
thiQ = this,
bgRatio = this.bgRatio();
// Re-build the coords array based on data in the ddform inputs.
// While long winded and unnecessary if there is only one drop item
// for a choice, it does account for moving any one of several drop items
// within a choice that have already been placed.
for (var i = 0; i <= numItems; i++) {
var drag = this.dragItem(choiceNo, i);
if (drag.length === 0) {
continue;
}
if (!drag.hasClass('beingdragged')) {
bgImgXY = this.convertToBgImgXY(new Shapes.Point(drag.offset().left, drag.offset().top));
if (this.coordsInBgImg(bgImgXY)) {
coords[coords.length] = bgImgXY;
if (items.length) {
items.each(function() {
var drag = $(this);
if (!drag.hasClass('beingdragged')) {
var dragXY = new Shapes.Point(drag.data('pagex'), drag.data('pagey'));
if (thiQ.coordsInBgImg(dragXY)) {
var bgImgXY = thiQ.convertToBgImgXY(dragXY);
bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);
coords[coords.length] = bgImgXY;
}
}
}
if (dropped && dropped.length !== 0 && (dropped[0].innerText === drag[0].innerText)) {
addme = false;
}
}
// If dropped has been passed it is because a new item has been dropped onto the background image
// so add its coordinates to the array.
if (addme) {
bgImgXY = this.convertToBgImgXY(new Shapes.Point(dropped.offset().left, dropped.offset().top));
if (this.coordsInBgImg(bgImgXY)) {
coords[coords.length] = bgImgXY;
}
});
}
this.getRoot().find('input.choice' + choiceNo).val(coords.join(';'));
@ -395,7 +321,7 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
* @param {KeyboardEvent} e
*/
DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {
var drag = $(e.target).closest('.dragitem'),
var drag = $(e.target).closest('.marker'),
point = new Shapes.Point(drag.offset().left, drag.offset().top),
choiceNo = this.getChoiceNoFromElement(drag);
@ -427,12 +353,28 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
if (point !== null) {
point = this.constrainToBgImg(point);
drag.offset({'left': point.x, 'top': point.y});
drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);
var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));
drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());
if (this.coordsInBgImg(new Shapes.Point(drag.offset().left, drag.offset().top))) {
if (drag.hasClass('unneeded')) {
this.sendDragToDrop(drag, true);
var hiddenDrag = this.getDragClone(drag);
if (hiddenDrag.length) {
hiddenDrag.addClass('active');
}
this.cloneDragIfNeeded(drag);
}
}
} else {
point = this.dragHomeXY(choiceNo);
drag.css('left', '').css('top', '');
drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);
this.sendDragHome(drag);
this.removeDragIfNeeded(drag);
}
drag.offset({'left': point.x, 'top': point.y});
this.saveCoordsForChoice(choiceNo, drag);
this.repositionDrags();
drag.focus();
this.saveCoordsForChoice(choiceNo);
};
/**
@ -488,27 +430,204 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
* Handle when the window is resized.
*/
DragDropMarkersQuestion.prototype.handleResize = function() {
this.repositionDrags();
var thisQ = this,
bgRatio = this.bgRatio();
if (this.isPrinting) {
bgRatio = 1;
}
this.getRoot().find('div.droparea .marker').not('.beingdragged').each(function(key, drag) {
$(drag)
.css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio))
.css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio));
thisQ.handleElementScale(drag, 'left top');
});
this.getRoot().find('div.droparea svg.dropzones')
.width(this.bgImage().width())
.height(this.bgImage().height());
for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {
var dropZone = thisQ.visibleDropZones[dropZoneNo];
var originCoords = dropZone.coords;
var shape = thisQ.shapes[dropZoneNo];
var shapeSVG = thisQ.shapeSVGs[dropZoneNo];
shape.parse(originCoords, bgRatio);
shape.updateSvg(shapeSVG);
var handles = shape.getHandlePositions();
var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);
markerSpan
.css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)
.css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));
thisQ.handleElementScale(markerSpan, 'center');
}
};
/**
* Check to see if the background image has moved. If so, refresh the layout.
* Clone the drag.
*/
DragDropMarkersQuestion.prototype.fixLayoutIfBackgroundMoved = function() {
var bgImage = this.bgImage(),
bgPosition = bgImage.offset(),
prevTop = bgImage.data('prev-top'),
prevLeft = bgImage.data('prev-left');
if (prevLeft === undefined || prevTop === undefined) {
// Question is not set up yet. Nothing to do.
return;
DragDropMarkersQuestion.prototype.cloneDrags = function() {
var thisQ = this;
this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {
var drag = $(draghome);
var placeHolder = drag.clone();
placeHolder.removeClass();
placeHolder.addClass('marker choice' +
thisQ.getChoiceNoFromElement(drag) + ' dragno' + thisQ.getDragNo(drag) + ' dragplaceholder');
drag.before(placeHolder);
});
};
/**
* Get the drag number of a drag.
*
* @param {jQuery} drag the drag.
* @returns {Number} the drag number.
*/
DragDropMarkersQuestion.prototype.getDragNo = function(drag) {
return this.getClassnameNumericSuffix(drag, 'dragno');
};
/**
* Get drag clone for a given drag.
*
* @param {jQuery} drag the drag.
* @returns {jQuery} the drag's clone.
*/
DragDropMarkersQuestion.prototype.getDragClone = function(drag) {
return this.getRoot().find('.draghomes' + ' span.marker' +
'.choice' + this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag) + '.dragplaceholder');
};
/**
* Get the drop area element.
* @returns {jQuery} droparea element.
*/
DragDropMarkersQuestion.prototype.dropArea = function() {
return this.getRoot().find('div.droparea');
};
/**
* Animate a drag back to its home.
*
* @param {jQuery} drag the item being moved.
*/
DragDropMarkersQuestion.prototype.sendDragHome = function(drag) {
drag.removeClass('beingdragged')
.addClass('unneeded')
.css('top', '')
.css('left', '')
.css('transform', '');
var placeHolder = this.getDragClone(drag);
placeHolder.after(drag);
placeHolder.removeClass('active');
};
/**
* Animate a drag item into a given place.
*
* @param {jQuery} drag the item to place.
* @param {boolean} isScaling Scaling or not
*/
DragDropMarkersQuestion.prototype.sendDragToDrop = function(drag, isScaling) {
var dropArea = this.dropArea(),
bgRatio = this.bgRatio();
drag.removeClass('beingdragged').removeClass('unneeded');
var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));
if (isScaling) {
drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);
drag.css('left', dragXY.x).css('top', dragXY.y);
} else {
drag.data('originX', dragXY.x).data('originY', dragXY.y);
drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);
}
if (prevTop === bgPosition.top && prevLeft === bgPosition.left) {
// Things have not moved.
return;
dropArea.append(drag);
this.handleElementScale(drag, 'left top');
};
/**
* Clone the drag at the draghome area if needed.
*
* @param {jQuery} drag the item to place.
*/
DragDropMarkersQuestion.prototype.cloneDragIfNeeded = function(drag) {
var inputNode = this.getInput(drag),
noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),
displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +
this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).length,
displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +
this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').length;
if (displayedDragsInDropArea < noOfDrags && displayedDragsInDragHomes === 0) {
var dragclone = drag.clone();
dragclone.addClass('unneeded')
.css('top', '')
.css('left', '')
.css('transform', '');
this.getDragClone(drag)
.removeClass('active')
.after(dragclone);
}
// We need to reposition things.
this.repositionDrags();
};
/**
* Remove the clone drag at the draghome area if needed.
*
* @param {jQuery} drag the item to place.
*/
DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {
var displayeddrags = this.getRoot().find('div.draghomes .marker.choice' +
this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').length;
if (displayeddrags > 1) {
this.getRoot().find('div.draghomes .marker.choice' +
this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').first().remove();
}
};
/**
* Get the input belong to drag.
*
* @param {jQuery} drag the item to place.
* @returns {jQuery} input element.
*/
DragDropMarkersQuestion.prototype.getInput = function(drag) {
var choiceNo = this.getChoiceNoFromElement(drag);
return this.getRoot().find('input.choices.choice' + choiceNo);
};
/**
* Return the background ratio.
*
* @returns {number} Background ratio.
*/
DragDropMarkersQuestion.prototype.bgRatio = function() {
var bgImg = this.bgImage();
var bgImgNaturalWidth = bgImg.get(0).naturalWidth;
var bgImgClientWidth = bgImg.width();
return bgImgClientWidth / bgImgNaturalWidth;
};
/**
* Scale the drag if needed.
*
* @param {jQuery} element the item to place.
* @param {String} type scaling type
*/
DragDropMarkersQuestion.prototype.handleElementScale = function(element, type) {
var bgRatio = parseFloat(this.bgRatio());
if (this.isPrinting) {
bgRatio = 1;
}
$(element).css({
'-webkit-transform': 'scale(' + bgRatio + ')',
'-moz-transform': 'scale(' + bgRatio + ')',
'-ms-transform': 'scale(' + bgRatio + ')',
'-o-transform': 'scale(' + bgRatio + ')',
'transform': 'scale(' + bgRatio + ')',
'transform-origin': type
});
};
/**
@ -524,6 +643,16 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
*/
eventHandlersInitialised: false,
/**
* {boolean} is printing or not.
*/
isPrinting: false,
/**
* {boolean} is keyboard navigation.
*/
isKeyboardNavigation: false,
/**
* {Object} all the questions on this page, indexed by containerId (id on the .que div).
*/
@ -533,13 +662,12 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
* Initialise one question.
*
* @param {String} containerId the id of the div.que that contains this question.
* @param {String} bgImgUrl URL fo the background image.
* @param {boolean} readOnly whether the question is read-only.
* @param {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.
*/
init: function(containerId, bgImgUrl, readOnly, visibleDropZones) {
init: function(containerId, readOnly, visibleDropZones) {
questionManager.questions[containerId] =
new DragDropMarkersQuestion(containerId, bgImgUrl, readOnly, visibleDropZones);
new DragDropMarkersQuestion(containerId, readOnly, visibleDropZones);
if (!questionManager.eventHandlersInitialised) {
questionManager.setupEventHandlers();
questionManager.eventHandlersInitialised = true;
@ -550,14 +678,45 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
* Set up the event handlers that make this question type work. (Done once per page.)
*/
setupEventHandlers: function() {
$('body').on('mousedown touchstart',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.dragitems .dragitem',
questionManager.handleDragStart)
$('body')
.on('mousedown touchstart',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', questionManager.handleDragStart)
.on('mousedown touchstart',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', questionManager.handleDragStart)
.on('keydown keypress',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.dragitems .dragitem',
questionManager.handleKeyPress);
$(window).on('resize', questionManager.handleWindowResize);
setTimeout(questionManager.fixLayoutIfThingsMoved, 100);
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', questionManager.handleKeyPress)
.on('keydown keypress',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', questionManager.handleKeyPress)
.on('focusin',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', function(e) {
questionManager.handleKeyboardFocus(e, true);
})
.on('focusin',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', function(e) {
questionManager.handleKeyboardFocus(e, true);
})
.on('focusout',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', function(e) {
questionManager.handleKeyboardFocus(e, false);
})
.on('focusout',
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', function(e) {
questionManager.handleKeyboardFocus(e, false);
});
$(window).on('resize', function() {
questionManager.handleWindowResize(false);
});
window.addEventListener('beforeprint', function() {
questionManager.isPrinting = true;
questionManager.handleWindowResize(questionManager.isPrinting);
});
window.addEventListener('afterprint', function() {
questionManager.isPrinting = false;
questionManager.handleWindowResize(questionManager.isPrinting);
});
setTimeout(function() {
questionManager.fixLayoutIfThingsMoved();
}, 100);
},
/**
@ -585,31 +744,41 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
/**
* Handle when the window is resized.
* @param {boolean} isPrinting
*/
handleWindowResize: function() {
handleWindowResize: function(isPrinting) {
for (var containerId in questionManager.questions) {
if (questionManager.questions.hasOwnProperty(containerId)) {
questionManager.questions[containerId].isPrinting = isPrinting;
questionManager.questions[containerId].handleResize();
}
}
},
/**
* Handle focus lost events on markers.
* @param {Event} e
* @param {boolean} isNavigating
*/
handleKeyboardFocus: function(e, isNavigating) {
questionManager.isKeyboardNavigation = isNavigating;
},
/**
* Sometimes, despite our best efforts, things change in a way that cannot
* be specifically caught (e.g. dock expanding or collapsing in Boost).
* Therefore, we need to periodically check everything is in the right position.
*/
fixLayoutIfThingsMoved: function() {
for (var containerId in questionManager.questions) {
if (questionManager.questions.hasOwnProperty(containerId)) {
questionManager.questions[containerId].fixLayoutIfBackgroundMoved();
}
if (!questionManager.isKeyboardNavigation) {
this.handleWindowResize(questionManager.isPrinting);
}
// We use setTimeout after finishing work, rather than setInterval,
// in case positioning things is slow. We want 100 ms gap
// between executions, not what setInterval does.
setTimeout(questionManager.fixLayoutIfThingsMoved, 100);
setTimeout(function() {
questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);
}, 100);
},
/**

@ -126,10 +126,11 @@ define(function() {
* Update the shape from the string representation.
*
* @param {String} coordinates in the form returned by getCoordinates.
* @param {number} ratio Ratio to scale.
* @return {boolean} true if the string could be parsed and the shape updated, else false.
*/
Shape.prototype.parse = function(coordinates) {
void (coordinates);
Shape.prototype.parse = function(coordinates, ratio) {
void (coordinates, ratio);
throw new Error('Not implemented.');
};
@ -264,14 +265,16 @@ define(function() {
svgEl.childNodes[1].textContent = this.label;
};
Circle.prototype.parse = function(coordinates) {
if (!coordinates.match(/^\d+,\d+;\d+$/)) {
Circle.prototype.parse = function(coordinates, ratio) {
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?$/)) {
return false;
}
var bits = coordinates.split(';');
this.centre = Point.parse(bits[0]);
this.radius = Math.round(bits[1]);
this.centre.x = this.centre.x * parseFloat(ratio);
this.centre.y = this.centre.y * parseFloat(ratio);
this.radius = Math.round(bits[1]) * parseFloat(ratio);
return true;
};
@ -384,16 +387,18 @@ define(function() {
svgEl.childNodes[1].textContent = this.label;
};
Rectangle.prototype.parse = function(coordinates) {
if (!coordinates.match(/^\d+,\d+;\d+,\d+$/)) {
Rectangle.prototype.parse = function(coordinates, ratio) {
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?,\d+(\.\d+)?$/)) {
return false;
}
var bits = coordinates.split(';');
this.centre = Point.parse(bits[0]);
this.centre.x = this.centre.x * parseFloat(ratio);
this.centre.y = this.centre.y * parseFloat(ratio);
var size = Point.parse(bits[1]);
this.width = size.x;
this.height = size.y;
this.width = size.x * parseFloat(ratio);
this.height = size.y * parseFloat(ratio);
return true;
};
@ -479,6 +484,7 @@ define(function() {
Shape.call(this, label, 0, 0);
this.points = points ? points.slice() : [new Point(10, 10), new Point(40, 10), new Point(10, 40)];
this.normalizeShape();
this.ratio = 1;
}
Polygon.prototype = new Shape();
@ -502,13 +508,14 @@ define(function() {
Polygon.prototype.updateSvg = function(svgEl) {
svgEl.childNodes[0].setAttribute('points', this.getCoordinates().replace(/[,;]/g, ' '));
svgEl.childNodes[0].setAttribute('transform', 'scale(' + parseFloat(this.ratio) + ')');
svgEl.childNodes[1].setAttribute('x', this.centre.x);
svgEl.childNodes[1].setAttribute('y', this.centre.y + 15);
svgEl.childNodes[1].textContent = this.label;
};
Polygon.prototype.parse = function(coordinates) {
if (!coordinates.match(/^\d+,\d+(?:;\d+,\d+)*$/)) {
Polygon.prototype.parse = function(coordinates, ratio) {
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?(?:;\d+(\.\d+)?,\d+(\.\d+)?)*$/)) {
return false;
}
@ -521,6 +528,7 @@ define(function() {
this.points = points;
this.centre.x = 0;
this.centre.y = 0;
this.ratio = ratio;
this.normalizeShape();
return true;
@ -636,6 +644,9 @@ define(function() {
editHandles.push(this.points[i].offset(this.centre.x, this.centre.y));
}
this.centre.x = this.centre.x * parseFloat(this.ratio);
this.centre.y = this.centre.y * parseFloat(this.ratio);
return {
moveHandle: this.centre,
editHandles: editHandles

@ -42,88 +42,86 @@ class qtype_ddmarker_renderer extends qtype_ddtoimage_renderer_base {
$question = $qa->get_question();
$response = $qa->get_last_qt_data();
$componentname = $question->qtype->plugin_name();
$questiontext = $question->format_questiontext($qa);
$output = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
$dropareaclass = 'droparea';
$draghomesclass = 'draghomes';
if ($options->readonly) {
$dropareaclass .= ' readonly';
$draghomesclass .= ' readonly';
}
$bgimage = self::get_url_for_image($qa, 'bgimage');
$output = html_writer::div($questiontext, 'qtext');
$img = html_writer::empty_tag('img', array(
'class' => 'dropbackground',
'alt' => get_string('dropbackground', 'qtype_ddmarker')));
$output .= html_writer::start_div('ddarea');
$output .= html_writer::start_div($dropareaclass);
$output .= html_writer::img(self::get_url_for_image($qa, 'bgimage'), get_string('dropbackground', 'qtype_ddmarker'),
['class' => 'dropbackground img-responsive img-fluid']);
$droparea = html_writer::tag('div', $img, array('class' => 'droparea'));
$output .= html_writer::div('', 'dropzones');
$output .= html_writer::div('', 'markertexts');
$output .= html_writer::end_div();
$output .= html_writer::start_div($draghomesclass);
$draghomes = '';
$orderedgroup = $question->get_ordered_choices(1);
$componentname = $question->qtype->plugin_name();
$hiddenfields = '';
foreach ($orderedgroup as $choiceno => $drag) {
$classes = array('draghome',
"choice{$choiceno}");
$classes = ['marker', 'choice' . $choiceno];
$attr = [];
if ($drag->infinite) {
$classes[] = 'infinite';
} else {
$classes[] = 'dragno'.$drag->noofdrags;
$classes[] = 'dragno' . $drag->noofdrags;
}
$targeticonhtml =
$this->output->image_icon('crosshairs', '', $componentname, array('class' => 'target'));
$markertextattrs = array('class' => 'markertext');
$markertext = html_writer::tag('span', $drag->text, $markertextattrs);
$draghomesattrs = array('class' => join(' ', $classes));
$draghomes .= html_writer::tag('span', $targeticonhtml . $markertext, $draghomesattrs);
if (!$options->readonly) {
$attr['tabindex'] = 0;
}
$dragoutput = html_writer::start_span(join(' ', $classes), $attr);
$targeticonhtml = $this->output->image_icon('crosshairs', '', $componentname, ['class' => 'target']);
$markertext = html_writer::span($drag->text, 'markertext');
$dragoutput .= $targeticonhtml . $markertext;
$dragoutput .= html_writer::end_span();
$output .= $dragoutput;
$hiddenfields .= $this->hidden_field_choice($qa, $choiceno, $drag->infinite, $drag->noofdrags);
}
$dragitemsclass = 'dragitems';
if ($options->readonly) {
$dragitemsclass .= ' readonly';
}
$dragitems = html_writer::tag('div', $draghomes, array('class' => $dragitemsclass));
$dropzones = html_writer::tag('div', '', array('class' => 'dropzones'));
$texts = html_writer::tag('div', '', array('class' => 'markertexts'));
$output .= html_writer::tag('div',
$droparea.$dragitems.$dropzones . $texts,
array('class' => 'ddarea'));
$output .= html_writer::end_div();
$output .= html_writer::end_div();
if ($question->showmisplaced && $qa->get_state()->is_finished()) {
$visibledropzones = $question->get_drop_zones_without_hit($response);
} else {
$visibledropzones = array();
$visibledropzones = [];
}
$this->page->requires->js_call_amd('qtype_ddmarker/question', 'init',
[$qa->get_outer_question_div_unique_id(), $bgimage, $options->readonly, $visibledropzones]);
if ($qa->get_state() == question_state::$invalid) {
$output .= html_writer::nonempty_tag('div',
$question->get_validation_error($qa->get_last_qt_data()),
array('class' => 'validationerror'));
$output .= html_writer::div($question->get_validation_error($qa->get_last_qt_data()), 'validationerror');
}
if ($question->showmisplaced && $qa->get_state()->is_finished()) {
$wrongparts = $question->get_drop_zones_without_hit($response);
if (count($wrongparts) !== 0) {
$wrongpartsstringspans = array();
$wrongpartsstringspans = [];
foreach ($wrongparts as $wrongpart) {
$wrongpartsstringspans[] = html_writer::nonempty_tag('span',
$wrongpart->markertext, array('class' => 'wrongpart'));
$wrongpartsstringspans[] = html_writer::span($wrongpart->markertext, 'wrongpart');
}
$wrongpartsstring = join(', ', $wrongpartsstringspans);
$output .= html_writer::nonempty_tag('span',
get_string('followingarewrongandhighlighted',
'qtype_ddmarker',
$wrongpartsstring),
array('class' => 'wrongparts'));
$output .= html_writer::span(get_string('followingarewrongandhighlighted', 'qtype_ddmarker', $wrongpartsstring),
'wrongparts');
}
}
$output .= html_writer::tag('div', $hiddenfields, array('class' => 'ddform'));
$output .= html_writer::div($hiddenfields, 'ddform');
$this->page->requires->js_call_amd('qtype_ddmarker/question', 'init',
[$qa->get_outer_question_div_unique_id(), $options->readonly, $visibledropzones]);
return $output;
}
protected function hidden_field_choice(question_attempt $qa, $choiceno, $infinite, $noofdrags, $value = null) {
$varname = 'c'.$choiceno;
$classes = array('choices', 'choice'.$choiceno, 'noofdrags'.$noofdrags);

@ -3,29 +3,46 @@
display: block;
}
.que.ddmarker .draghome img,
.que.ddmarker .draghome span {
visibility: hidden;
.que.ddmarker .droparea {
display: inline-block;
position: relative;
}
.que.ddmarker .dragitems .dragitem {
cursor: move;
.que.ddmarker .droparea .dropzones,
.que.ddmarker .droparea .markertexts {
position: absolute;
z-index: 2;
top: 0;
left: 0;
}
.que.ddmarker .dragitems .draghome {
.que.ddmarker .draghomes .marker,
.que.ddmarker .droparea .marker {
vertical-align: top;
cursor: move;
}
.que.ddmarker .draghomes.readonly .marker,
.que.ddmarker .droparea.readonly .marker {
cursor: auto;
}
.que.ddmarker .droparea .marker {
position: absolute;
}
.que.ddmarker .draghomes .marker {
position: relative;
display: inline-block;
margin: 10px;
vertical-align: top;
}
.que.ddmarker .dragitems {
margin-top: 10px;
.que.ddmarker .draghomes .marker.dragplaceholder {
display: none;
}
.que.ddmarker .dragitems.readonly .dragitem {
cursor: auto;
.que.ddmarker .draghomes .marker.dragplaceholder.active {
visibility: hidden;
display: inline-block;
}
.que.ddmarker div.ddarea,
@ -41,9 +58,16 @@ form.mform fieldset#id_previewareaheader div.ddarea .markertexts {
form.mform fieldset#id_previewareaheader .dropbackground {
margin: 0 auto;
border: 1px solid black;
}
form.mform fieldset#id_previewareaheader .dropbackground {
max-width: none;
}
.que.ddmarker .dropbackground.img-responsive.img-fluid {
width: 100%;
}
.que.ddmarker div.dragitems div.draghome,
.que.ddmarker div.dragitems div.dragitem,
form.mform fieldset#id_previewareaheader div.draghome,
@ -51,7 +75,8 @@ form.mform fieldset#id_previewareaheader div.drag {
font: 13px/1.231 arial, helvetica, clean, sans-serif;
}
.que.ddmarker div.dragitems span.markertext,
.que.ddmarker .droparea .marker span.markertext,
.que.ddmarker .draghomes .marker span.markertext,
.que.ddmarker div.markertexts span.markertext,
form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
margin: 0 5px;
@ -66,11 +91,17 @@ form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
opacity: 0.6;
}
.que.ddmarker .droparea .marker span.markertext,
.que.ddmarker .draghomes .marker span.markertext {
white-space: nowrap;
}
.que.ddmarker div.markertexts span.markertext {
z-index: 2;
background-color: yellow;
border: 2px solid khaki;
position: absolute;
white-space: nowrap;
}
.que.ddmarker span.wrongpart {
@ -84,7 +115,8 @@ form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
display: inline-block;
}
.que.ddmarker div.dragitems img.target {
.que.ddmarker .droparea .marker img.target,
.que.ddmarker .draghomes .marker img.target {
position: absolute;
left: -7px; /* This must be half the size of the target image, minus 0.5. */
top: -7px; /* In other words, this works for a 15x15 cross-hair. */
@ -94,7 +126,11 @@ form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
display: none;
}
.que.ddmarker .dragitem.beingdragged span.markertext {
.que.ddmarker .marker.beingdragged {
position: absolute;
}
.que.ddmarker .marker.beingdragged span.markertext {
z-index: 3;
box-shadow: 3px 3px 4px #000;
}
@ -196,3 +232,7 @@ body#page-question-type-ddmarker div[id^=fitem_id_][id*=hintclearwrong_] {
body#page-question-type-ddmarker #fitem_id_penalty {
margin-bottom: 2em;
}
body#page-question-type-ddmarker .ddarea.que.ddmarker {
overflow-y: scroll;
}

@ -37,22 +37,20 @@ class behat_qtype_ddmarker extends behat_base {
/**
* Get the xpath for a given drag item.
* @param string $dragitem the text of the item to drag.
*
* @param string $marker the text of the item to drag.
* @param bool $iskeyboard is using keyboard or not.
* @return string the xpath expression.
*/
protected function marker_xpath($marker, $item = 0) {
return '//span[contains(@class, " dragitem ") and contains(@class, " item' . $item .
'") and span[@class = "markertext" and contains(normalize-space(.), "' .
$this->escape($marker) . '")]]';
}
protected function parse_marker_name($marker) {
$item = 0;
if (preg_match('~,(\d+)$~', $marker, $matches)) {
$item = $matches[1];
$marker = substr($marker, 0, -1 - strlen($item));
protected function marker_xpath($marker, $iskeyboard = false) {
if ($iskeyboard) {
return '//span[contains(@class, "marker") and not(contains(@class, "dragplaceholder")) ' .
'and span[@class = "markertext" and contains(normalize-space(.), "' .
$this->escape($marker) . '")]]';
}
return array($marker, $item);
return '//span[contains(@class, "marker") and contains(@class, "unneeded") ' .
'and not(contains(@class, "dragplaceholder")) and span[@class = "markertext" and contains(normalize-space(.), "' .
$this->escape($marker) . '")]]';
}
/**
@ -64,7 +62,6 @@ class behat_qtype_ddmarker extends behat_base {
* @Given /^I drag "(?P<marker>[^"]*)" to "(?P<coordinates>\d+,\d+)" in the drag and drop markers question$/
*/
public function i_drag_to_in_the_drag_and_drop_markers_question($marker, $coordinates) {
list($marker, $item) = $this->parse_marker_name($marker);
list($x, $y) = explode(',', $coordinates);
// This is a bit nasty, but Behat (indeed Selenium) will only drag on
@ -81,7 +78,6 @@ class behat_qtype_ddmarker extends behat_base {
var target = document.createElement('div');
target.setAttribute('id', 'target-{$x}-{$y}');
var container = document.querySelector('.droparea');
container.style.setProperty('position', 'relative');
container.insertBefore(target, image);
var xadjusted = {$x} + (container.offsetWidth - image.offsetWidth) / 2;
var yadjusted = {$y} + (container.offsetHeight - image.offsetHeight) / 2;
@ -93,7 +89,7 @@ class behat_qtype_ddmarker extends behat_base {
}())");
$generalcontext = behat_context_helper::get('behat_general');
$generalcontext->i_drag_and_i_drop_it_in($this->marker_xpath($marker, $item),
$generalcontext->i_drag_and_i_drop_it_in($this->marker_xpath($marker),
'xpath_element', "#target-{$x}-{$y}", 'css_element');
}
@ -113,8 +109,7 @@ class behat_qtype_ddmarker extends behat_base {
'left' => chr(37),
'right' => chr(39),
);
list($marker, $item) = $this->parse_marker_name($marker);
$node = $this->get_selected_node('xpath_element', $this->marker_xpath($marker, $item));
$node = $this->get_selected_node('xpath_element', $this->marker_xpath($marker, true));
$this->ensure_node_is_visible($node);
for ($i = 0; $i < $repeats; $i++) {
$node->keyDown($keycodes[$direction]);

@ -33,10 +33,10 @@ Feature: Preview a drag-drop marker question
And I change window size to "large"
And I wait "2" seconds
# Odd, but the <br>s go to nothing, not a space.
And I drag "OU" to "342,230" in the drag and drop markers question
And I drag "Railway station" to "254,197" in the drag and drop markers question
And I drag "Railway station,1" to "326,319" in the drag and drop markers question
And I drag "Railway station,2" to "203,101" in the drag and drop markers question
And I drag "OU" to "345,230" in the drag and drop markers question
And I drag "Railway station" to "262,197" in the drag and drop markers question
And I drag "Railway station" to "334,319" in the drag and drop markers question
And I drag "Railway station" to "211,101" in the drag and drop markers question
And I press "Submit and finish"
Then the state of "Please place the markers on the map of Milton Keynes" question is shown as "Correct"
And I should see "Mark 1.00 out of 1.00"

@ -46,7 +46,7 @@ class qtype_ddmarker_walkthrough_test extends qbehaviour_walkthrough_test_base {
* @return question_contains_tag_with_attributes the expectation.
*/
protected function get_contains_draggable_marker_home_expectation($choice, $infinite) {
$class = 'draghome choice'.$choice;
$class = 'marker choice'.$choice;
if ($infinite) {
$class .= ' infinite';
}