Commit fcc50ccb authored by David Monllaó's avatar David Monllaó
Browse files

Merge branch 'MDL-63277' of https://github.com/timhunt/moodle

parents f560f366 b51f4f2e
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
This diff is collapsed.
......@@ -36,31 +36,14 @@ require_once($CFG->dirroot . '/question/type/gapselect/rendererbase.php');
*/
class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_renderer {
protected function qtext_classname() {
return 'qtext ddwtos_questionid_for_javascript';
}
public function formulation_and_controls(question_attempt $qa,
question_display_options $options) {
global $PAGE;
$result = parent::formulation_and_controls($qa, $options);
$inputids = array();
$question = $qa->get_question();
foreach ($question->places as $placeno => $place) {
$inputids[$placeno] = $this->box_id($qa, $question->field($placeno));
}
$params = array(
'inputids' => $inputids,
'topnode' => 'div.que.ddwtos#q' . $qa->get_slot(),
'readonly' => $options->readonly
);
$PAGE->requires->yui_module('moodle-qtype_ddwtos-dd',
'M.qtype_ddwtos.init_question', array($params));
$PAGE->requires->js_call_amd('qtype_ddwtos/ddwtos', 'init',
['q' . $qa->get_slot(), $options->readonly]);
return $result;
}
......@@ -76,17 +59,13 @@ class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_ren
}
$classes = array('answercontainer');
if (!$options->readonly) {
$classes[] = 'notreadonly';
} else {
if ($options->readonly) {
$classes[] = 'readonly';
}
$result .= html_writer::tag('div', $dragboxs, array('class' => implode(' ', $classes)));
$classes = array('drags');
if (!$options->readonly) {
$classes[] = 'notreadonly';
} else {
if ($options->readonly) {
$classes[] = 'readonly';
}
$result .= html_writer::tag('div', '', array('class' => implode(' ', $classes)));
......@@ -155,8 +134,12 @@ class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_ren
}
/**
* Actually, this question type abuses this method to always ouptut the
* Actually, this question type abuses this method to always output the
* hidden fields it needs.
*
* @param question_attempt $qa the question attempt.
* @param bool $reallyclear whether we are really clearing the responses, or just outputting them.
* @return string HTML to output.
*/
public function clear_wrong(question_attempt $qa, $reallyclear = true) {
$question = $qa->get_question();
......@@ -172,30 +155,37 @@ class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_ren
foreach ($question->places as $place => $group) {
$fieldname = $question->field($place);
if (array_key_exists($fieldname, $response)) {
$value = $response[$fieldname];
$value = (string) $response[$fieldname];
} else {
$value = '0';
}
if (array_key_exists($fieldname, $cleanresponse)) {
$cleanvalue = $cleanresponse[$fieldname];
$cleanvalue = (string) $cleanresponse[$fieldname];
} else {
$cleanvalue = '0';
}
if ($cleanvalue != $value) {
if ($cleanvalue === $value) {
// Normal case: just one hidden input, to store the
// current value and be the value submitted.
$output .= html_writer::empty_tag('input', array(
'type' => 'hidden',
'id' => $this->box_id($qa, 'p' . $place),
'value' => s($value))) .
html_writer::empty_tag('input', array(
'type' => 'hidden',
'class' => 'placeinput place' . $place . ' group' . $group,
'name' => $qa->get_qt_field_name($fieldname),
'value' => s($cleanvalue)));
'value' => s($value)));
} else {
// The case, which only happens when the question is read-only, where
// we want to show the drag item in a given place (first hidden input),
// but when submitted, we want it to go to a different place (second input).
$output .= html_writer::empty_tag('input', array(
'type' => 'hidden',
'id' => $this->box_id($qa, 'p' . $place),
'class' => 'placeinput place' . $place . ' group' . $group,
'value' => s($value))) .
html_writer::empty_tag('input', array(
'type' => 'hidden',
'name' => $qa->get_qt_field_name($fieldname),
'value' => s($value)));
'value' => s($cleanvalue)));
}
}
return $output;
......
......@@ -18,16 +18,15 @@
margin-bottom: 2px;
}
.que.ddwtos .drags {
height: 0;
}
.que.ddwtos .draghome,
.que.ddwtos .drag {
display: inline-block;
text-align: center;
background: transparent;
border: 0;
}
.que.ddwtos .draghome,
.que.ddwtos .drag.unplaced {
border: 1px solid #000;
}
......@@ -36,28 +35,26 @@
}
.que.ddwtos .drag {
position: absolute;
z-index: 2;
cursor: move;
}
.que.ddwtos .drag.yui3-dd-dragging {
.que.ddwtos .readonly .drag {
cursor: default;
}
.que.ddwtos .drag.beingdragged {
z-index: 3;
box-shadow: 3px 3px 4px #000;
}
.que.ddwtos .drop:focus,
.que.ddwtos .drop.yui3-dd-drop-over.yui3-dd-drop-active-valid {
.que.ddwtos .drop.valid-drag-over-drop {
border-color: #0a0;
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
}
.que.ddwtos .notreadonly .drag {
cursor: move;
}
.que.ddwtos .readonly .drag {
cursor: default;
}
.que.ddwtos span.incorrect {
background-color: #faa;
}
......
......@@ -30,7 +30,7 @@ Feature: Preview a drag-drop into text 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
And I drag "quick" to space "1" in the drag and drop into text question
And I drag "fox" to space "2" in the drag and drop into text question
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
{
"name": "moodle-qtype_ddwtos-dd",
"builds": {
"moodle-qtype_ddwtos-dd": {
"jsfiles": [
"ddwtos.js"
]
}
}
}
// 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 code for the ddwtos question type.
*
* @package qtype
* @subpackage ddwtos
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var DDWTOSDDNAME = 'ddwtos_dd';
var DDWTOS_DD = function() {
DDWTOS_DD.superclass.constructor.apply(this, arguments);
};
/**
* This is the class for ddwtos question rendering.
* A DDWTOS_DD class is created for each question.
*/
Y.extend(DDWTOS_DD, Y.Base, {
selectors: null,
passiveSupported: false,
initializer: function() {
var pendingid = 'qtype_ddwtos-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(pendingid);
this.selectors = this.css_selectors(this.get('topnode'));
this.set_padding_sizes_all();
this.clone_drag_items();
this.initial_place_of_drag_items();
this.make_drop_zones();
if (!this.get('readonly')) {
Y.later(500, this, this.position_drag_items, [pendingid, true]);
} else {
Y.later(500, this, this.position_drag_items, [pendingid, 3]);
Y.one('window').on('resize', function() {
this.position_drag_items(pendingid);
}, this);
}
this.checkPassiveSupported();
},
/**
* put all our selectors in the same place so we can quickly find and change them later
* if the structure of the document changes.
*/
css_selectors: function(topnode) {
return {
top_node: function() {
return topnode;
},
drag_container: function() {
return topnode + ' div.drags';
},
drags: function() {
return this.drag_container() + ' span.drag';
},
drag: function(no) {
return this.drags() + '.no' + no;
},
drags_in_group: function(groupno) {
return this.drags() + '.group' + groupno;
},
unplaced_drags_in_group: function(groupno) {
return this.drags_in_group(groupno) + '.unplaced';
},
drags_for_choice_in_group: function(choiceno, groupno) {
return this.drags_in_group(groupno) + '.choice' + choiceno;
},
unplaced_drags_for_choice_in_group: function(choiceno, groupno) {
return this.unplaced_drags_in_group(groupno) + '.choice' + choiceno;
},
drops: function() {
return topnode + ' span.drop';
},
drop_for_place: function(placeno) {
return this.drops() + '.place' + placeno;
},
drops_in_group: function(groupno) {
return this.drops() + '.group' + groupno;
},
drag_homes: function() {
return topnode + ' span.draghome';
},
drag_homes_group: function(groupno) {
return topnode + ' .draggrouphomes' + groupno + ' span.draghome';
},
drag_home: function(groupno, choiceno) {
return topnode + ' .draggrouphomes' + groupno + ' span.draghome.choice' + choiceno;
},
drops_group: function(groupno) {
return topnode + ' span.drop.group' + groupno;
}
};
},
set_padding_sizes_all: function() {
for (var groupno = 1; groupno <= 8; groupno++) {
this.set_padding_size_for_group(groupno);
}
},
set_padding_size_for_group: function(groupno) {
var groupitems = Y.all(this.selectors.drag_homes_group(groupno));
if (groupitems.size() !== 0) {
var maxwidth = 0;
var maxheight = 0;
// find max height and width
groupitems.each(function(item) {
maxwidth = Math.max(maxwidth, Math.ceil(item.get('offsetWidth')));
maxheight = Math.max(maxheight, Math.ceil(item.get('offsetHeight')));
}, this);
maxwidth += 8;
maxheight += 2;
groupitems.each(function(item) {
this.pad_to_width_height(item, maxwidth, maxheight);
}, this);
Y.all(this.selectors.drops_group(groupno)).each(function(item) {
this.pad_to_width_height(item, maxwidth + 2, maxheight + 2);
}, this);
}
},
pad_to_width_height: function(node, width, height) {
node.setStyle('width', width + 'px').setStyle('height', height + 'px')
.setStyle('lineHeight', height + 'px');
},
/**
* 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.
*/
clone_drag_items: function() {
Y.all(this.selectors.drag_homes()).each(this.clone_drag_items_for_one_choice, this);
},
clone_drag_items_for_one_choice: function(draghome) {
if (draghome.hasClass('infinite')) {
var groupno = this.get_group(draghome);
var noofdrags = Y.all(this.selectors.drops_in_group(groupno)).size();
for (var i = 0; i < noofdrags; i++) {
this.clone_drag_item(draghome);
}
} else {
this.clone_drag_item(draghome);
}
},
nextdragitemno: 1,
clone_drag_item: function(draghome) {
var drag = draghome.cloneNode(true);
drag.removeClass('draghome');
drag.addClass('drag');
drag.addClass('no' + this.nextdragitemno);
this.nextdragitemno++;
drag.setStyles({'visibility': 'visible', 'position': 'absolute'});
Y.one(this.selectors.drag_container()).appendChild(drag);
if (!this.get('readonly')) {
this.make_draggable(drag);
}
},
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 Number(match[0]);
}
}
}
throw 'Prefix "' + prefix + '" not found in class names.';
},
get_choice: function(node) {
return this.get_classname_numeric_suffix(node, 'choice');
},
get_group: function(node) {
return this.get_classname_numeric_suffix(node, 'group');
},
get_place: function(node) {
return this.get_classname_numeric_suffix(node, 'place');
},
get_no: function(node) {
return this.get_classname_numeric_suffix(node, 'no');
},
placed: null,
initial_place_of_drag_items: function() {
Y.all(this.selectors.drags()).addClass('unplaced');
this.placed = [];
for (var placeno in this.get('inputids')) {
var inputid = this.get('inputids')[placeno];
var inputnode = Y.one('input#' + inputid);
var choiceno = Number(inputnode.get('value'));
if (choiceno !== 0) {
var drop = Y.one(this.selectors.drop_for_place(placeno));
var groupno = this.get_group(drop);
var drag =
Y.one(this.selectors.unplaced_drags_for_choice_in_group(choiceno, groupno));
this.place_drag_in_drop(drag, drop);
this.position_drag_item(drag);
}
}
},
make_draggable: function(drag) {
new Y.DD.Drag({
node: drag,
groups: [this.get_group(drag)],
dragMode: 'point'
}).plug(Y.Plugin.DDConstrained, {constrain2node: this.selectors.top_node()});
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(drag);
},
/**
* 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;
}
},
make_drop_zones: function() {
Y.all(this.selectors.drops()).each(this.make_drop_zone, this);
},
make_drop_zone: function(drop) {
var dropdd = new Y.DD.Drop({
node: drop,
groups: [this.get_group(drop)]});
dropdd.on('drop:hit', function(e) {
var drag = e.drag.get('node');
var drop = e.drop.get('node');
if (this.get_group(drop) === this.get_group(drag)) {
this.place_drag_in_drop(drag, drop);
}
}, this);
if (!this.get('readonly')) {
drop.on('dragchange', this.drop_zone_key_press, this);
}
},
place_drag_in_drop: function(drag, drop) {
var placeno = this.get_place(drop);
var inputid = this.get('inputids')[placeno];
var inputnode = Y.one('input#' + inputid);
if (drag !== null) {
inputnode.set('value', this.get_choice(drag));
} else {
inputnode.set('value', '0');
}
for (var alreadytheredragno in this.placed) {
if (this.placed[alreadytheredragno] === placeno) {
delete this.placed[alreadytheredragno];
var alreadytheredrag = Y.one(this.selectors.drag(alreadytheredragno));
if (alreadytheredrag && alreadytheredrag.dd) {
alreadytheredrag.dd.detach('drag:start');
}
}
}
if (drag !== null) {
this.placed[this.get_no(drag)] = placeno;
if (drag.dd) {
drag.dd.once('drag:start', function(e, inputnode, drag) {
inputnode.set('value', 0);
delete this.placed[this.get_no(drag)];
drag.addClass('unplaced');
}, this, inputnode, drag);
}
}
},
remove_drag_from_drop: function(drop) {
this.place_drag_in_drop(null, drop);
},
/**
* Postition, or reposition, all the drag items.
* @param pendingid (optional) if given, then mark the js task complete after the
* items are all positioned.
* @param dotimeout (optional) if true, continually re-position the items so
* they stay in place. Else, if an integer, reposition this many times before stopping.
*/
position_drag_items: function(pendingid, dotimeout) {
Y.all(this.selectors.drags()).each(this.position_drag_item, this);
M.util.js_complete(pendingid);
if (dotimeout === true || dotimeout > 0) {
if (dotimeout !== true) {
dotimeout -= 1;
}
Y.later(500, this, this.position_drag_items, [pendingid, dotimeout]);
}
},
position_drag_item: function(drag) {
if (!drag.hasClass('yui3-dd-dragging')) {
if (!this.placed[this.get_no(drag)]) {
var groupno = this.get_group(drag);
var choiceno = this.get_choice(drag);
var home = Y.one(this.selectors.drag_home(groupno, choiceno));
drag.setXY(home.getXY());
drag.addClass('unplaced');
} else {
var placeno = this.placed[this.get_no(drag)];
var drop = Y.one(this.selectors.drop_for_place(placeno));
drag.setXY([drop.getX() + 2, drop.getY() + 2]);
drag.removeClass('unplaced');
}
}
},
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();
},
place_next_drag_in: function(drop) {
this.choose_next_choice_for_drop(drop, 1);
},
place_previous_drag_in: function(drop) {
this.choose_next_choice_for_drop(drop, -1);
},
choose_next_choice_for_drop: function(drop, direction) {
var next;
var groupno = this.get_group(drop);
var current = this.current_choice_in_drop(drop);
var unplaceddragsingroup = Y.all(this.selectors.unplaced_drags_in_group(groupno));
if (0 === current) {
if (direction === 1) {
next = 1;
} else {
var lastdrag = unplaceddragsingroup.pop();
next = this.get_choice(lastdrag);
}
} else {
next = current + direction;
}
var drag;
do {
drag = Y.one(this.selectors.unplaced_drags_for_choice_in_group(next, groupno));
if (Y.one(this.selectors.drags_for_choice_in_group(next, groupno)) === null) {
this.remove_drag_from_drop(drop);
return;
}