MDL-63277 qtype_ddwtos: Re-implement old YUI JS in AMD

In the rewrite is one improvement: drag items now animate when they
are moved, for example out of a drop place and back home. Other than
that, the functionality is unchanged. Also, as far as possible, the
HTML structure has not been changed (althought with changes to the HTML,
further JS improvements would be possible. However, those have been left
for a future issue).
This commit is contained in:
M Kassaei
2018-10-20 15:22:12 +01:00
committed by Tim Hunt
parent ed7e30fa5c
commit 68943aff09
10 changed files with 784 additions and 1358 deletions
File diff suppressed because one or more lines are too long
+777
View File
@@ -0,0 +1,777 @@
// 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 allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.
*
* @package qtype
* @subpackage ddwtos
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/dragdrop'], function ($, dd) {
"use strict";
var t = {
/**
* The accuracy of the drag item in brink of a slot to be accepted.
* @private
*/
DRAG_PROXIMITY: 10,
/**
* Tolerance in px for highlighting the borders of question container
* when a drag is about to leave the containe's boundaries.
* @private
*/
CONTAINER_BORDER_TOLERANCE: 10,
/**
* Animation time in milliseconds
* @private
*/
ANIMATION_DURATION: 300,
/**
* Params object, Todo: I only need to know whether the question is 'readonly'
* @private
*/
params: {},
nextDragItemNo : 1,
/**
* Put all our selectors in the same place so we can quickly find and change them later
* if the structure of the document changes.
*/
cssSelectors: function (topnode) {
return {
topNode: function () {
return topnode;
},
dragContainer: function () {
return topnode + ' div.drags';
},
drags: function () {
return this.dragContainer() + ' span.drag';
},
drag: function (no) {
return this.drags() + '.no' + no;
},
dragsInGroup: function (groupno) {
return this.drags() + '.group' + groupno;
},
dragsForChoiceInGroup: function (choiceno, groupno) {
return this.dragsInGroup(groupno) + '.choice' + choiceno;
},
placedDragsInGroup: function (groupno) {
return this.dragsInGroup(groupno) + '.placed';
},
unplacedDragsInGroup: function (groupno) {
return this.dragsInGroup(groupno) + '.unplaced';
},
unplacedDragsForChoiceInGroup: function (choiceno, groupno) {
return this.unplacedDragsInGroup(groupno) + '.choice' + choiceno;
},
dragHomes: function () {
return topnode + ' span.draghome';
},
dragHomesGroup: function (groupno) {
return topnode + ' .draggrouphomes' + groupno + ' span.draghome';
},
dragHome: function (groupno, choiceno) {
return topnode + ' .draggrouphomes' + groupno + ' span.draghome.choice' + choiceno;
},
// List of selectors related to drops.
drops: function () {
return topnode + ' span.drop';
},
dropForPlace: function (placeno) {
return this.drops() + '.place' + placeno;
},
dropsInGroup: function (groupno) {
return this.drops() + '.group' + groupno;
},
dropsGroup: function (groupno) {
return topnode + ' span.drop.group' + groupno;
}
};
},
/**
* Return an array of inputids.
* @param topNode
* @returns {Array}
*/
getInputIds : function (topNode) {
var inputIds = [];
var count = 0;
$(topNode).find('input[type=hidden]').each(function(index, input) {
var questionIdNumber = parseInt(topNode.match(/\d/g));
if ($(input)[0].id.indexOf('_' + questionIdNumber + '_p') !== -1) {
inputIds[++count] = $(input)[0].id;
}
});
return inputIds;
},
/**
* Return a given inputid
* @param topNode
* @param no
* @returns {*}
*/
getInputId : function(topNode, no) {
return t.getInputIds(topNode)[no];
},
/**
* Initialise questions
* @param params
*/
init : function (params) {
var pendingId = 'qtype_ddwtos-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(pendingId);
t.params = params;
t.initQuestion(params['topnode']);
M.util.js_complete(pendingId);
},
/**
* Update the position on the drags on resize
* @param string topNode, unique question id
*/
updateOnResize : function(topNode) {
$(window).on('resize', function () {
t.updateDragsPositions(topNode);
}).resize();
},
/**
* Initialise question.
* @param topNode
*/
initQuestion: function (topNode) {
// Return if question has been initialised.
if ($(topNode).data('initialised') === 1 ) {
return;
}
t.setPaddingSizesAll(topNode);
t.cloneDragItems(topNode);
t.updateDragsPositions(topNode);
t.updateOnResize(topNode);
t.makeDropZones(topNode);
// Remember that the question has been initialised.
$(topNode).data('initialised', 1);
},
/**
* Set the position of the drag to the position of the drop item.
* @param topNode
* @param drag
* @param drop
*/
putDragInDrop: function (topNode, drag, drop) {
var inputNode = $('input#' + t.getInputId(topNode, t.getPlace(drop)));
if (inputNode.val() > 0) {
var prevDrag = $(t.cssSelectors(topNode).dragsForChoiceInGroup(inputNode.val(), t.getGroup(drop)));
if (prevDrag.length) {
t.putBackToOrigin(topNode, prevDrag);
}
}
if (drag && drag.length) {
inputNode.val(t.getChoice(drag));
t.setDragOffset(drag, drop);
drag.css('z-index', (Number(drag.css('z-index')) - 1));
} else {
inputNode.val(0);
drag.removeClass('placed');
drag.addClass('unplaced');
}
},
/**
* Update the position of drags
* @param topNode
*/
updateDragsPositions : function (topNode) {
var unplacedDrags = $(t.cssSelectors(topNode).drags());
unplacedDrags.addClass('unplaced');
for (var i = 0; i < unplacedDrags.length; i++) {
var unplaceDrag = $(unplacedDrags[i]);
var groupNo = t.getGroup(unplaceDrag);
var choiceNo = t.getChoice(unplaceDrag);
unplaceDrag.offset($(topNode + ' .draggrouphomes' + groupNo + ' span:nth-child(' + choiceNo + ')').offset());
}
// Let the drag in target follow the position of the target after resize.
for (var pn in t.getInputIds(topNode)) {
//var drop = $(t.cssSelectors(topNode).dropForPlace(pn));
var inputNode = $('input#' + t.getInputId(topNode, pn));
var choiceNo = Number(inputNode.val());
if (choiceNo !== 0) {
var drop = $(t.cssSelectors(topNode).dropForPlace(pn));
var groupNo = t.getGroup(drop);
var drag = $(t.cssSelectors(topNode).unplacedDragsForChoiceInGroup(choiceNo, groupNo));
if (drag.hasClass('infinite')) {
drag = $(t.cssSelectors(topNode).drag(t.getNo(drag)));
}
t.setDragOffset(drag, drop);
}
}
},
/**
* 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.
* @param topNode
*/
cloneDragItems: function(topNode) {
t.nextDragItemNo = 1;
$(t.cssSelectors(topNode).dragHomes()).each(function(index, draghome) {
t.cloneDragItemsForOneChoice(topNode, $(draghome));
});
},
/**
* Clone drag item for one choice.
* @param topNode
* @param draghome
*/
cloneDragItemsForOneChoice: function(topNode, draghome) {
if (draghome.hasClass('infinite')) {
var groupno = t.getGroup(draghome);
var noofdrags = $(t.cssSelectors(topNode).dropsInGroup(groupno)).length;
for (var i = 0; i < noofdrags; i++) {
t.cloneDragItem(topNode, draghome);
}
} else {
t.cloneDragItem(topNode, draghome);
}
},
/**
* Clone drag item.
* @param topNode
* @param draghome
*/
cloneDragItem: function(topNode, draghome) {
var drag = draghome.clone();
drag.removeClass('draghome');
drag.addClass('drag');
drag.addClass('no' + t.nextDragItemNo);
t.nextDragItemNo++;
drag.css({
'top': draghome.offset().top,
'left': draghome.offset().left,
'visibility': 'visible',
'position': 'absolute'
});
$(t.cssSelectors(topNode).dragContainer()).append(drag);
if (!t.params['readonly']) {
drag.addClass('unplaced');
drag.on('mousedown touchstart', t.mouseDownOrTouchStart(topNode, drag));
}
},
/**
* Return a function on mousedown or touchstart.
* @param dragProxy
* @returns {Function}
*/
mouseDownOrTouchStart: function(topNode, dragProxy) {
return function (e) {
if (dd.prepare(e).start === true) {
e.preventDefault();
dragProxy.css('cursor', 'move');
dragProxy.addClass('moodle-has-zindex');
var choice = t.getChoice(dragProxy);
var group = t.getGroup(dragProxy);
var draginfo = {};
draginfo.id = 'g' + group + 'c' + choice;
draginfo.group = group;
draginfo.choice = choice;
draginfo.drag = dragProxy;
draginfo.origin = dragProxy.position();
var om = function (x, y) {
t.onMove(topNode, x, y, draginfo, dragProxy);
};
var od = function () {
t.onDrop(topNode, draginfo, dragProxy);
};
dragProxy.css('z-index', (Number(dragProxy.css('z-index')) + 1));
dd.start(e, dragProxy, om, od);
}
};
},
/**
* Called when user drags a drag item.
*
* @param topNode
* @param x
* @param y
* @param draginfo
* @param dragProxy
*/
onMove: function (topNode, x, y, draginfo, dragProxy) {
var container = $(topNode).find('.formulation');
if (t.isDragOutsideQuestionContainer(container, x, y)) {
// Highlight the border around the question container.
container.addClass('outside-container');
dragProxy.on('mouseup', function() {
// Remove the highlighted border around the question container.
container.removeClass('outside-container');
});
return;
}
var drops = $(t.cssSelectors(topNode).dropsInGroup(draginfo.group));
drops.each(function(index, drop){
if (t.isDragCloseToTarget(dragProxy, $(drop))) {
$(drop).addClass('valid-drag-over-drop');
} else {
$(drop).removeClass('valid-drag-over-drop');
}
});
},
/**
* Called when user drops a drag item and applies the change.
*
* @param topNode
* @param draginfo
* @param dragProxy
*/
onDrop: function (topNode, draginfo, dragProxy) {
var drops = $(t.cssSelectors(topNode).dropsInGroup(draginfo.group));
var breakOut = false;
drops.each(function(index, drop){
// If dropping the same drag inside the drop break out.
if (t.isDragInTheSameDrop(dragProxy, $(drop))) {
breakOut = true;
return false;
}
if (t.isDragCloseToTarget(dragProxy, $(drop))) {
$(drop).removeClass('valid-drag-over-drop');
t.putDragInDrop(topNode, $(dragProxy), $(drop));
breakOut = true;
return false;
}
});
if(breakOut) {
//breakOut = false;
return false;
} else {
t.putBackToOrigin(topNode, dragProxy);
}
},
/**
* Make drop zones for keyboard access.
* @param topNode
*/
makeDropZones: function(topNode) {
$(t.cssSelectors(topNode).drops()).each(function(index, drop){
t.makeDropZone(topNode, $(drop));
});
},
/**
* Make a drop dropn zone to accept key press.
* @param topNode
* @param drop
*/
makeDropZone: function(topNode, drop) {
if (!t.params['readonly']) {
drop.on('keydown', function(e) {
t.dropZoneKeyPress(topNode, e);
});
}
},
/**
* It put back the drag in position
* @param drag
*/
putBackToOrigin: function (topNode, drag) {
drag.removeClass('placed');
drag.addClass('unplaced');
var groupNo = t.getGroup(drag);
var choiceNo = t.getChoice(drag);
// Find the drag in the list of this draggrouphomes, make the original visible and destroy the cloned drag.
var dragHome = $(topNode + ' .draggrouphomes' + groupNo + ' span:nth-child(' + choiceNo + ')');
// Animate the option back to the original position.
t.animateEl(drag, dragHome, t.ANIMATION_DURATION, 'swing');
},
/**
* Check whether a slot contains an option and returns the option.
*
* @param slot the slot object which may contain an option object
* @returns option object if slot contains an option or null if it doesn't
*/
isDragInDrop: function(topNode, drop) {
if (!drop.length) {
return false;
}
// If there is a drag in this drop put it back to origin.
var dragInDrop = drop.children(topNode + ' span.placed');
if (dragInDrop.length) {
//t.putBackToOrigin(dragInDrop);
return dragInDrop;
}
return false;
},
isDragOutsideQuestionContainer : function (container, x, y) {
var containerEdges = {
'lt': { 'Y': container.offset().top + t.CONTAINER_BORDER_TOLERANCE,
'X': container.offset().left + t.CONTAINER_BORDER_TOLERANCE},
'rt': { 'Y' : container.offset().top + t.CONTAINER_BORDER_TOLERANCE,
'X' : container.offset().left + container[0].clientWidth - t.CONTAINER_BORDER_TOLERANCE},
'lb': { 'Y' : container.offset().top + container[0].clientHeight - t.CONTAINER_BORDER_TOLERANCE,
'X' : container.offset().left},
'rb': { 'Y' : container.offset().top + container[0].clientHeight - t.CONTAINER_BORDER_TOLERANCE,
'X' : container.offset().left + container[0].clientWidth - t.CONTAINER_BORDER_TOLERANCE}
};
// Check boundaries (I know that these ifs could be simplified as one if, bit i think this is more readable).
if (y < containerEdges.lt.Y || x < containerEdges.lt.X) {
$(container).addClass('outside-container');
return true;
}
if (y < containerEdges.rt.Y || x > containerEdges.rt.X) {
$(container).addClass('outside-container');
return true;
}
if (y > containerEdges.lb.Y || x < containerEdges.lb.X) {
$(container).addClass('outside-container');
return true;
}
if (y > containerEdges.rb.Y || x > containerEdges.rb.X) {
$(container).addClass('outside-container');
return true;
}
$(container).removeClass('outside-container');
return false;
},
/**
* Animate the object to the given destination.
*
* @param el object: the object to be animated
* @param duration int: duration on animation in miliseconds
* @param easing string: the easing type
* @param ctop css value of current top
* @param cleft css value of current left
* @param top css value of origin top
* @param left css value of origin left
*/
animateEl: function (dragBack, dragHome, duration, easing) {
var durationFactor = (Math.abs(dragBack.css('left') - dragHome.offset().left) +
Math.abs(dragBack.css('top') - dragHome.offset().top));
durationFactor = (durationFactor > 1) ? durationFactor : 1;
dragBack.animate(
{
opacity: 0.5,
width: (parseInt(dragBack.css('width')) * 1.2) + 'px',
height: (parseInt(dragBack.css('height')) * 1.2) + 'px',
top: dragHome.offset().top,
left: dragHome.offset().left
},
{
duration: duration / durationFactor,
easing: easing
}
);
dragBack.animate(
{
opacity: 1,
width: parseInt(dragBack.css('width')) +'px',
height: parseInt(dragBack.css('height')) + 'px',
top: dragHome.offset().top,
left: dragHome.offset().left
},
{
duration: duration / durationFactor,
easing: easing
}
);
},
/**
* isDragCloseToTarget checks whether the drag item is close enough to the target
*
* @param drag
* @param target
* @returns {boolean}
*/
isDragCloseToTarget: function (drag, target) {
var midDragX = drag.offset().left + drag.outerWidth() / 2;
var midDragY = drag.offset().top + drag.outerHeight() / 2;
var midTargetX = target.offset().left + target.outerWidth() / 2;
var midTargetY = target.offset().top + target.outerHeight() / 2;
// Is the drag item close to target?
if ((midDragX > (midTargetX - t.DRAG_PROXIMITY) &&
midDragX < (midTargetX + t.DRAG_PROXIMITY)) &&
(midDragY > (midTargetY - t.DRAG_PROXIMITY) &&
midDragY < (midTargetY + t.DRAG_PROXIMITY))) {
return true;
}
return false;
},
/**
* Set padding to all drags and drops.
*/
setPaddingSizesAll: function (topNode) {
for (var groupno = 1; groupno <= 8; groupno++) {
t.setPaddingSizeForGroup(topNode, groupno);
}
},
/**
* Set padding to drags and drops in a group.
* @param groupNo
*/
setPaddingSizeForGroup: function (topNode, groupNo) {
var groupItems = $(t.cssSelectors(topNode).dragHomesGroup(groupNo));
if (groupItems.length != 0) {
var maxWidth = 0;
var maxHeight = 0;
// Find max height and width
$(groupItems).each(function (item, groupItem) {
maxWidth = Math.max(maxWidth, Math.ceil(groupItem.offsetWidth));
maxHeight = Math.max(maxHeight, Math.ceil(groupItem.offsetHeight));
});
maxWidth += 8;
maxHeight += 2;
$(groupItems).each(function (item, dragItem) {
t.padWidthHeight(dragItem, maxWidth, maxHeight);
t.positionDrags(topNode, groupNo, dragItem);
});
$(t.cssSelectors(topNode).dropsGroup(groupNo)).each(function (item, dropItem) {
t.padWidthHeight(dropItem, maxWidth + 2, maxHeight + 2);
});
}
},
/**
*
*
* @param topNode
* @param groupNo
* @param dragItem
*/
positionDrags: function (topNode, groupNo, dragItem) {
var dragsingroup = $(t.cssSelectors(topNode).dragsInGroup(groupNo));
$(t.cssSelectors(topNode).dragContainer()).append($(dragsingroup).append($(dragItem)));
},
padWidthHeight: function (node, width, height) {
$(node).css(
{
'width': width + 'px',
'height': height + 'px',
'lineHeight': height + 'px'
}
);
},
/**
* Return the number at the end of the prefix.
* @param node
* @param prefix
* @returns {*|number}
*/
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]);
}
}
}
throw 'Prefix "' + prefix + '" not found in class names.';
},
getChoice: function (node) {
return t.getClassnameNumericSuffix(node, 'choice');
},
getGroup: function (node) {
return t.getClassnameNumericSuffix(node, 'group');
},
getPlace: function (node) {
return t.getClassnameNumericSuffix(node, 'place');
},
getNo: function (node) {
return t.getClassnameNumericSuffix(node, 'no');
},
/**
* Set drag's offset to the appropriate drop's offset.
*
* @param drag
* @param drop
*/
setDragOffset : function(drag, drop) {
drag.removeClass('unplaced');
drag.addClass('placed');
drag.offset(
{
'top' : Math.round(drop.offset().top) + 1,
'left' : Math.round(drop.offset().left) + 1
}
);
},
/**
* Check whether user drop the current drag is in the same drop
* @param drag
* @param drop
* @returns {boolean}
*/
isDragInTheSameDrop: function (drag, drop) {
if (parseInt(drag.css('top')) === Math.round(drop.offset().top) + 1) {
return true;
}
return false;
},
/**
* Remove drag from drop.
* @param drop
*/
removeDragFromDrop: function (topNode, drop) {
t.putDragInDrop(topNode, null, drop);
},
/**
* Tab through drops and place drag in drop or remove drag from drop using keybord.
* @param e
*/
dropZoneKeyPress: function(topNode, e) {
var keys = {
'27': 'remove', // Escape
'32': 'next', // Space
'37': 'previous', // Left arrow
'38': 'previous', // Up arrow
'39': 'next', // Right arrow
'40': 'next' // Down arrow
};
e.direction = keys[e.keyCode];
//e.preventDefault();
switch (e.direction) {
case 'next' :
e.preventDefault();
t.placeNextDragIn(topNode, $(e.target));
break;
case 'previous' :
e.preventDefault();
t.placePreviousDragIn(topNode, $(e.target));
break;
case 'remove' :
e.preventDefault();
t.removeDragFromDrop(topNode, $(e.target));
break;
}
},
/**
* Place next drag in drop.
* @param drop
*/
placeNextDragIn: function(topNode, drop) {
t.chooseNextChoiceForDrop(topNode, drop, 1);
},
/**
* Place previous drag in drop.
* @param drop
*/
placePreviousDragIn: function(topNode, drop) {
t.chooseNextChoiceForDrop(topNode, drop, -1);
},
/**
* Choose the next drag to put in drop.
* @param drop
* @param direction
*/
chooseNextChoiceForDrop: function(topNode, drop, direction) {
var groupNo = t.getGroup(drop);
var current = t.currentChoiceInDrop(topNode, $(drop));
var unplacedDragsInGroup = $(t.cssSelectors(topNode).unplacedDragsInGroup(groupNo));
var next = current;
var arrayOfDrags = unplacedDragsInGroup.toArray();
if (0 === current) {
if (direction === 1) {
next = t.getChoice($(arrayOfDrags.shift()));
} else {
next = t.getChoice($(arrayOfDrags.pop()));
}
} else {
next = current + direction;
}
var drag = $(t.cssSelectors(topNode).unplacedDragsForChoiceInGroup(next, groupNo));
if (drag.length === 0) {
drag = $(t.cssSelectors(topNode).unplacedDragsForChoiceInGroup(next + direction, groupNo));
}
if ($(t.cssSelectors(topNode).dragsForChoiceInGroup(next, groupNo)) === null) {
t.removeDragFromDrop(topNode, drop);
return;
} else {
if (drag.hasClass('infinite')) {
var bOut = false; // Break out of each loop boolean variable.
drag.each(function (index, draginf) {
if ($(draginf).hasClass('unplaced')) {
t.putDragInDrop(topNode, $(draginf), drop);
bOut = true;
return false;
}
});
if (bOut) {
bOut = false;
return false;
}
} else {
t.putDragInDrop(topNode, drag, drop);
}
}
},
/**
* Return curent drag choice in drop.
*
* @param drop
* @returns {*|number}
*/
currentChoiceInDrop: function(topNode, drop) {
var inputNode = $('input#' + t.getInputId(topNode, t.getPlace(drop)));
return Number($(inputNode).val());
}
};
return t;
});
+1 -5
View File
@@ -51,16 +51,12 @@ class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_ren
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', array($params));
return $result;
}
+5 -1
View File
@@ -45,10 +45,14 @@
}
.que.ddwtos .drop:focus,
.que.ddwtos .drop.yui3-dd-drop-over.yui3-dd-drop-active-valid {
.que.ddwtos .drop.valid-drag-over-drop {
border-color: #0a0;
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
}
.que.ddwtos .outside-container {
border-color: #FFA500;
box-shadow: 0 0 5px 5px rgba(255, 200, 50, 1);
}
.que.ddwtos .notreadonly .drag {
cursor: move;
@@ -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"
]
}
}