MDL-68113 Drag and drop into text questions: better responsive design

These changes also make it possible to print these questions.
This commit is contained in:
Huong Nguyen 2020-02-25 14:08:03 +07:00
parent 1d4fdb0d1c
commit f389e916f2
6 changed files with 244 additions and 142 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -120,40 +120,17 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
*/
DragDropToTextQuestion.prototype.cloneDrags = function() {
var thisQ = this;
this.getRoot().find('span.draghome').each(function(index, draghome) {
thisQ.cloneDragsForOneChoice($(draghome));
thisQ.getRoot().find('span.draghome').each(function(index, draghome) {
var drag = $(draghome);
var placeHolder = drag.clone();
placeHolder.removeClass();
placeHolder.addClass('draghome choice' +
thisQ.getChoice(drag) + ' group' +
thisQ.getGroup(drag) + ' dragplaceholder');
drag.before(placeHolder);
});
};
/**
* Clone drag item for one choice.
*
* @param {jQuery} dragHome the drag home to clone.
*/
DragDropToTextQuestion.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
*/
DragDropToTextQuestion.prototype.cloneDrag = function(dragHome) {
var drag = dragHome.clone();
drag.removeClass('draghome')
.addClass('drag unplaced moodle-has-zindex')
.offset(dragHome.offset());
this.getRoot().find('div.drags').append(drag);
};
/**
* Update the position of drags.
*/
@ -162,12 +139,12 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
root = this.getRoot();
// First move all items back home.
root.find('span.drag').each(function(i, dragNode) {
root.find('span.draghome').not('.dragplaceholder').each(function(i, dragNode) {
var drag = $(dragNode),
currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');
drag.addClass('unplaced')
.removeClass('placed')
.offset(thisQ.getDragHome(thisQ.getGroup(drag), thisQ.getChoice(drag)).offset());
.removeClass('placed');
drag.removeAttr('tabindex');
if (currentPlace !== null) {
drag.removeClass('inplace' + currentPlace);
}
@ -189,42 +166,16 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
return;
}
thisQ.getUnplacedChoice(thisQ.getGroup(input), choice)
.removeClass('unplaced')
.addClass('placed inplace' + place)
.offset(root.find('.drop.place' + place).offset());
});
};
/**
* Check to see if a drop target has moved. If so, refresh the layout.
*/
DragDropToTextQuestion.prototype.fixLayoutIfDropsMoved = function() {
var thisQ = this,
root = this.getRoot(),
didMove = false;
root.find('input.placeinput').each(function(i, inputNode) {
var place = thisQ.getPlace($(inputNode)),
drop = root.find('.drop.place' + place),
dropPosition = drop.offset(),
prevTop = drop.data('prev-top'),
prevLeft = drop.data('prev-left');
if (prevLeft === undefined || prevTop === undefined) {
// Question is not set up yet. Nothing to do.
return;
// Get the unplaced drag.
var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);
// Get the clone of the drag.
var hiddenDrag = thisQ.getDragClone(unplacedDrag);
if (hiddenDrag.length) {
hiddenDrag.addClass('active');
}
if (prevTop === dropPosition.top && prevLeft === dropPosition.left) {
// Things have not moved.
return;
}
didMove = true;
// Send the drag to drop.
thisQ.sendDragToDrop(thisQ.getUnplacedChoice(thisQ.getGroup(input), choice), drop);
});
if (didMove) {
// We need to reposition things.
this.positionDrags();
}
};
/**
@ -234,20 +185,45 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
*/
DragDropToTextQuestion.prototype.handleDragStart = function(e) {
var thisQ = this,
drag = $(e.target).closest('.drag');
drag = $(e.target).closest('.draghome');
var info = dragDrop.prepare(e);
if (!info.start) {
return;
}
drag.addClass('beingdragged');
var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
if (currentPlace !== null) {
this.setInputValue(currentPlace, 0);
drag.removeClass('inplace' + currentPlace);
var hiddenDrop = thisQ.getDrop(drag, currentPlace);
if (hiddenDrop.length) {
hiddenDrop.addClass('active');
drag.offset(hiddenDrop.offset());
}
} else {
var hiddenDrag = thisQ.getDragClone(drag);
if (hiddenDrag.length) {
if (drag.hasClass('infinite')) {
var noOfDrags = this.noOfDropsInGroup(this.getGroup(drag));
var cloneDrags = this.getInfiniteDragClones(drag, false);
if (cloneDrags.length < noOfDrags) {
var cloneDrag = drag.clone();
cloneDrag.removeClass('beingdragged');
hiddenDrag.after(cloneDrag);
drag.offset(cloneDrag.offset());
} else {
hiddenDrag.addClass('active');
drag.offset(hiddenDrag.offset());
}
} else {
hiddenDrag.addClass('active');
drag.offset(hiddenDrag.offset());
}
}
}
drag.addClass('beingdragged');
dragDrop.start(e, drag, function(x, y, drag) {
thisQ.dragMove(x, y, drag);
}, function(x, y, drag) {
@ -272,6 +248,14 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
drop.removeClass('valid-drag-over-drop');
}
});
this.getRoot().find('span.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {
var drop = $(dropNode);
if (thisQ.isPointInDrop(pageX, pageY, drop)) {
drop.addClass('valid-drag-over-drop');
} else {
drop.removeClass('valid-drag-over-drop');
}
});
};
/**
@ -299,6 +283,22 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
return false; // Stop the each() here.
});
root.find('span.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) {
var placedDrag = $(placedNode);
if (!thisQ.isPointInDrop(pageX, pageY, placedDrag)) {
// Not this placed drag.
return true;
}
// Now put this drag into the drop.
placedDrag.removeClass('valid-drag-over-drop');
var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace');
var drop = thisQ.getDrop(drag, currentPlace);
thisQ.sendDragToDrop(drag, drop);
placed = true;
return false; // Stop the each() here.
});
if (!placed) {
this.sendDragHome(drag);
}
@ -314,15 +314,24 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
// Is there already a drag in this drop? if so, evict it.
var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));
if (oldDrag.length !== 0) {
oldDrag.addClass('beingdragged');
oldDrag.offset(oldDrag.offset());
var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');
var hiddenDrop = this.getDrop(oldDrag, currentPlace);
hiddenDrop.addClass('active');
this.sendDragHome(oldDrag);
}
if (drag.length === 0) {
this.setInputValue(this.getPlace(drop), 0);
if (drop.data('isfocus')) {
drop.focus();
}
} else {
this.setInputValue(this.getPlace(drop), this.getChoice(drag));
drag.removeClass('unplaced')
.addClass('placed inplace' + this.getPlace(drop));
drag.attr('tabindex', 0);
this.animateTo(drag, drop);
}
};
@ -333,11 +342,11 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
* @param {jQuery} drag the item being moved.
*/
DragDropToTextQuestion.prototype.sendDragHome = function(drag) {
drag.removeClass('placed').addClass('unplaced');
var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');
if (currentPlace !== null) {
drag.removeClass('inplace' + currentPlace);
}
drag.data('unplaced', true);
this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));
};
@ -351,8 +360,15 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
* @param {KeyboardEvent} e
*/
DragDropToTextQuestion.prototype.handleKeyPress = function(e) {
var drop = $(e.target).closest('.drop'),
currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),
var drop = $(e.target).closest('.drop');
if (drop.length === 0) {
var placedDrag = $(e.target);
var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');
if (currentPlace !== null) {
drop = this.getDrop(placedDrag, currentPlace);
}
}
var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),
nextDrag = $();
switch (e.keyCode) {
@ -374,6 +390,12 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
return; // To avoid the preventDefault below.
}
if (nextDrag.length) {
nextDrag.data('isfocus', true);
} else {
drop.data('isfocus', true);
}
e.preventDefault();
this.sendDragToDrop(nextDrag, drop);
};
@ -438,9 +460,10 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
*/
DragDropToTextQuestion.prototype.animateTo = function(drag, target) {
var currentPos = drag.offset(),
targetPos = target.offset();
drag.addClass('beingdragged');
targetPos = target.offset(),
thisQ = this;
M.util.js_pending('qtype_ddwtos-animate-' + thisQ.containerId);
// Animate works in terms of CSS position, whereas locating an object
// on the page works best with jQuery offset() function. So, to get
// the right target position, we work out the required change in
@ -453,10 +476,8 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
{
duration: 'fast',
done: function() {
drag.removeClass('beingdragged');
// It seems that the animation sometimes leaves the drag
// one pixel out of position. Put it in exactly the right place.
drag.offset(targetPos);
$('body').trigger('dragmoved', [drag, target, thisQ]);
M.util.js_complete('qtype_ddwtos-animate-' + thisQ.containerId);
}
}
);
@ -503,7 +524,13 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
* @returns {jQuery} containing that div.
*/
DragDropToTextQuestion.prototype.getDragHome = function(group, choice) {
return this.getRoot().find('.draghome.group' + group + '.choice' + choice);
if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {
return this.getRoot().find('.draggrouphomes' + group +
' span.draghome.infinite' +
'.choice' + choice +
'.group' + group);
}
return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);
};
/**
@ -514,7 +541,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
* @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.
*/
DragDropToTextQuestion.prototype.getUnplacedChoice = function(group, choice) {
return this.getRoot().find('.drag.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);
return this.getRoot().find('.draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);
};
/**
@ -524,7 +551,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
* @return {jQuery} the current drag (or an empty jQuery if none).
*/
DragDropToTextQuestion.prototype.getCurrentDragInPlace = function(place) {
return this.getRoot().find('span.drag.inplace' + place);
return this.getRoot().find('span.draghome.inplace' + place);
};
/**
@ -601,6 +628,54 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
return this.getClassnameNumericSuffix(node, 'place');
};
/**
* Get drag clone for a given drag.
*
* @param {jQuery} drag the drag.
* @returns {jQuery} the drag's clone.
*/
DragDropToTextQuestion.prototype.getDragClone = function(drag) {
return this.getRoot().find('.draggrouphomes' +
this.getGroup(drag) +
' span.draghome' +
'.choice' + this.getChoice(drag) +
'.group' + this.getGroup(drag) +
'.dragplaceholder');
};
/**
* Get infinite drag clones for given drag.
*
* @param {jQuery} drag the drag.
* @param {Boolean} inHome in the home area or not.
* @returns {jQuery} the drag's clones.
*/
DragDropToTextQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {
if (inHome) {
return this.getRoot().find('.draggrouphomes' +
this.getGroup(drag) +
' span.draghome' +
'.choice' + this.getChoice(drag) +
'.group' + this.getGroup(drag) +
'.infinite').not('.dragplaceholder');
}
return this.getRoot().find('span.draghome' +
'.choice' + this.getChoice(drag) +
'.group' + this.getGroup(drag) +
'.infinite').not('.dragplaceholder');
};
/**
* Get drop for a given drag and place.
*
* @param {jQuery} drag the drag.
* @param {Integer} currentPlace the current place of drag.
* @returns {jQuery} the drop's clone.
*/
DragDropToTextQuestion.prototype.getDrop = function(drag, currentPlace) {
return this.getRoot().find('.drop.group' + this.getGroup(drag) + '.place' + currentPlace);
};
/**
* Singleton that tracks all the DragDropToTextQuestions on this page, and deals
* with event dispatching.
@ -637,14 +712,15 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
*/
setupEventHandlers: function() {
$('body').on('mousedown touchstart',
'.que.ddwtos:not(.qtype_ddwtos-readonly) span.drag',
questionManager.handleDragStart)
'.que.ddwtos:not(.qtype_ddwtos-readonly) span.draghome',
questionManager.handleDragStart)
.on('keydown',
'.que.ddwtos:not(.qtype_ddwtos-readonly) span.drop',
questionManager.handleKeyPress);
$(window).on('resize', questionManager.handleWindowResize);
setTimeout(questionManager.fixLayoutIfThingsMoved, 100);
questionManager.handleKeyPress)
.on('keydown',
'.que.ddwtos:not(.qtype_ddwtos-readonly) span.draghome.placed:not(.beingdragged)',
questionManager.handleKeyPress)
.on('dragmoved', questionManager.handleDragMoved);
},
/**
@ -670,35 +746,6 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
}
},
/**
* Handle when the window is resized.
*/
handleWindowResize: function() {
for (var containerId in questionManager.questions) {
if (questionManager.questions.hasOwnProperty(containerId)) {
questionManager.questions[containerId].positionDrags();
}
}
},
/**
* 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].fixLayoutIfDropsMoved();
}
}
// 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);
},
/**
* Given an event, work out which question it affects.
*
@ -708,6 +755,55 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
getQuestionForEvent: function(e) {
var containerId = $(e.currentTarget).closest('.que.ddwtos').attr('id');
return questionManager.questions[containerId];
},
/**
* Handle when drag moved.
*
* @param {Event} e the event.
* @param {jQuery} drag the drag
* @param {jQuery} target the target
* @param {DragDropToTextQuestion} thisQ the question.
*/
handleDragMoved: function(e, drag, target, thisQ) {
drag.removeClass('beingdragged');
drag.css('top', '').css('left', '');
target.after(drag);
target.removeClass('active');
if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {
drag.removeClass('placed').addClass('unplaced');
drag.removeAttr('tabindex');
drag.removeData('unplaced');
if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {
setTimeout(function() {
thisQ.getInfiniteDragClones(drag, true).first().remove();
});
}
}
if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {
var hiddenDrag = thisQ.getDragClone(drag);
if (hiddenDrag.length) {
if (drag.hasClass('infinite')) {
var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(drag));
var cloneDrags = thisQ.getInfiniteDragClones(drag, false);
if (cloneDrags.length < noOfDrags) {
var cloneDrag = drag.clone();
cloneDrag.removeClass('beingdragged');
cloneDrag.removeAttr('tabindex');
hiddenDrag.after(cloneDrag);
} else {
hiddenDrag.addClass('active');
}
} else {
hiddenDrag.addClass('active');
}
}
drag.focus();
drag.removeData('isfocus');
}
if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {
target.removeData('isfocus');
}
}
};

