This commit is contained in:
David Monllao
2018-10-24 10:22:16 +02:00
12 changed files with 713 additions and 1413 deletions
File diff suppressed because one or more lines are too long
+669
View File
@@ -0,0 +1,669 @@
// 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 make drag-drop into text questions work.
*
* Some vocabulary to help understand this code:
*
* The question text contains 'drops' - blanks into which the 'drags', the missing
* words, can be put.
*
* The thing that can be moved into the drops are called 'drags'. There may be
* multiple copies of the 'same' drag which does not really cause problems.
* Each drag has a 'choice' number which is the value set on the drop's hidden
* input when this drag is placed in a drop.
*
* These may be in separate 'groups', distinguished by colour.
* Things can only interact with other things in the same group.
* The groups are numbered from 1.
*
* The place where a given drag started from is called its 'home'.
*
* @module qtype_ddwtos/ddwtos
* @package qtype_ddwtos
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.6
*/
define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) {
"use strict";
/**
* Object to handle one drag-drop into text question.
*
* @param {String} containerId id of the outer div for this question.
* @param {boolean} readOnly whether the question is being displayed read-only.
* @constructor
*/
function DragDropToTextQuestion(containerId, readOnly) {
this.containerId = containerId;
if (readOnly) {
this.getRoot().addClass('qtype_ddwtos-readonly');
}
this.resizeAllDragsAndDrops();
this.cloneDrags();
this.positionDrags();
}
/**
* In each group, resize all the items to be the same size.
*/
DragDropToTextQuestion.prototype.resizeAllDragsAndDrops = function() {
var thisQ = this;
this.getRoot().find('.answercontainer > 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.
*/
DragDropToTextQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {
var thisQ = this,
dragHomes = this.getRoot().find('.draggrouphomes' + group + ' span.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(0 + drag.offsetHeight));
});
// The size we will want to set is a bit bigger than this.
maxWidth += 8;
maxHeight += 2;
// Set each drag home to that size.
dragHomes.each(function(i, drag) {
thisQ.setElementSize(drag, maxWidth, maxHeight);
});
// Set each drop to that size.
this.getRoot().find('span.drop.group' + group).each(function(i, drop) {
thisQ.setElementSize(drop, maxWidth, maxHeight);
});
};
/**
* Set a given DOM element to be a particular size.
*
* @param {HTMLElement} element
* @param {int} width
* @param {int} height
*/
DragDropToTextQuestion.prototype.setElementSize = function(element, width, height) {
$(element).width(width).height(height).css('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.
*/
DragDropToTextQuestion.prototype.cloneDrags = function() {
var thisQ = this;
this.getRoot().find('span.draghome').each(function(index, draghome) {
thisQ.cloneDragsForOneChoice($(draghome));
});
};
/**
* 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.
*/
DragDropToTextQuestion.prototype.positionDrags = function() {
var thisQ = this,
root = this.getRoot();
// First move all items back home.
root.find('span.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 once 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('.drop.place' + place).offset());
});
};
/**
* Handles the start of dragging an item.
*
* @param {Event} e the touch start or mouse down event.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.prototype.dragMove = function(pageX, pageY, drag) {
var thisQ = this;
this.getRoot().find('span.drop.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.
*/
DragDropToTextQuestion.prototype.dragEnd = function(pageX, pageY, drag) {
var thisQ = this,
root = this.getRoot(),
placed = false;
root.find('span.drop.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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.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
*/
DragDropToTextQuestion.prototype.handleKeyPress = function(e) {
var drop = $(e.target).closest('.drop'),
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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.prototype.getRoot = function() {
return $(document.getElementById(this.containerId));
};
/**
* Get drag home for a given choice.
*
* @param {int} group the group.
* @param {int} choice the choice number.
* @returns {jQuery} containing that div.
*/
DragDropToTextQuestion.prototype.getDragHome = function(group, choice) {
return this.getRoot().find('.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.
*/
DragDropToTextQuestion.prototype.getUnplacedChoice = function(group, choice) {
return this.getRoot().find('.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).
*/
DragDropToTextQuestion.prototype.getCurrentDragInPlace = function(place) {
return this.getRoot().find('span.drag.inplace' + place);
};
/**
* Return the number of blanks in a given group.
*
* @param {int} group the group number.
* @returns {int} the number of drops.
*/
DragDropToTextQuestion.prototype.noOfDropsInGroup = function(group) {
return this.getRoot().find('.drop.group' + group).length;
};
/**
* Return the number of choices in a given group.
*
* @param {int} group the group number.
* @returns {int} the number of choices.
*/
DragDropToTextQuestion.prototype.noOfChoicesInGroup = function(group) {
return this.getRoot().find('.draghome.group' + group).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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.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.
*/
DragDropToTextQuestion.prototype.getPlace = function(node) {
return this.getClassnameNumericSuffix(node, 'place');
};
/**
* Singleton that tracks all the DragDropToTextQuestions on this page, and deals
* with event dispatching.
*
* @type {Object}
*/
var questionManager = {
/**
* {boolean} used to ensure the event handlers are only initialised once per page.
*/
eventHandlersInitialised: false,
/**
* {DragDropToTextQuestion[]} all the questions on this page, indexed by containerId (id on the .que div).
*/
questions: {},
/**
* Initialise questions.
*
* @param {String} containerId id of the outer div for this question.
* @param {boolean} readOnly whether the question is being displayed read-only.
*/
init: function(containerId, readOnly) {
questionManager.questions[containerId] = new DragDropToTextQuestion(containerId, readOnly);
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.ddwtos:not(.qtype_ddwtos-readonly) span.drag',
questionManager.handleDragStart)
.on('keydown',
'.que.ddwtos:not(.qtype_ddwtos-readonly) span.drop',
questionManager.handleKeyPress);
$(window).on('resize', questionManager.handleWindowResize);
},
/**
* Handle mouse down / touch start 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 on drops.
* @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].positionDrags();
}
}
},
/**
* Given an event, work out which question it affects.
*
* @param {Event} e the event.
* @returns {DragDropToTextQuestion|undefined} The question, or undefined.
*/
getQuestionForEvent: function(e) {
var containerId = $(e.currentTarget).closest('.que.ddwtos').attr('id');
return questionManager.questions[containerId];
}
};
/**
* @alias module:qtype_ddwtos/ddwtos
*/
return {
/**
* Initialise one drag-drop into text question.
*
* @param {String} containerId id of the outer div for this question.
* @param {boolean} readOnly whether the question is being displayed read-only.
*/
init: questionManager.init
};
});
+25 -35
View File
@@ -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),
'class' => 'placeinput place' . $place . ' group' . $group,
'name' => $qa->get_qt_field_name($fieldname),
'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($cleanvalue)));
} else {
$output .= html_writer::empty_tag('input', array(
'type' => 'hidden',
'id' => $this->box_id($qa, 'p' . $place),
'name' => $qa->get_qt_field_name($fieldname),
'value' => s($value)));
}
}
return $output;
+16 -19
View File
@@ -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,21 +35,8 @@
}
.que.ddwtos .drag {
position: absolute;
z-index: 2;
}
.que.ddwtos .drag.yui3-dd-dragging {
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 {
border-color: #0a0;
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
}
.que.ddwtos .notreadonly .drag {
cursor: move;
}
@@ -58,6 +44,17 @@
cursor: default;
}
.que.ddwtos .drag.beingdragged {
z-index: 3;
box-shadow: 3px 3px 4px #000;
}
.que.ddwtos .drop:focus,
.que.ddwtos .drop.valid-drag-over-drop {
border-color: #0a0;
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
}
.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
@@ -1,445 +0,0 @@
YUI.add('moodle-qtype_ddwtos-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/>.
/**
* 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;
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_choice_in_drop: function(drop) {
var inputid = this.get('inputids')[this.get_place(drop)];
var inputnode = Y.one('input#' + inputid);
return Number(inputnode.get('value'));
}
}, {
NAME: DDWTOSDDNAME,
ATTRS: {
readonly: {value: false},
topnode: {value: null},
inputids: {value: null}
}
});
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_ddwtos = M.qtype_ddwtos || {};
M.qtype_ddwtos.init_question = function(config) {
return new DDWTOS_DD(config);
};
}, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});
File diff suppressed because one or more lines are too long
@@ -1,445 +0,0 @@
YUI.add('moodle-qtype_ddwtos-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/>.
/**
* 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;
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_choice_in_drop: function(drop) {
var inputid = this.get('inputids')[this.get_place(drop)];
var inputnode = Y.one('input#' + inputid);
return Number(inputnode.get('value'));
}
}, {
NAME: DDWTOSDDNAME,
ATTRS: {
readonly: {value: false},
topnode: {value: null},
inputids: {value: null}
}
});
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_ddwtos = M.qtype_ddwtos || {};
M.qtype_ddwtos.init_question = function(config) {
return new DDWTOS_DD(config);
};
}, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});
@@ -1,10 +0,0 @@
{
"name": "moodle-qtype_ddwtos-dd",
"builds": {
"moodle-qtype_ddwtos-dd": {
"jsfiles": [
"ddwtos.js"
]
}
}
}
-440
View File
@@ -1,440 +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/>.
/**
* 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;
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_choice_in_drop: function(drop) {
var inputid = this.get('inputids')[this.get_place(drop)];
var inputnode = Y.one('input#' + inputid);
return Number(inputnode.get('value'));
}
}, {
NAME: DDWTOSDDNAME,
ATTRS: {
readonly: {value: false},
topnode: {value: null},
inputids: {value: null}
}
});
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_ddwtos = M.qtype_ddwtos || {};
M.qtype_ddwtos.init_question = function(config) {
return new DDWTOS_DD(config);
};
@@ -1,10 +0,0 @@
{
"moodle-qtype_ddwtos-dd": {
"requires": [
"node",
"dd",
"dd-drop",
"dd-constrain"
]
}
}
+1 -6
View File
@@ -59,8 +59,7 @@ abstract class qtype_elements_embedded_in_question_text_renderer
}
$result = '';
$result .= html_writer::tag('div', $questiontext,
array('class' => $this->qtext_classname(), 'id' => $this->qtext_id($qa)));
$result .= html_writer::tag('div', $questiontext, array('class' => 'qtext'));
$result .= $this->post_qtext_elements($qa, $options);
@@ -99,10 +98,6 @@ abstract class qtype_elements_embedded_in_question_text_renderer
return $glues;
}
protected function qtext_classname() {
return 'qtext';
}
protected function qtext_id($qa) {
return str_replace(':', '_', $qa->get_qt_field_name(''));
}