Merge branch 'MDL-63277' of https://github.com/timhunt/moodle
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
-445
@@ -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"]});
|
||||
-2
File diff suppressed because one or more lines are too long
-445
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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(''));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user