View File

@ -64,12 +64,6 @@ class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_ren
}
$result .= html_writer::tag('div', $dragboxs, array('class' => implode(' ', $classes)));
$classes = array('drags');
if ($options->readonly) {
$classes[] = 'readonly';
}
$result .= html_writer::tag('div', '', array('class' => implode(' ', $classes)));
// We abuse the clear_wrong method to output the hidden form fields we
// want irrespective of whether we are actually clearing the wrong
// bits of the response.
@ -89,7 +83,7 @@ class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_ren
$value = $qa->get_last_qt_var($question->field($place));
$attributes = array(
'class' => 'place' . $place . ' drop group' . $group
'class' => 'place' . $place . ' drop active group' . $group
);
if ($options->readonly) {

View File

@ -11,46 +11,56 @@
margin-bottom: 0.5em;
}
.que.ddwtos .drop {
.que.ddwtos .drop.active {
display: inline-block;
text-align: center;
border: 1px solid #000;
margin-bottom: 2px;
}
.que.ddwtos .drop {
display: none;
}
.que.ddwtos .drags {
height: 0;
}
.que.ddwtos .draghome,
.que.ddwtos .drag {
.que.ddwtos .draghome {
display: inline-block;
text-align: center;
background: transparent;
border: 1px solid #000;
}
.que.ddwtos .draghome {
visibility: hidden;
}
.que.ddwtos .drag {
position: absolute;
z-index: 2;
cursor: move;
}
.que.ddwtos .readonly .drag {
.que.ddwtos .readonly .draghome {
cursor: default;
}
.que.ddwtos .drag.beingdragged {
.que.ddwtos .draghome.beingdragged {
z-index: 3;
box-shadow: 3px 3px 4px #000;
position: absolute;
}
.que.ddwtos .draghome.dragplaceholder {
display: none;
}
.que.ddwtos .draghome.dragplaceholder.active {
visibility: hidden;
display: inline-block;
}
.que.ddwtos .draghome.placed {
margin-bottom: 2px;
}
.que.ddwtos .drop:focus,
.que.ddwtos .drop.valid-drag-over-drop {
.que.ddwtos .drop.valid-drag-over-drop,
.que.ddwtos .draghome.placed:focus:not(.beingdragged),
.que.ddwtos .draghome.placed.valid-drag-over-drop {
border-color: #0a0;
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
}

View File

@ -41,7 +41,8 @@ class behat_qtype_ddwtos extends behat_base {
* @return string the xpath expression.
*/
protected function drag_xpath($dragitem) {
return '//span[contains(@class, " drag ") and contains(., "' . $this->escape($dragitem) . '")]';
return '//span[contains(@class, "draghome") and contains(., "' . $this->escape($dragitem) .
'") and not(contains(@class, "dragplaceholder"))]';
}
/**
@ -82,6 +83,7 @@ class behat_qtype_ddwtos extends behat_base {
$node->keyDown($key);
$node->keyPress($key);
$node->keyUp($key);
$this->wait_for_pending_js();
}
}
}