From 68943aff090c67c6ee26555c9559900a181f391b Mon Sep 17 00:00:00 2001 From: M Kassaei Date: Fri, 9 Mar 2018 11:40:10 +0000 Subject: [PATCH 1/3] 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). --- question/type/ddwtos/amd/build/ddwtos.min.js | 1 + question/type/ddwtos/amd/src/ddwtos.js | 777 ++++++++++++++++++ question/type/ddwtos/renderer.php | 6 +- question/type/ddwtos/styles.css | 6 +- .../moodle-qtype_ddwtos-dd-debug.js | 445 ---------- .../moodle-qtype_ddwtos-dd-min.js | 2 - .../moodle-qtype_ddwtos-dd.js | 445 ---------- .../type/ddwtos/yui/src/ddwtos/build.json | 10 - .../type/ddwtos/yui/src/ddwtos/js/ddwtos.js | 440 ---------- .../ddwtos/yui/src/ddwtos/meta/ddwtos.json | 10 - 10 files changed, 784 insertions(+), 1358 deletions(-) create mode 100644 question/type/ddwtos/amd/build/ddwtos.min.js create mode 100644 question/type/ddwtos/amd/src/ddwtos.js delete mode 100644 question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js delete mode 100644 question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js delete mode 100644 question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js delete mode 100644 question/type/ddwtos/yui/src/ddwtos/build.json delete mode 100644 question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js delete mode 100644 question/type/ddwtos/yui/src/ddwtos/meta/ddwtos.json diff --git a/question/type/ddwtos/amd/build/ddwtos.min.js b/question/type/ddwtos/amd/build/ddwtos.min.js new file mode 100644 index 00000000000..3c4d47542cb --- /dev/null +++ b/question/type/ddwtos/amd/build/ddwtos.min.js @@ -0,0 +1 @@ +define(["jquery","core/dragdrop"],function(a,b){"use strict";var c={DRAG_PROXIMITY:10,CONTAINER_BORDER_TOLERANCE:10,ANIMATION_DURATION:300,params:{},nextDragItemNo:1,cssSelectors:function(a){return{topNode:function(){return a},dragContainer:function(){return a+" div.drags"},drags:function(){return this.dragContainer()+" span.drag"},drag:function(a){return this.drags()+".no"+a},dragsInGroup:function(a){return this.drags()+".group"+a},dragsForChoiceInGroup:function(a,b){return this.dragsInGroup(b)+".choice"+a},placedDragsInGroup:function(a){return this.dragsInGroup(a)+".placed"},unplacedDragsInGroup:function(a){return this.dragsInGroup(a)+".unplaced"},unplacedDragsForChoiceInGroup:function(a,b){return this.unplacedDragsInGroup(b)+".choice"+a},dragHomes:function(){return a+" span.draghome"},dragHomesGroup:function(b){return a+" .draggrouphomes"+b+" span.draghome"},dragHome:function(b,c){return a+" .draggrouphomes"+b+" span.draghome.choice"+c},drops:function(){return a+" span.drop"},dropForPlace:function(a){return this.drops()+".place"+a},dropsInGroup:function(a){return this.drops()+".group"+a},dropsGroup:function(b){return a+" span.drop.group"+b}}},getInputIds:function(b){var c=[],d=0;return a(b).find("input[type=hidden]").each(function(e,f){var g=parseInt(b.match(/\d/g));a(f)[0].id.indexOf("_"+g+"_p")!==-1&&(c[++d]=a(f)[0].id)}),c},getInputId:function(a,b){return c.getInputIds(a)[b]},init:function(a){var b="qtype_ddwtos-"+Math.random().toString(36).slice(2);M.util.js_pending(b),c.params=a,c.initQuestion(a.topnode),M.util.js_complete(b)},updateOnResize:function(b){a(window).on("resize",function(){c.updateDragsPositions(b)}).resize()},initQuestion:function(b){1!==a(b).data("initialised")&&(c.setPaddingSizesAll(b),c.cloneDragItems(b),c.updateDragsPositions(b),c.updateOnResize(b),c.makeDropZones(b),a(b).data("initialised",1))},putDragInDrop:function(b,d,e){var f=a("input#"+c.getInputId(b,c.getPlace(e)));if(f.val()>0){var g=a(c.cssSelectors(b).dragsForChoiceInGroup(f.val(),c.getGroup(e)));g.length&&c.putBackToOrigin(b,g)}d&&d.length?(f.val(c.getChoice(d)),c.setDragOffset(d,e),d.css("z-index",Number(d.css("z-index"))-1)):(f.val(0),d.removeClass("placed"),d.addClass("unplaced"))},updateDragsPositions:function(b){var d=a(c.cssSelectors(b).drags());d.addClass("unplaced");for(var e=0;ef.rt.X?(a(b).addClass("outside-container"),!0):e>f.lb.Y||df.rb.Y||d>f.rb.X?(a(b).addClass("outside-container"),!0):(a(b).removeClass("outside-container"),!1)},animateEl:function(a,b,c,d){var e=Math.abs(a.css("left")-b.offset().left)+Math.abs(a.css("top")-b.offset().top);e=e>1?e:1,a.animate({opacity:.5,width:1.2*parseInt(a.css("width"))+"px",height:1.2*parseInt(a.css("height"))+"px",top:b.offset().top,left:b.offset().left},{duration:c/e,easing:d}),a.animate({opacity:1,width:parseInt(a.css("width"))+"px",height:parseInt(a.css("height"))+"px",top:b.offset().top,left:b.offset().left},{duration:c/e,easing:d})},isDragCloseToTarget:function(a,b){var d=a.offset().left+a.outerWidth()/2,e=a.offset().top+a.outerHeight()/2,f=b.offset().left+b.outerWidth()/2,g=b.offset().top+b.outerHeight()/2;return d>f-c.DRAG_PROXIMITY&&dg-c.DRAG_PROXIMITY&&e. + +/* + * 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; +}); diff --git a/question/type/ddwtos/renderer.php b/question/type/ddwtos/renderer.php index 7425082df2a..b59e1e41266 100644 --- a/question/type/ddwtos/renderer.php +++ b/question/type/ddwtos/renderer.php @@ -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; } diff --git a/question/type/ddwtos/styles.css b/question/type/ddwtos/styles.css index aa7b8ab3732..201f6c9a224 100644 --- a/question/type/ddwtos/styles.css +++ b/question/type/ddwtos/styles.css @@ -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; diff --git a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js b/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js deleted file mode 100644 index eaad12d19a3..00000000000 --- a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js +++ /dev/null @@ -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 . - -/** - * 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"]}); diff --git a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js b/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js deleted file mode 100644 index ff5bf8c1fde..00000000000 --- a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js +++ /dev/null @@ -1,2 +0,0 @@ -YUI.add("moodle-qtype_ddwtos-dd",function(e,t){var n="ddwtos_dd",r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,e.Base,{selectors:null,passiveSupported:!1,initializer:function(){var t="qtype_ddwtos-"+Math.random().toString(36).slice(2);M.util.js_pending(t),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(),this.get("readonly")?(e.later(500,this,this.position_drag_items,[t,3]),e.one("window").on("resize",function(){this.position_drag_items(t)},this)):e.later(500,this,this.position_drag_items,[t,!0]),this.checkPassiveSupported()},css_selectors:function(e){return{top_node:function(){return e},drag_container:function(){return e+" div.drags"},drags:function(){return this.drag_container()+" span.drag"},drag:function(e){return this.drags()+".no"+e},drags_in_group:function(e){return this.drags()+".group"+e},unplaced_drags_in_group:function(e){return this.drags_in_group(e)+".unplaced"},drags_for_choice_in_group:function(e,t){return this.drags_in_group(t)+".choice"+e},unplaced_drags_for_choice_in_group:function(e,t){return this.unplaced_drags_in_group(t)+".choice"+e},drops:function(){return e+" span.drop"},drop_for_place:function(e){return this.drops()+".place"+e},drops_in_group:function(e){return this.drops()+".group"+e},drag_homes:function(){return e+" span.draghome"},drag_homes_group:function(t){return e+" .draggrouphomes"+t+" span.draghome"},drag_home:function(t,n){return e+" .draggrouphomes"+t+" span.draghome.choice"+n},drops_group:function(t){return e+" span.drop.group"+t}}},set_padding_sizes_all:function(){for(var e=1;e<=8;e++)this.set_padding_size_for_group(e)},set_padding_size_for_group:function(t){var n=e.all(this.selectors.drag_homes_group(t));if(n.size()!==0){var r=0,i=0;n.each(function(e){r=Math.max(r,Math.ceil(e.get("offsetWidth"))),i=Math.max(i,Math.ceil(e.get("offsetHeight")))},this),r+=8,i+=2,n.each(function(e){this.pad_to_width_height(e,r,i)},this),e.all(this.selectors.drops_group(t)).each(function(e){this.pad_to_width_height(e,r+2,i+2)},this)}},pad_to_width_height:function(e,t,n){e.setStyle("width",t+"px").setStyle("height",n+"px").setStyle("lineHeight",n+"px")},clone_drag_items:function(){e.all(this.selectors.drag_homes()).each(this.clone_drag_items_for_one_choice,this)},clone_drag_items_for_one_choice:function(t){if(t.hasClass("infinite")){var n=this.get_group(t),r=e.all(this.selectors.drops_in_group(n)).size();for(var i=0;i0)n!==!0&&(n-=1),e.later(500,this,this.position_drag_items,[t,n])},position_drag_item:function(t){if(!t.hasClass("yui3-dd-dragging"))if(!this.placed[this.get_no(t)]){var n=this.get_group(t),r=this.get_choice(t),i=e.one(this.selectors.drag_home(n,r));t.setXY(i.getXY()),t.addClass("unplaced")}else{var s=this -.placed[this.get_no(t)],o=e.one(this.selectors.drop_for_place(s));t.setXY([o.getX()+2,o.getY()+2]),t.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)}e.preventDefault()},place_next_drag_in:function(e){this.choose_next_choice_for_drop(e,1)},place_previous_drag_in:function(e){this.choose_next_choice_for_drop(e,-1)},choose_next_choice_for_drop:function(t,n){var r,i=this.get_group(t),s=this.current_choice_in_drop(t),o=e.all(this.selectors.unplaced_drags_in_group(i));if(0===s)if(n===1)r=1;else{var u=o.pop();r=this.get_choice(u)}else r=s+n;var a;do{a=e.one(this.selectors.unplaced_drags_for_choice_in_group(r,i));if(e.one(this.selectors.drags_for_choice_in_group(r,i))===null){this.remove_drag_from_drop(t);return}r+=n}while(a===null);this.place_drag_in_drop(a,t)},current_choice_in_drop:function(t){var n=this.get("inputids")[this.get_place(t)],r=e.one("input#"+n);return Number(r.get("value"))}},{NAME:n,ATTRS:{readonly:{value:!1},topnode:{value:null},inputids:{value:null}}}),e.Event.define("dragchange",{_event:e.UA.webkit||e.UA.ie?"keydown":"keypress",_keys:{32:"next",37:"previous",38:"previous",39:"next",40:"next",27:"remove"},_keyHandler:function(e,t){this._keys[e.keyCode]&&(e.direction=this._keys[e.keyCode],t.fire(e))},on:function(e,t,n){t._detacher=e.on(this._event,this._keyHandler,this,n)}}),M.qtype_ddwtos=M.qtype_ddwtos||{},M.qtype_ddwtos.init_question=function(e){return new r(e)}},"@VERSION@",{requires:["node","dd","dd-drop","dd-constrain"]}); diff --git a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js b/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js deleted file mode 100644 index eaad12d19a3..00000000000 --- a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js +++ /dev/null @@ -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 . - -/** - * 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"]}); diff --git a/question/type/ddwtos/yui/src/ddwtos/build.json b/question/type/ddwtos/yui/src/ddwtos/build.json deleted file mode 100644 index d67dd9a123b..00000000000 --- a/question/type/ddwtos/yui/src/ddwtos/build.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "moodle-qtype_ddwtos-dd", - "builds": { - "moodle-qtype_ddwtos-dd": { - "jsfiles": [ - "ddwtos.js" - ] - } - } -} diff --git a/question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js b/question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js deleted file mode 100644 index 79fd922e000..00000000000 --- a/question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js +++ /dev/null @@ -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 . - -/** - * 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); -}; diff --git a/question/type/ddwtos/yui/src/ddwtos/meta/ddwtos.json b/question/type/ddwtos/yui/src/ddwtos/meta/ddwtos.json deleted file mode 100644 index 6e7953d7c73..00000000000 --- a/question/type/ddwtos/yui/src/ddwtos/meta/ddwtos.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "moodle-qtype_ddwtos-dd": { - "requires": [ - "node", - "dd", - "dd-drop", - "dd-constrain" - ] - } -} From c28d0aa113cc192476cd34e46cf089cb739c64d4 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 12 Oct 2018 16:40:54 +0100 Subject: [PATCH 2/3] MDL-63277 qtype_ddwtos: further improvements to the JavaScript --- question/type/ddwtos/amd/build/ddwtos.min.js | 2 +- question/type/ddwtos/amd/src/ddwtos.js | 1376 ++++++++--------- question/type/ddwtos/renderer.php | 52 +- question/type/ddwtos/styles.css | 29 +- .../type/ddwtos/tests/behat/preview.feature | 2 +- 5 files changed, 672 insertions(+), 789 deletions(-) diff --git a/question/type/ddwtos/amd/build/ddwtos.min.js b/question/type/ddwtos/amd/build/ddwtos.min.js index 3c4d47542cb..388b76148ec 100644 --- a/question/type/ddwtos/amd/build/ddwtos.min.js +++ b/question/type/ddwtos/amd/build/ddwtos.min.js @@ -1 +1 @@ -define(["jquery","core/dragdrop"],function(a,b){"use strict";var c={DRAG_PROXIMITY:10,CONTAINER_BORDER_TOLERANCE:10,ANIMATION_DURATION:300,params:{},nextDragItemNo:1,cssSelectors:function(a){return{topNode:function(){return a},dragContainer:function(){return a+" div.drags"},drags:function(){return this.dragContainer()+" span.drag"},drag:function(a){return this.drags()+".no"+a},dragsInGroup:function(a){return this.drags()+".group"+a},dragsForChoiceInGroup:function(a,b){return this.dragsInGroup(b)+".choice"+a},placedDragsInGroup:function(a){return this.dragsInGroup(a)+".placed"},unplacedDragsInGroup:function(a){return this.dragsInGroup(a)+".unplaced"},unplacedDragsForChoiceInGroup:function(a,b){return this.unplacedDragsInGroup(b)+".choice"+a},dragHomes:function(){return a+" span.draghome"},dragHomesGroup:function(b){return a+" .draggrouphomes"+b+" span.draghome"},dragHome:function(b,c){return a+" .draggrouphomes"+b+" span.draghome.choice"+c},drops:function(){return a+" span.drop"},dropForPlace:function(a){return this.drops()+".place"+a},dropsInGroup:function(a){return this.drops()+".group"+a},dropsGroup:function(b){return a+" span.drop.group"+b}}},getInputIds:function(b){var c=[],d=0;return a(b).find("input[type=hidden]").each(function(e,f){var g=parseInt(b.match(/\d/g));a(f)[0].id.indexOf("_"+g+"_p")!==-1&&(c[++d]=a(f)[0].id)}),c},getInputId:function(a,b){return c.getInputIds(a)[b]},init:function(a){var b="qtype_ddwtos-"+Math.random().toString(36).slice(2);M.util.js_pending(b),c.params=a,c.initQuestion(a.topnode),M.util.js_complete(b)},updateOnResize:function(b){a(window).on("resize",function(){c.updateDragsPositions(b)}).resize()},initQuestion:function(b){1!==a(b).data("initialised")&&(c.setPaddingSizesAll(b),c.cloneDragItems(b),c.updateDragsPositions(b),c.updateOnResize(b),c.makeDropZones(b),a(b).data("initialised",1))},putDragInDrop:function(b,d,e){var f=a("input#"+c.getInputId(b,c.getPlace(e)));if(f.val()>0){var g=a(c.cssSelectors(b).dragsForChoiceInGroup(f.val(),c.getGroup(e)));g.length&&c.putBackToOrigin(b,g)}d&&d.length?(f.val(c.getChoice(d)),c.setDragOffset(d,e),d.css("z-index",Number(d.css("z-index"))-1)):(f.val(0),d.removeClass("placed"),d.addClass("unplaced"))},updateDragsPositions:function(b){var d=a(c.cssSelectors(b).drags());d.addClass("unplaced");for(var e=0;ef.rt.X?(a(b).addClass("outside-container"),!0):e>f.lb.Y||df.rb.Y||d>f.rb.X?(a(b).addClass("outside-container"),!0):(a(b).removeClass("outside-container"),!1)},animateEl:function(a,b,c,d){var e=Math.abs(a.css("left")-b.offset().left)+Math.abs(a.css("top")-b.offset().top);e=e>1?e:1,a.animate({opacity:.5,width:1.2*parseInt(a.css("width"))+"px",height:1.2*parseInt(a.css("height"))+"px",top:b.offset().top,left:b.offset().left},{duration:c/e,easing:d}),a.animate({opacity:1,width:parseInt(a.css("width"))+"px",height:parseInt(a.css("height"))+"px",top:b.offset().top,left:b.offset().left},{duration:c/e,easing:d})},isDragCloseToTarget:function(a,b){var d=a.offset().left+a.outerWidth()/2,e=a.offset().top+a.outerHeight()/2,f=b.offset().left+b.outerWidth()/2,g=b.offset().top+b.outerHeight()/2;return d>f-c.DRAG_PROXIMITY&&dg-c.DRAG_PROXIMITY&&e div").each(function(b){a.resizeAllDragsAndDropsInGroup(b+1)})},d.prototype.resizeAllDragsAndDropsInGroup=function(a){var b=this,c=this.getRoot().find(".draggrouphomes"+a+" span.draghome"),d=0,e=0;c.each(function(a,b){d=Math.max(d,Math.ceil(b.offsetWidth)),e=Math.max(e,Math.ceil(0+b.offsetHeight))}),d+=8,e+=2,c.each(function(a,c){b.setElementSize(c,d,e)}),this.getRoot().find("span.drop.group"+a).each(function(a,c){b.setElementSize(c,d,e)})},d.prototype.setElementSize=function(b,c,d){a(b).width(c).height(d).css("lineHeight",d+"px")},d.prototype.cloneDrags=function(){var b=this;this.getRoot().find("span.draghome").each(function(c,d){b.cloneDragsForOneChoice(a(d))})},d.prototype.cloneDragsForOneChoice=function(a){if(a.hasClass("infinite"))for(var b=this.noOfDropsInGroup(this.getGroup(a)),c=0;c1;)c--,d=this.getUnplacedChoice(a,c);return d},d.prototype.animateTo=function(a,b){var c=a.offset(),d=b.offset();a.addClass("beingdragged"),a.animate({left:parseInt(a.css("left"))+d.left-c.left,top:parseInt(a.css("top"))+d.top-c.top},{duration:"fast",done:function(){a.removeClass("beingdragged"),a.offset(d)}})},d.prototype.isPointInDrop=function(a,b,c){var d=c.offset();return a>=d.left&&a=d.top&&b. -/* - * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard. +/** + * JavaScript to make drag-drop into text questions work. * - * @package qtype - * @subpackage ddwtos + * 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'], function ($, dd) { +define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys) { "use strict"; - var t = { - /** - * The accuracy of the drag item in brink of a slot to be accepted. - * @private + /** + * 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); + }); + }; - */ - DRAG_PROXIMITY: 10, + /** + * 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; - /** - * 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, + // 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)); + }); - /** - * Animation time in milliseconds - * @private - */ - ANIMATION_DURATION: 300, + // The size we will want to set is a bit bigger than this. + maxWidth += 8; + maxHeight += 2; - /** - * Params object, Todo: I only need to know whether the question is 'readonly' - * @private - */ - params: {}, + // Set each drag home to that size. + dragHomes.each(function(i, drag) { + thisQ.setElementSize(drag, maxWidth, maxHeight); + }); - nextDragItemNo : 1, + // Set each drop to that size. + this.getRoot().find('span.drop.group' + group).each(function(i, drop) { + thisQ.setElementSize(drop, maxWidth, maxHeight); + }); + }; - /** - * 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; - } - }; - }, + /** + * 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'); + }; - /** - * 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; - }, + /** + * 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)); + }); + }; - /** - * 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; + /** + * 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); } - 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()); + } else { + this.cloneDrag(dragHome); } }; - return t; + + /** + * 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 + }; }); diff --git a/question/type/ddwtos/renderer.php b/question/type/ddwtos/renderer.php index b59e1e41266..a1bbc5fcf7b 100644 --- a/question/type/ddwtos/renderer.php +++ b/question/type/ddwtos/renderer.php @@ -46,17 +46,8 @@ class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_ren $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->js_call_amd('qtype_ddwtos/ddwtos', 'init', array($params)); + $PAGE->requires->js_call_amd('qtype_ddwtos/ddwtos', 'init', + ['q' . $qa->get_slot(), $options->readonly]); return $result; } @@ -72,17 +63,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))); @@ -151,8 +138,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(); @@ -168,30 +159,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; diff --git a/question/type/ddwtos/styles.css b/question/type/ddwtos/styles.css index 201f6c9a224..b0292889543 100644 --- a/question/type/ddwtos/styles.css +++ b/question/type/ddwtos/styles.css @@ -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,10 +35,16 @@ } .que.ddwtos .drag { + position: absolute; z-index: 2; + cursor: move; } -.que.ddwtos .drag.yui3-dd-dragging { +.que.ddwtos .readonly .drag { + cursor: default; +} + +.que.ddwtos .drag.beingdragged { z-index: 3; box-shadow: 3px 3px 4px #000; } @@ -49,18 +54,6 @@ 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; -} - -.que.ddwtos .readonly .drag { - cursor: default; -} .que.ddwtos span.incorrect { background-color: #faa; diff --git a/question/type/ddwtos/tests/behat/preview.feature b/question/type/ddwtos/tests/behat/preview.feature index 46652534a87..33740931e5b 100644 --- a/question/type/ddwtos/tests/behat/preview.feature +++ b/question/type/ddwtos/tests/behat/preview.feature @@ -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 From b51f4f2ececf981fe936fee6c890cdf11f135f24 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Tue, 16 Oct 2018 11:48:54 +0100 Subject: [PATCH 3/3] MDL-63277 qtype_ddwtos: removed unneeded qtype_classname method --- question/type/ddwtos/renderer.php | 4 ---- question/type/gapselect/rendererbase.php | 7 +------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/question/type/ddwtos/renderer.php b/question/type/ddwtos/renderer.php index a1bbc5fcf7b..5e53cbf9331 100644 --- a/question/type/ddwtos/renderer.php +++ b/question/type/ddwtos/renderer.php @@ -36,10 +36,6 @@ 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; diff --git a/question/type/gapselect/rendererbase.php b/question/type/gapselect/rendererbase.php index 0577a219251..3bca12b1c77 100644 --- a/question/type/gapselect/rendererbase.php +++ b/question/type/gapselect/rendererbase.php @@ -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('')); }