mirror of
https://github.com/moodle/moodle.git
synced 2025-03-14 04:30:15 +01:00
Merge branch 'wip-MDL-62411-master' of https://github.com/timhunt/moodle
This commit is contained in:
commit
66de50c366
1
lib/amd/build/dragdrop.min.js
vendored
Normal file
1
lib/amd/build/dragdrop.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
define(["jquery","core/autoscroll"],function(a,b){var c={eventCaptureOptions:{passive:!1,capture:!0},dragProxy:null,onMove:null,onDrop:null,initialPosition:null,initialX:null,initialY:null,touching:null,prepare:function(a){a.preventDefault();var b;if(b="touchstart"===a.type?null===c.touching&&a.changedTouches.length>0:1===a.which){var d=c.getEventXY(a);return d.start=!0,d}return{start:!1}},start:function(a,d,e,f){var g=c.getEventXY(a);switch(c.initialX=g.x,c.initialY=g.y,c.initialPosition=d.offset(),c.dragProxy=d,c.onMove=e,c.onDrop=f,a.type){case"mousedown":c.addEventSpecial("mousemove",c.mouseMove),c.addEventSpecial("mouseup",c.mouseUp);break;case"touchstart":c.addEventSpecial("touchend",c.touchEnd),c.addEventSpecial("touchcancel",c.touchEnd),c.addEventSpecial("touchmove",c.touchMove),c.touching=a.changedTouches[0].identifier;break;default:throw new Error("Unexpected event type: "+a.type)}b.start(c.scroll)},addEventSpecial:function(a,b){try{window.addEventListener(a,b,c.eventCaptureOptions)}catch(d){c.eventCaptureOptions=!0,window.addEventListener(a,b,c.eventCaptureOptions)}},getEventXY:function(a){switch(a.type){case"touchstart":return{x:a.changedTouches[0].pageX,y:a.changedTouches[0].pageY};case"mousedown":return{x:a.pageX,y:a.pageY};default:throw new Error("Unexpected event type: "+a.type)}},touchMove:function(a){a.preventDefault();for(var b=0;b<a.changedTouches.length;b++)a.changedTouches[b].identifier===c.touching&&c.handleMove(a.changedTouches[b].pageX,a.changedTouches[b].pageY)},mouseMove:function(a){c.handleMove(a.pageX,a.pageY)},handleMove:function(b,d){var e=c.dragProxy.offset(),f=e.top-parseInt(c.dragProxy.css("top")),g=e.left-parseInt(c.dragProxy.css("left")),h=a(document).height()-c.dragProxy.outerHeight()-f,i=a(document).width()-c.dragProxy.outerWidth()-g,j=-f,k=-g,l=c.initialPosition,m={top:Math.max(j,Math.min(h,l.top+(d-c.initialY)-f)),left:Math.max(k,Math.min(i,l.left+(b-c.initialX)-g))};c.dragProxy.css(m),c.onMove(b,d,c.dragProxy)},touchEnd:function(a){a.preventDefault();for(var b=0;b<a.changedTouches.length;b++)a.changedTouches[b].identifier===c.touching&&c.handleEnd(a.changedTouches[b].pageX,a.changedTouches[b].pageY)},mouseUp:function(a){c.handleEnd(a.pageX,a.pageY)},handleEnd:function(a,d){null!==c.touching?(window.removeEventListener("touchend",c.touchEnd,c.eventCaptureOptions),window.removeEventListener("touchcancel",c.touchEnd,c.eventCaptureOptions),window.removeEventListener("touchmove",c.touchMove,c.eventCaptureOptions),c.touching=null):(window.removeEventListener("mousemove",c.mouseMove,c.eventCaptureOptions),window.removeEventListener("mouseup",c.mouseUp,c.eventCaptureOptions)),b.stop(),c.onDrop(a,d,c.dragProxy)},scroll:function(b){var d=a(document).height()-c.dragProxy.outerHeight(),e=c.dragProxy.offset();e.top=Math.min(d,e.top+b),c.dragProxy.css(e)}};return{prepare:c.prepare,start:c.start}});
|
@ -203,7 +203,7 @@ define(['jquery'], function($) {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
stop: autoscroll.stop,
|
||||
stop: autoscroll.stop
|
||||
};
|
||||
|
||||
});
|
||||
|
340
lib/amd/src/dragdrop.js
vendored
Normal file
340
lib/amd/src/dragdrop.js
vendored
Normal file
@ -0,0 +1,340 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/*
|
||||
* JavaScript to handle drag operations, including automatic scrolling.
|
||||
*
|
||||
* Note: this module is defined statically. It is a singleton. You
|
||||
* can only have one use of it active at any time. However, you
|
||||
* can only drag one thing at a time, this is not a problem in practice.
|
||||
*
|
||||
* @module core/dragdrop
|
||||
* @class dragdrop
|
||||
* @package core
|
||||
* @copyright 2016 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 3.6
|
||||
*/
|
||||
define(['jquery', 'core/autoscroll'], function($, autoScroll) {
|
||||
/**
|
||||
* @alias module:core/dragdrop
|
||||
*/
|
||||
var dragdrop = {
|
||||
/**
|
||||
* A boolean or options argument depending on whether browser supports passive events.
|
||||
* @private
|
||||
*/
|
||||
eventCaptureOptions: {passive: false, capture: true},
|
||||
|
||||
/**
|
||||
* Drag proxy if any.
|
||||
* @private
|
||||
*/
|
||||
dragProxy: null,
|
||||
|
||||
/**
|
||||
* Function called on move.
|
||||
* @private
|
||||
*/
|
||||
onMove: null,
|
||||
|
||||
/**
|
||||
* Function called on drop.
|
||||
* @private
|
||||
*/
|
||||
onDrop: null,
|
||||
|
||||
/**
|
||||
* Initial position of proxy at drag start.
|
||||
*/
|
||||
initialPosition: null,
|
||||
|
||||
/**
|
||||
* Initial page X of cursor at drag start.
|
||||
*/
|
||||
initialX: null,
|
||||
|
||||
/**
|
||||
* Initial page Y of cursor at drag start.
|
||||
*/
|
||||
initialY: null,
|
||||
|
||||
/**
|
||||
* If touch event is in progress, this will be the id, otherwise null
|
||||
*/
|
||||
touching: null,
|
||||
|
||||
/**
|
||||
* Prepares to begin a drag operation - call with a mousedown or touchstart event.
|
||||
*
|
||||
* If the returned object has 'start' true, then you can set up a drag proxy, and call
|
||||
* start. This function will call preventDefault automatically regardless of whether
|
||||
* starting or not.
|
||||
*
|
||||
* @public
|
||||
* @param {Object} event Event (should be either mousedown or touchstart)
|
||||
* @return {Object} Object with start (boolean flag) and x, y (only if flag true) values
|
||||
*/
|
||||
prepare: function(event) {
|
||||
event.preventDefault();
|
||||
var start;
|
||||
if (event.type === 'touchstart') {
|
||||
// For touch, start if there's at least one touch and we are not currently doing
|
||||
// a touch event.
|
||||
start = (dragdrop.touching === null) && event.changedTouches.length > 0;
|
||||
} else {
|
||||
// For mousedown, start if it's the left button.
|
||||
start = event.which === 1;
|
||||
}
|
||||
if (start) {
|
||||
var details = dragdrop.getEventXY(event);
|
||||
details.start = true;
|
||||
return details;
|
||||
} else {
|
||||
return {start: false};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Call to start a drag operation, in response to a mouse down or touch start event.
|
||||
* Normally call this after calling prepare and receiving start true (you can probably
|
||||
* skip prepare if only supporting drag not touch).
|
||||
*
|
||||
* Note: The caller is responsible for creating a 'drag proxy' which is the
|
||||
* thing that actually gets dragged. At present, this doesn't really work
|
||||
* properly unless it is added directly within the body tag.
|
||||
*
|
||||
* You also need to ensure that there is CSS so the proxy is absolutely positioned,
|
||||
* and styled to look like it is floating.
|
||||
*
|
||||
* You also need to absolutely position the proxy where you want it to start.
|
||||
*
|
||||
* @public
|
||||
* @param {Object} event Event (should be either mousedown or touchstart)
|
||||
* @param {jQuery} dragProxy An absolute-positioned element for dragging
|
||||
* @param {Object} onMove Function that receives X and Y page locations for a move
|
||||
* @param {Object} onDrop Function that receives X and Y page locations when dropped
|
||||
*/
|
||||
start: function(event, dragProxy, onMove, onDrop) {
|
||||
var xy = dragdrop.getEventXY(event);
|
||||
dragdrop.initialX = xy.x;
|
||||
dragdrop.initialY = xy.y;
|
||||
dragdrop.initialPosition = dragProxy.offset();
|
||||
dragdrop.dragProxy = dragProxy;
|
||||
dragdrop.onMove = onMove;
|
||||
dragdrop.onDrop = onDrop;
|
||||
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
// Cannot use jQuery 'on' because events need to not be passive.
|
||||
dragdrop.addEventSpecial('mousemove', dragdrop.mouseMove);
|
||||
dragdrop.addEventSpecial('mouseup', dragdrop.mouseUp);
|
||||
break;
|
||||
case 'touchstart':
|
||||
dragdrop.addEventSpecial('touchend', dragdrop.touchEnd);
|
||||
dragdrop.addEventSpecial('touchcancel', dragdrop.touchEnd);
|
||||
dragdrop.addEventSpecial('touchmove', dragdrop.touchMove);
|
||||
dragdrop.touching = event.changedTouches[0].identifier;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unexpected event type: ' + event.type);
|
||||
}
|
||||
autoScroll.start(dragdrop.scroll);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an event listener with special event capture options (capture, not passive). If the
|
||||
* browser does not support passive events, it will fall back to the boolean for capture.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} event Event type string
|
||||
* @param {Object} handler Handler function
|
||||
*/
|
||||
addEventSpecial: function(event, handler) {
|
||||
try {
|
||||
window.addEventListener(event, handler, dragdrop.eventCaptureOptions);
|
||||
} catch (ex) {
|
||||
dragdrop.eventCaptureOptions = true;
|
||||
window.addEventListener(event, handler, dragdrop.eventCaptureOptions);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets X/Y co-ordinates of an event, which can be either touchstart or mousedown.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} event Event (should be either mousedown or touchstart)
|
||||
* @return {Object} X/Y co-ordinates
|
||||
*/
|
||||
getEventXY: function(event) {
|
||||
switch (event.type) {
|
||||
case 'touchstart':
|
||||
return {x: event.changedTouches[0].pageX,
|
||||
y: event.changedTouches[0].pageY};
|
||||
case 'mousedown':
|
||||
return {x: event.pageX, y: event.pageY};
|
||||
default:
|
||||
throw new Error('Unexpected event type: ' + event.type);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler for touch move.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} e Event
|
||||
*/
|
||||
touchMove: function(e) {
|
||||
e.preventDefault();
|
||||
for (var i = 0; i < e.changedTouches.length; i++) {
|
||||
if (e.changedTouches[i].identifier === dragdrop.touching) {
|
||||
dragdrop.handleMove(e.changedTouches[i].pageX, e.changedTouches[i].pageY);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler for mouse move.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} e Event
|
||||
*/
|
||||
mouseMove: function(e) {
|
||||
dragdrop.handleMove(e.pageX, e.pageY);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shared handler for move event (mouse or touch).
|
||||
*
|
||||
* @private
|
||||
* @param {number} pageX X co-ordinate
|
||||
* @param {number} pageY Y co-ordinate
|
||||
*/
|
||||
handleMove: function(pageX, pageY) {
|
||||
// Move the drag proxy, not letting you move it out of screen or window bounds.
|
||||
var current = dragdrop.dragProxy.offset();
|
||||
var topOffset = current.top - parseInt(dragdrop.dragProxy.css('top'));
|
||||
var leftOffset = current.left - parseInt(dragdrop.dragProxy.css('left'));
|
||||
var maxY = $(document).height() - dragdrop.dragProxy.outerHeight() - topOffset;
|
||||
var maxX = $(document).width() - dragdrop.dragProxy.outerWidth() - leftOffset;
|
||||
var minY = -topOffset;
|
||||
var minX = -leftOffset;
|
||||
var initial = dragdrop.initialPosition;
|
||||
var position = {
|
||||
top: Math.max(minY, Math.min(maxY, initial.top + (pageY - dragdrop.initialY) - topOffset)),
|
||||
left: Math.max(minX, Math.min(maxX, initial.left + (pageX - dragdrop.initialX) - leftOffset))
|
||||
};
|
||||
dragdrop.dragProxy.css(position);
|
||||
|
||||
// Trigger move handler.
|
||||
dragdrop.onMove(pageX, pageY, dragdrop.dragProxy);
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler for touch end.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} e Event
|
||||
*/
|
||||
touchEnd: function(e) {
|
||||
e.preventDefault();
|
||||
for (var i = 0; i < e.changedTouches.length; i++) {
|
||||
if (e.changedTouches[i].identifier === dragdrop.touching) {
|
||||
dragdrop.handleEnd(e.changedTouches[i].pageX, e.changedTouches[i].pageY);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler for mouse up.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} e Event
|
||||
*/
|
||||
mouseUp: function(e) {
|
||||
dragdrop.handleEnd(e.pageX, e.pageY);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shared handler for end drag (mouse or touch).
|
||||
*
|
||||
* @private
|
||||
* @param {number} pageX X
|
||||
* @param {number} pageY Y
|
||||
*/
|
||||
handleEnd: function(pageX, pageY) {
|
||||
if (dragdrop.touching !== null) {
|
||||
window.removeEventListener('touchend', dragdrop.touchEnd, dragdrop.eventCaptureOptions);
|
||||
window.removeEventListener('touchcancel', dragdrop.touchEnd, dragdrop.eventCaptureOptions);
|
||||
window.removeEventListener('touchmove', dragdrop.touchMove, dragdrop.eventCaptureOptions);
|
||||
dragdrop.touching = null;
|
||||
} else {
|
||||
window.removeEventListener('mousemove', dragdrop.mouseMove, dragdrop.eventCaptureOptions);
|
||||
window.removeEventListener('mouseup', dragdrop.mouseUp, dragdrop.eventCaptureOptions);
|
||||
}
|
||||
autoScroll.stop();
|
||||
dragdrop.onDrop(pageX, pageY, dragdrop.dragProxy);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the page scrolls.
|
||||
*
|
||||
* @private
|
||||
* @param {number} offset Amount of scroll
|
||||
*/
|
||||
scroll: function(offset) {
|
||||
// Move the proxy to match.
|
||||
var maxY = $(document).height() - dragdrop.dragProxy.outerHeight();
|
||||
var currentPosition = dragdrop.dragProxy.offset();
|
||||
currentPosition.top = Math.min(maxY, currentPosition.top + offset);
|
||||
dragdrop.dragProxy.css(currentPosition);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Prepares to begin a drag operation - call with a mousedown or touchstart event.
|
||||
*
|
||||
* If the returned object has 'start' true, then you can set up a drag proxy, and call
|
||||
* start. This function will call preventDefault automatically regardless of whether
|
||||
* starting or not.
|
||||
*
|
||||
* @param {Object} event Event (should be either mousedown or touchstart)
|
||||
* @return {Object} Object with start (boolean flag) and x, y (only if flag true) values
|
||||
*/
|
||||
prepare: dragdrop.prepare,
|
||||
|
||||
/**
|
||||
* Call to start a drag operation, in response to a mouse down or touch start event.
|
||||
* Normally call this after calling prepare and receiving start true (you can probably
|
||||
* skip prepare if only supporting drag not touch).
|
||||
*
|
||||
* Note: The caller is responsible for creating a 'drag proxy' which is the
|
||||
* thing that actually gets dragged. At present, this doesn't really work
|
||||
* properly unless it is added directly within the body tag.
|
||||
*
|
||||
* You also need to ensure that there is CSS so the proxy is absolutely positioned,
|
||||
* and styled to look like it is floating.
|
||||
*
|
||||
* You also need to absolutely position the proxy where you want it to start.
|
||||
*
|
||||
* @param {Object} event Event (should be either mousedown or touchstart)
|
||||
* @param {jQuery} dragProxy An absolute-positioned element for dragging
|
||||
* @param {Object} onMove Function that receives X and Y page locations for a move
|
||||
* @param {Object} onDrop Function that receives X and Y page locations when dropped
|
||||
*/
|
||||
start: dragdrop.start
|
||||
};
|
||||
});
|
1
question/type/ddmarker/amd/build/form.min.js
vendored
Normal file
1
question/type/ddmarker/amd/build/form.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
question/type/ddmarker/amd/build/question.min.js
vendored
Normal file
1
question/type/ddmarker/amd/build/question.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
question/type/ddmarker/amd/build/shapes.min.js
vendored
Normal file
1
question/type/ddmarker/amd/build/shapes.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
745
question/type/ddmarker/amd/src/form.js
Normal file
745
question/type/ddmarker/amd/src/form.js
Normal file
@ -0,0 +1,745 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* This class provides the enhancements to the drag-drop marker editing form.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @subpackage form
|
||||
* @copyright 2018 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes'], function($, dragDrop, Shapes) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Create the manager object that deals with keeping everything synchronised for one drop zone.
|
||||
*
|
||||
* @param {int} dropzoneNo the index of this drop zone in the form. 0, 1, ....
|
||||
* @constructor
|
||||
*/
|
||||
function DropZoneManager(dropzoneNo) {
|
||||
this.dropzoneNo = dropzoneNo;
|
||||
this.svgEl = null;
|
||||
|
||||
this.shape = Shapes.make(this.getShapeType(), this.getLabel());
|
||||
this.updateCoordinatesFromForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the coordinates from a particular string.
|
||||
*
|
||||
* @param {SVGElement} [svg] the SVG element that is the preview.
|
||||
*/
|
||||
DropZoneManager.prototype.updateCoordinatesFromForm = function(svg) {
|
||||
var coordinates = this.getCoordinates(),
|
||||
currentNumPoints = this.shape.getType() === 'polygon' && this.shape.points.length;
|
||||
if (this.shape.getCoordinates() === coordinates) {
|
||||
return;
|
||||
}
|
||||
if (!this.shape.parse(coordinates)) {
|
||||
// Invalid coordinates. Don't update the preview.
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shape.getType() === 'polygon' && currentNumPoints !== this.shape.points.length) {
|
||||
// Polygon, and size has changed.
|
||||
var currentyActive = this.isActive();
|
||||
this.removeFromSvg();
|
||||
if (svg) {
|
||||
this.addToSvg(svg);
|
||||
if (currentyActive) {
|
||||
this.setActive();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Simple update.
|
||||
this.updateSvgEl();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the label.
|
||||
*/
|
||||
DropZoneManager.prototype.updateLabel = function() {
|
||||
var label = this.getLabel();
|
||||
if (this.shape.label !== label) {
|
||||
this.shape.label = label;
|
||||
this.updateSvgEl();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle if the type of shape has changed.
|
||||
*
|
||||
* @param {SVGElement} [svg] an SVG element to add this new shape to.
|
||||
*/
|
||||
DropZoneManager.prototype.changeShape = function(svg) {
|
||||
var newShapeType = this.getShapeType(),
|
||||
currentyActive = this.isActive();
|
||||
|
||||
if (newShapeType === this.shape.getType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It has really changed.
|
||||
this.removeFromSvg();
|
||||
this.shape = Shapes.getSimilar(newShapeType, this.shape);
|
||||
if (svg) {
|
||||
this.addToSvg(svg);
|
||||
if (currentyActive) {
|
||||
this.setActive();
|
||||
}
|
||||
}
|
||||
this.setCoordinatesInForm();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add this drop zone to an SVG graphic.
|
||||
*
|
||||
* @param {SVGElement} svg the SVG image to which to add this drop zone.
|
||||
*/
|
||||
DropZoneManager.prototype.addToSvg = function(svg) {
|
||||
if (this.svgEl !== null) {
|
||||
throw new Error('this.svgEl already set');
|
||||
}
|
||||
this.svgEl = this.shape.makeSvg(svg);
|
||||
if (!this.svgEl) {
|
||||
return;
|
||||
}
|
||||
this.svgEl.setAttribute('class', 'dropzone');
|
||||
this.svgEl.setAttribute('data-dropzone-no', this.dropzoneNo);
|
||||
|
||||
// Add handles.
|
||||
var handles = this.shape.getHandlePositions();
|
||||
if (handles === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var moveHandle = Shapes.createSvgElement(this.svgEl, 'circle');
|
||||
moveHandle.setAttribute('cx', handles.moveHandle.x);
|
||||
moveHandle.setAttribute('cy', handles.moveHandle.y);
|
||||
moveHandle.setAttribute('r', 7);
|
||||
moveHandle.setAttribute('class', 'handle move');
|
||||
|
||||
for (var i = 0; i < handles.editHandles.length; ++i) {
|
||||
this.makeEditHandle(i, handles.editHandles[i]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new edit handle.
|
||||
*
|
||||
* @param {int} index the handle index.
|
||||
* @param {Point} point the point at which to add the handle.
|
||||
*/
|
||||
DropZoneManager.prototype.makeEditHandle = function(index, point) {
|
||||
var editHandle = Shapes.createSvgElement(this.svgEl, 'rect');
|
||||
editHandle.setAttribute('x', point.x - 6);
|
||||
editHandle.setAttribute('y', point.y - 6);
|
||||
editHandle.setAttribute('width', 11);
|
||||
editHandle.setAttribute('height', 11);
|
||||
editHandle.setAttribute('class', 'handle edit');
|
||||
editHandle.setAttribute('data-edit-handle-no', index);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove this drop zone from an SVG image.
|
||||
*/
|
||||
DropZoneManager.prototype.removeFromSvg = function() {
|
||||
if (this.svgEl !== null) {
|
||||
this.svgEl.parentNode.removeChild(this.svgEl);
|
||||
this.svgEl = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the shape of this drop zone (but not type) in an SVG image.
|
||||
*/
|
||||
DropZoneManager.prototype.updateSvgEl = function() {
|
||||
if (this.svgEl === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shape.updateSvg(this.svgEl);
|
||||
|
||||
// Adjust handles.
|
||||
var handles = this.shape.getHandlePositions();
|
||||
if (handles === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move handle.
|
||||
// The shape + its label are the first two children of svgEl.
|
||||
// Then come the move handle followed by the edit handles.
|
||||
this.svgEl.childNodes[2].setAttribute('cx', handles.moveHandle.x);
|
||||
this.svgEl.childNodes[2].setAttribute('cy', handles.moveHandle.y);
|
||||
|
||||
// Edit handles.
|
||||
for (var i = 0; i < handles.editHandles.length; ++i) {
|
||||
this.svgEl.childNodes[3 + i].setAttribute('x', handles.editHandles[i].x - 6);
|
||||
this.svgEl.childNodes[3 + i].setAttribute('y', handles.editHandles[i].y - 6);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find out of this drop zone is currently being edited.
|
||||
*
|
||||
* @return {boolean} true if it is.
|
||||
*/
|
||||
DropZoneManager.prototype.isActive = function() {
|
||||
return this.svgEl !== null && this.svgEl.getAttribute('class').match(/\bactive\b/);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set this drop zone as being edited.
|
||||
*/
|
||||
DropZoneManager.prototype.setActive = function() {
|
||||
// Move this one to last, so that it is always on top.
|
||||
// (Otherwise the handles may not be able to receive events.)
|
||||
var parent = this.svgEl.parentNode;
|
||||
parent.removeChild(this.svgEl);
|
||||
parent.appendChild(this.svgEl);
|
||||
this.svgEl.setAttribute('class', this.svgEl.getAttribute('class') + ' active');
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the coordinates in the form to match the current shape.
|
||||
*/
|
||||
DropZoneManager.prototype.setCoordinatesInForm = function() {
|
||||
dragDropForm.form.setFormValue('drops', [this.dropzoneNo, 'coords'], this.shape.getCoordinates());
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the coordinates for a drop zone from the text input in the form.
|
||||
* @returns {string} the coordinates.
|
||||
*/
|
||||
DropZoneManager.prototype.getCoordinates = function() {
|
||||
return dragDropForm.form.getFormValue('drops', [this.dropzoneNo, 'coords']).replace(/\s*/g, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the selected marker number from the dropdown in the form.
|
||||
* @returns {int} choice number.
|
||||
*/
|
||||
DropZoneManager.prototype.getChoiceNo = function() {
|
||||
return dragDropForm.form.getFormValue('drops', [this.dropzoneNo, 'choice']);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the selected marker number from the dropdown in the form.
|
||||
* @returns {String} marker label text.
|
||||
*/
|
||||
DropZoneManager.prototype.getLabel = function() {
|
||||
return dragDropForm.form.getMarkerText(this.getChoiceNo());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the selected type of shape in the form.
|
||||
* @returns {String} 'circle', 'rectangle' or 'polygon'.
|
||||
*/
|
||||
DropZoneManager.prototype.getShapeType = function() {
|
||||
return dragDropForm.form.getFormValue('drops', [this.dropzoneNo, 'shape']);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start responding to dragging the move handle.
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
DropZoneManager.prototype.handleMove = function(e) {
|
||||
var info = dragDrop.prepare(e);
|
||||
if (!info.start) {
|
||||
return;
|
||||
}
|
||||
|
||||
var movingDropZone = this,
|
||||
lastX = info.x,
|
||||
lastY = info.y,
|
||||
dragProxy = this.makeDragProxy(info.x, info.y),
|
||||
bgImg = $('fieldset#id_previewareaheader .dropbackground'),
|
||||
maxX = bgImg.width(),
|
||||
maxY = bgImg.height();
|
||||
|
||||
dragDrop.start(e, $(dragProxy), function(pageX, pageY) {
|
||||
movingDropZone.shape.move(pageX - lastX, pageY - lastY, maxX, maxY);
|
||||
lastX = pageX;
|
||||
lastY = pageY;
|
||||
movingDropZone.updateSvgEl();
|
||||
movingDropZone.setCoordinatesInForm();
|
||||
}, function() {
|
||||
document.body.removeChild(dragProxy);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Start responding to dragging the move handle.
|
||||
* @param {Event} e Event object
|
||||
* @param {int} handleIndex
|
||||
* @param {SVGElement} [svg] an SVG element to add this new shape to.
|
||||
*/
|
||||
DropZoneManager.prototype.handleEdit = function(e, handleIndex, svg) {
|
||||
var info = dragDrop.prepare(e);
|
||||
if (!info.start) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For polygons, CTRL + drag adds a new point.
|
||||
if (this.shape.getType() === 'polygon' && (e.ctrlKey || e.metaKey)) {
|
||||
this.shape.addNewPointAfter(handleIndex);
|
||||
this.removeFromSvg();
|
||||
this.addToSvg(svg);
|
||||
this.setActive();
|
||||
}
|
||||
|
||||
var changingDropZone = this,
|
||||
lastX = info.x,
|
||||
lastY = info.y,
|
||||
dragProxy = this.makeDragProxy(info.x, info.y),
|
||||
bgImg = $('fieldset#id_previewareaheader .dropbackground'),
|
||||
maxX = bgImg.width(),
|
||||
maxY = bgImg.height();
|
||||
|
||||
dragDrop.start(e, $(dragProxy), function(pageX, pageY) {
|
||||
changingDropZone.shape.edit(handleIndex, pageX - lastX, pageY - lastY, maxX, maxY);
|
||||
lastX = pageX;
|
||||
lastY = pageY;
|
||||
changingDropZone.updateSvgEl();
|
||||
changingDropZone.setCoordinatesInForm();
|
||||
}, function() {
|
||||
document.body.removeChild(dragProxy);
|
||||
changingDropZone.shape.normalizeShape();
|
||||
changingDropZone.updateSvgEl();
|
||||
changingDropZone.setCoordinatesInForm();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Make an invisible drag proxy.
|
||||
*
|
||||
* @param {int} x x position .
|
||||
* @param {int} y y position.
|
||||
* @returns {HTMLElement} the drag proxy.
|
||||
*/
|
||||
DropZoneManager.prototype.makeDragProxy = function(x, y) {
|
||||
var dragProxy = document.createElement('div');
|
||||
dragProxy.style.position = 'absolute';
|
||||
dragProxy.style.top = y + 'px';
|
||||
dragProxy.style.left = x + 'px';
|
||||
dragProxy.style.width = '1px';
|
||||
dragProxy.style.height = '1px';
|
||||
document.body.appendChild(dragProxy);
|
||||
return dragProxy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton object for managing all the parts of the form.
|
||||
*/
|
||||
var dragDropForm = {
|
||||
|
||||
/**
|
||||
* @var {object} with properties width and height.
|
||||
*/
|
||||
maxSizes: null, // Object containing maximum sizes for the background image.
|
||||
|
||||
/**
|
||||
* @var {object} for interacting with the file pickers.
|
||||
*/
|
||||
fp: null, // Object containing functions associated with the file picker.
|
||||
|
||||
/**
|
||||
* @var {int} the number of drop-zones on the form.
|
||||
*/
|
||||
noDropZones: null,
|
||||
|
||||
/**
|
||||
* @var {DropZoneManager[]} the drop zones in the preview, indexed by drop zone number.
|
||||
*/
|
||||
dropZones: [],
|
||||
|
||||
/**
|
||||
* Initialise the form.
|
||||
*
|
||||
* @param {Object} maxBgimageSize object with two properties width and height.
|
||||
*/
|
||||
init: function(maxBgimageSize) {
|
||||
dragDropForm.maxSizes = maxBgimageSize;
|
||||
dragDropForm.fp = dragDropForm.filePickers();
|
||||
dragDropForm.noDropZones = dragDropForm.form.getFormValue('nodropzone', []);
|
||||
dragDropForm.setupPreviewArea();
|
||||
dragDropForm.setOptionsForDragItemSelectors();
|
||||
dragDropForm.createShapes();
|
||||
dragDropForm.setupEventHandlers();
|
||||
dragDropForm.waitForFilePickerToInitialise();
|
||||
},
|
||||
|
||||
/**
|
||||
* Add html for the preview area.
|
||||
*/
|
||||
setupPreviewArea: function() {
|
||||
$('fieldset#id_previewareaheader div.fcontainer').append(
|
||||
'<div class="ddarea que ddmarker">' +
|
||||
' <div id="ddm-droparea" class="droparea">' +
|
||||
' <img class="dropbackground" />' +
|
||||
' <div id="ddm-dropzone" class="dropzones">' +
|
||||
' </div>' +
|
||||
' </div>' +
|
||||
'</div>');
|
||||
},
|
||||
|
||||
/**
|
||||
* When a new marker is added this function updates the Marker dropdown controls in Drop zones.
|
||||
*/
|
||||
setOptionsForDragItemSelectors: function() {
|
||||
var dragItemsOptions = {'0': ''};
|
||||
var noItems = dragDropForm.form.getFormValue('noitems', []);
|
||||
var selectedValues = [];
|
||||
var selector;
|
||||
var i, label;
|
||||
for (i = 1; i <= noItems; i++) {
|
||||
label = dragDropForm.form.getMarkerText(i);
|
||||
if (label !== "") {
|
||||
// HTML escape the label.
|
||||
dragItemsOptions[i] = $('<div/>').text(label).html();
|
||||
}
|
||||
}
|
||||
// Get all the currently selected drags for each drop.
|
||||
for (i = 0; i < dragDropForm.noDropZones; i++) {
|
||||
selector = $('#id_drops_' + i + '_choice');
|
||||
selectedValues[i] = Number(selector.val());
|
||||
}
|
||||
for (i = 0; i < dragDropForm.noDropZones; i++) {
|
||||
selector = $('#id_drops_' + i + '_choice');
|
||||
// Remove all options for drag choice.
|
||||
selector.find('option').remove();
|
||||
// And recreate the options.
|
||||
for (var value in dragItemsOptions) {
|
||||
value = Number(value);
|
||||
var option = '<option value="' + value + '">' + dragItemsOptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.find('option[value="' + value + '"]');
|
||||
|
||||
|
||||
if (value === 0) {
|
||||
continue; // The 'no item' option is always selectable.
|
||||
}
|
||||
|
||||
// Is this the currently selected value?
|
||||
if (value === selectedValues[i]) {
|
||||
optionnode.attr('selected', true);
|
||||
continue; // If it s selected, we must leave it enabled.
|
||||
}
|
||||
|
||||
// Count how many times it is used, and if necessary, disable.
|
||||
var noofdrags = dragDropForm.form.getFormValue('drags', [value - 1, 'noofdrags']);
|
||||
if (Number(noofdrags) === 0) { // 'noofdrags === 0' means infinite.
|
||||
continue; // Nothing to check.
|
||||
}
|
||||
|
||||
// Go through all selected values in drop downs.
|
||||
for (var k in selectedValues) {
|
||||
if (Number(selectedValues[k]) !== value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count down 'noofdrags' and if reach zero then set disabled option for this drag item.
|
||||
if (Number(noofdrags) === 1) {
|
||||
optionnode.attr('disabled', true);
|
||||
break;
|
||||
} else {
|
||||
noofdrags--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dragDropForm.dropZones.length > 0) {
|
||||
dragDropForm.dropZones[i].updateLabel();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the shape representation of each dropZone.
|
||||
*/
|
||||
createShapes: function() {
|
||||
for (var dropzoneNo = 0; dropzoneNo < dragDropForm.noDropZones; dropzoneNo++) {
|
||||
dragDropForm.dropZones[dropzoneNo] = new DropZoneManager(dropzoneNo);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Events linked to form actions.
|
||||
*/
|
||||
setupEventHandlers: function() {
|
||||
// Changes to labels in the Markers section.
|
||||
$('fieldset#id_draggableitemheader').on('change input', 'input, select', function() {
|
||||
dragDropForm.setOptionsForDragItemSelectors();
|
||||
});
|
||||
|
||||
// Changes to Drop zones section: shape, coordinates and marker.
|
||||
$('fieldset#id_dropzoneheader').on('change input', 'input, select', function(e) {
|
||||
var ids = e.currentTarget.name.match(/^drops\[(\d+)]\[([a-z]*)]$/);
|
||||
if (!ids) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dropzoneNo = ids[1],
|
||||
inputType = ids[2],
|
||||
dropZone = dragDropForm.dropZones[dropzoneNo];
|
||||
|
||||
switch (inputType) {
|
||||
case 'shape':
|
||||
dropZone.changeShape(dragDropForm.form.getSvg());
|
||||
break;
|
||||
|
||||
case 'coords':
|
||||
dropZone.updateCoordinatesFromForm(dragDropForm.form.getSvg());
|
||||
break;
|
||||
|
||||
case 'choice':
|
||||
dropZone.updateLabel();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Click to toggle graphical editing.
|
||||
var previewArea = $('fieldset#id_previewareaheader');
|
||||
previewArea.on('click', 'g.dropzone', function(e) {
|
||||
var dropzoneNo = $(e.currentTarget).data('dropzone-no'),
|
||||
currentlyActive = dragDropForm.dropZones[dropzoneNo].isActive();
|
||||
|
||||
$(dragDropForm.form.getSvg()).find('.dropzone.active').removeClass('active');
|
||||
|
||||
if (!currentlyActive) {
|
||||
dragDropForm.dropZones[dropzoneNo].setActive();
|
||||
}
|
||||
});
|
||||
|
||||
// Drag start on a move handle.
|
||||
previewArea.on('mousedown touchstart', '.dropzone .handle.move', function(e) {
|
||||
var dropzoneNo = $(e.currentTarget).closest('g').data('dropzoneNo');
|
||||
|
||||
dragDropForm.dropZones[dropzoneNo].handleMove(e);
|
||||
});
|
||||
|
||||
// Drag start on a move handle.
|
||||
previewArea.on('mousedown touchstart', '.dropzone .handle.edit', function(e) {
|
||||
var dropzoneNo = $(e.currentTarget).closest('g').data('dropzoneNo'),
|
||||
handleIndex = e.currentTarget.getAttribute('data-edit-handle-no');
|
||||
|
||||
dragDropForm.dropZones[dropzoneNo].handleEdit(e, handleIndex, dragDropForm.form.getSvg());
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevents adding drop zones until the preview background image is ready to load.
|
||||
*/
|
||||
waitForFilePickerToInitialise: function() {
|
||||
if (dragDropForm.fp.file('bgimage').href === null) {
|
||||
// It would be better to use an onload or onchange event rather than this timeout.
|
||||
// Unfortunately attempts to do this early are overwritten by filepicker during its loading.
|
||||
setTimeout(dragDropForm.waitForFilePickerToInitialise, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// From now on, when a new file gets loaded into the filepicker, update the preview.
|
||||
// This is not in the setupEventHandlers section as it needs to be delayed until
|
||||
// after filepicker's javascript has finished.
|
||||
$('form.mform').on('change', '#id_bgimage', dragDropForm.loadPreviewImage);
|
||||
|
||||
dragDropForm.loadPreviewImage();
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the preview background image.
|
||||
*/
|
||||
loadPreviewImage: function() {
|
||||
$('fieldset#id_previewareaheader .dropbackground')
|
||||
.one('load', dragDropForm.afterPreviewImageLoaded)
|
||||
.attr('src', dragDropForm.fp.file('bgimage').href);
|
||||
},
|
||||
|
||||
/**
|
||||
* Functions to run after background image loaded.
|
||||
*/
|
||||
afterPreviewImageLoaded: function() {
|
||||
var bgImg = $('fieldset#id_previewareaheader .dropbackground');
|
||||
dragDropForm.constrainImageSize();
|
||||
// Place the dropzone area over the background image (adding one to account for the border).
|
||||
$('#ddm-dropzone').css('position', 'relative').css('top', (bgImg.height() + 1) * -1);
|
||||
$('#ddm-droparea').css('height', bgImg.height() + 20);
|
||||
dragDropForm.updateSvgDisplay();
|
||||
},
|
||||
|
||||
/**
|
||||
* Limits the background image display size.
|
||||
*/
|
||||
constrainImageSize: function() {
|
||||
var bgImg = $('fieldset#id_previewareaheader .dropbackground');
|
||||
var reduceby = Math.max(bgImg.width() / dragDropForm.maxSizes.width,
|
||||
bgImg.height() / dragDropForm.maxSizes.height);
|
||||
if (reduceby > 1) {
|
||||
bgImg.css('width', Math.floor(bgImg.width() / reduceby));
|
||||
}
|
||||
bgImg.addClass('constrained');
|
||||
},
|
||||
|
||||
/**
|
||||
* Draws or re-draws all dropzones in the preview area based on form data.
|
||||
* Call this function when there is a change in the form data.
|
||||
*/
|
||||
updateSvgDisplay: function() {
|
||||
var bgImg = $('fieldset#id_previewareaheader .dropbackground'),
|
||||
dropzoneNo;
|
||||
|
||||
if (dragDropForm.form.getSvg()) {
|
||||
// Already exists, just need to be updated.
|
||||
for (dropzoneNo = 0; dropzoneNo < dragDropForm.noDropZones; dropzoneNo++) {
|
||||
dragDropForm.dropZones[dropzoneNo].updateSvgEl();
|
||||
}
|
||||
|
||||
} else {
|
||||
// Create.
|
||||
$('#ddm-dropzone').html('<svg xmlns="http://www.w3.org/2000/svg" class="dropzones" ' +
|
||||
'width="' + bgImg.outerWidth() + '" ' +
|
||||
'height="' + bgImg.outerHeight() + '"></svg>');
|
||||
for (dropzoneNo = 0; dropzoneNo < dragDropForm.noDropZones; dropzoneNo++) {
|
||||
dragDropForm.dropZones[dropzoneNo].addToSvg(dragDropForm.form.getSvg());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper to make it easy to work with form elements with names like "drops[0][shape]".
|
||||
*/
|
||||
form: {
|
||||
/**
|
||||
* Returns the label text for a marker.
|
||||
* @param {int} markerNo
|
||||
* @returns {string} Marker text
|
||||
*/
|
||||
getMarkerText: function(markerNo) {
|
||||
if (Number(markerNo) !== 0) {
|
||||
var label = dragDropForm.form.getFormValue('drags', [markerNo - 1, 'label']);
|
||||
return label.replace(new RegExp("^\\s*(.*)\\s*$"), "$1");
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the SVG element, if there is one, otherwise return null.
|
||||
*
|
||||
* @returns {SVGElement|null} the SVG element or null.
|
||||
*/
|
||||
getSvg: function() {
|
||||
var svg = $('fieldset#id_previewareaheader svg');
|
||||
if (svg.length === 0) {
|
||||
return null;
|
||||
} else {
|
||||
return svg[0];
|
||||
}
|
||||
},
|
||||
|
||||
toNameWithIndex: function(name, indexes) {
|
||||
var indexString = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexString = indexString + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexString;
|
||||
},
|
||||
|
||||
getEl: function(name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.toNameWithIndex(name, indexes)];
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper to get the value of a form elements with name like "drops[0][shape]".
|
||||
*
|
||||
* @param {String} name the base name, e.g. 'drops'.
|
||||
* @param {String[]} indexes the indexes, e.g. ['0', 'shape'].
|
||||
* @return {String} the value of that field.
|
||||
*/
|
||||
getFormValue: function(name, indexes) {
|
||||
var el = this.getEl(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper to get the value of a form elements with name like "drops[0][shape]".
|
||||
*
|
||||
* @param {String} name the base name, e.g. 'drops'.
|
||||
* @param {String[]} indexes the indexes, e.g. ['0', 'shape'].
|
||||
* @param {String} value the value to set.
|
||||
*/
|
||||
setFormValue: function(name, indexes, value) {
|
||||
var el = this.getEl(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Utility to get the file name and url from the filepicker.
|
||||
* @returns {Object} object containing functions {file, name}
|
||||
*/
|
||||
filePickers: function() {
|
||||
var draftItemIdsToName;
|
||||
var nameToParentNode;
|
||||
if (draftItemIdsToName === undefined) {
|
||||
draftItemIdsToName = {};
|
||||
nameToParentNode = {};
|
||||
$('form.mform input.filepickerhidden').each(function(key, filepicker) {
|
||||
draftItemIdsToName[filepicker.value] = filepicker.name;
|
||||
nameToParentNode[filepicker.name] = filepicker.parentNode;
|
||||
});
|
||||
}
|
||||
return {
|
||||
file: function(name) {
|
||||
var fileAnchor = $(nameToParentNode[name]).find('div.filepicker-filelist a');
|
||||
if (fileAnchor.length) {
|
||||
return {href: fileAnchor.get(0).href, name: fileAnchor.get(0).innerHTML};
|
||||
} else {
|
||||
return {href: null, name: null};
|
||||
}
|
||||
},
|
||||
name: function(draftitemid) {
|
||||
return draftItemIdsToName[draftitemid];
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @alias module:qtype_ddmarker/form
|
||||
*/
|
||||
return {
|
||||
/**
|
||||
* Initialise the form javascript features.
|
||||
* @param {Object} maxBgimageSize object with two properties: width and height.
|
||||
*/
|
||||
init: dragDropForm.init
|
||||
};
|
||||
});
|
597
question/type/ddmarker/amd/src/question.js
Normal file
597
question/type/ddmarker/amd/src/question.js
Normal file
@ -0,0 +1,597 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Question class for drag and drop marker question type, used to support the question and preview pages.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @subpackage question
|
||||
* @copyright 2018 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], function($, dragDrop, Shapes, keys) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Object to handle one drag-drop markers question.
|
||||
*
|
||||
* @param {String} containerId id of the outer div for this question.
|
||||
* @param {String} bgImgUrl the URL of the background image.
|
||||
* @param {boolean} readOnly whether the question is being displayed read-only.
|
||||
* @param {Object[]} visibleDropZones the geometry of any drop-zones to show.
|
||||
* Objects have fields shape, coords and markertext.
|
||||
* @constructor
|
||||
*/
|
||||
function DragDropMarkersQuestion(containerId, bgImgUrl, readOnly, visibleDropZones) {
|
||||
this.containerId = containerId;
|
||||
this.visibleDropZones = visibleDropZones;
|
||||
if (readOnly) {
|
||||
this.getRoot().addClass('qtype_ddmarker-readonly');
|
||||
}
|
||||
this.loadImage(bgImgUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the background image is loaded, then do the rest of the display.
|
||||
*
|
||||
* @param {String} bgImgUrl the URL of the background image.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.loadImage = function(bgImgUrl) {
|
||||
var thisQ = this;
|
||||
this.getRoot().find('.dropbackground')
|
||||
.one('load', function() {
|
||||
if (thisQ.visibleDropZones.length > 0) {
|
||||
thisQ.drawDropzones();
|
||||
}
|
||||
thisQ.repositionDrags();
|
||||
})
|
||||
.attr('src', bgImgUrl)
|
||||
.css({'border': '1px solid #000', 'max-width': 'none'});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws the svg shapes of any drop zones that should be visible for feedback purposes.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.drawDropzones = function() {
|
||||
var bgImage = this.getRoot().find('img.dropbackground');
|
||||
|
||||
this.getRoot().find('div.dropzones').html('<svg xmlns="http://www.w3.org/2000/svg" class="dropzones" ' +
|
||||
'width="' + bgImage.outerWidth() + '" ' +
|
||||
'height="' + bgImage.outerHeight() + '"></svg>');
|
||||
var svg = this.getRoot().find('svg.dropzones');
|
||||
svg.css('position', 'absolute');
|
||||
|
||||
var nextColourIndex = 0;
|
||||
for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {
|
||||
var colourClass = 'color' + nextColourIndex;
|
||||
nextColourIndex = (nextColourIndex + 1) % 8;
|
||||
this.addDropzone(svg, dropZoneNo, colourClass);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a dropzone shape with colour, coords and link provided to the array of shapes.
|
||||
*
|
||||
* @param {jQuery} svg the SVG image to which to add this drop zone.
|
||||
* @param {int} dropZoneNo which drop-zone to add.
|
||||
* @param {string} colourClass class name
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {
|
||||
var dropZone = this.visibleDropZones[dropZoneNo],
|
||||
shape = Shapes.make(dropZone.shape, ''),
|
||||
existingmarkertext;
|
||||
if (!shape.parse(dropZone.coords)) {
|
||||
return;
|
||||
}
|
||||
|
||||
existingmarkertext = this.getRoot().find('div.markertexts span.markertext' + dropZoneNo);
|
||||
if (existingmarkertext.length) {
|
||||
if (dropZone.markertext !== '') {
|
||||
existingmarkertext.html(dropZone.markertext);
|
||||
} else {
|
||||
existingmarkertext.remove();
|
||||
}
|
||||
} else if (dropZone.markertext !== '') {
|
||||
var classnames = 'markertext markertext' + dropZoneNo;
|
||||
this.getRoot().find('div.markertexts').append('<span class="' + classnames + '">' +
|
||||
dropZone.markertext + '</span>');
|
||||
}
|
||||
|
||||
var shapeSVG = shape.makeSvg(svg[0]);
|
||||
shapeSVG.setAttribute('class', 'dropzone ' + colourClass);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws the drag items on the page (and drop zones if required).
|
||||
* The idea is to re-draw all the drags and drops whenever there is a change
|
||||
* like a widow resize or an item dropped in place.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.repositionDropZones = function() {
|
||||
var svg = this.getRoot().find('svg.dropzones');
|
||||
if (svg.length === 0) {
|
||||
return;
|
||||
}
|
||||
var bgPosition = this.convertToWindowXY(new Shapes.Point(-1, 0));
|
||||
svg.offset({'left': bgPosition.x, 'top': bgPosition.y});
|
||||
|
||||
for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {
|
||||
var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);
|
||||
if (markerspan.length === 0) {
|
||||
continue;
|
||||
}
|
||||
var dropZone = this.visibleDropZones[dropZoneNo],
|
||||
shape = Shapes.make(dropZone.shape, '');
|
||||
if (!shape.parse(dropZone.coords)) {
|
||||
continue;
|
||||
}
|
||||
var handles = shape.getHandlePositions(),
|
||||
textPos = this.convertToWindowXY(handles.moveHandle.offset(
|
||||
-markerspan.outerWidth() / 2, -markerspan.outerHeight() / 2));
|
||||
markerspan.offset({'left': textPos.x - 4, 'top': textPos.y});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws the drag items on the page (and drop zones if required).
|
||||
* The idea is to re-draw all the drags and drops whenever there is a change
|
||||
* like a widow resize or an item dropped in place.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.repositionDrags = function() {
|
||||
var root = this.getRoot(),
|
||||
thisQ = this;
|
||||
|
||||
root.find('div.dragitems .dragitem').each(function(key, item) {
|
||||
$(item).addClass('unneeded');
|
||||
});
|
||||
|
||||
root.find('input.choices').each(function(key, input) {
|
||||
var choiceNo = thisQ.getChoiceNoFromElement(input),
|
||||
coords = thisQ.getCoords(input),
|
||||
dragHome = thisQ.dragHome(choiceNo);
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
var drag = thisQ.dragItem(choiceNo, i);
|
||||
if (!drag.length || drag.hasClass('beingdragged')) {
|
||||
drag = thisQ.cloneNewDragItem(dragHome, i);
|
||||
} else {
|
||||
drag.removeClass('unneeded');
|
||||
}
|
||||
drag.offset({'left': coords[i].x, 'top': coords[i].y});
|
||||
}
|
||||
});
|
||||
|
||||
root.find('div.dragitems .dragitem').each(function(key, itm) {
|
||||
var item = $(itm);
|
||||
if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
|
||||
item.remove();
|
||||
}
|
||||
});
|
||||
|
||||
this.repositionDropZones();
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine what drag items need to be shown and
|
||||
* return coords of all drag items except any that are currently being dragged
|
||||
* based on contents of hidden inputs and whether drags are 'infinite' or how many
|
||||
* drags should be shown.
|
||||
*
|
||||
* @param {jQuery} inputNode
|
||||
* @returns {Point[]} coordinates of however many copies of the drag item should be shown.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.getCoords = function(inputNode) {
|
||||
var root = this.getRoot(),
|
||||
choiceNo = this.getChoiceNoFromElement(inputNode),
|
||||
noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),
|
||||
dragging = root.find('span.dragitem.beingdragged.choice' + choiceNo).length > 0,
|
||||
coords = [],
|
||||
val = $(inputNode).val();
|
||||
if (val !== '') {
|
||||
var coordsStrings = val.split(';');
|
||||
for (var i = 0; i < coordsStrings.length; i++) {
|
||||
coords[i] = this.convertToWindowXY(Shapes.Point.parse(coordsStrings[i]));
|
||||
}
|
||||
}
|
||||
var displayeddrags = coords.length + (dragging ? 1 : 0);
|
||||
if ($(inputNode).hasClass('infinite') || (displayeddrags < noOfDrags)) {
|
||||
coords[coords.length] = this.dragHomeXY(choiceNo);
|
||||
}
|
||||
return coords;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the relative x and y position coordinates into
|
||||
* absolute x and y position coordinates.
|
||||
*
|
||||
* @param {Point} point relative to the background image.
|
||||
* @returns {Point} point relative to the page.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.convertToWindowXY = function(point) {
|
||||
var bgImage = this.bgImage();
|
||||
// The +1 seems rather odd, but seems to give the best results in
|
||||
// the three main browsers at a range of zoom levels.
|
||||
// (Its due to the 1px border around the image, that shifts the
|
||||
// image pixels by 1 down and to the left.)
|
||||
return point.offset(bgImage.offset().left + 1, bgImage.offset().top + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function converting window coordinates to relative to the
|
||||
* background image coordinates.
|
||||
*
|
||||
* @param {Point} point relative to the page.
|
||||
* @returns {Point} point relative to the background image.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.convertToBgImgXY = function(point) {
|
||||
var bgImage = this.bgImage();
|
||||
return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the point within the background image?
|
||||
*
|
||||
* @param {Point} point relative to the BG image.
|
||||
* @return {boolean} true it they are.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {
|
||||
var bgImage = this.bgImage();
|
||||
return point.x > 0 && point.x <= bgImage.width() &&
|
||||
point.y > 0 && point.y <= bgImage.height();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns coordinates for the home position of a choice.
|
||||
*
|
||||
* @param {Number} choiceNo
|
||||
* @returns {Point} coordinates
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.dragHomeXY = function(choiceNo) {
|
||||
var dragItemHome = this.dragHome(choiceNo);
|
||||
return new Shapes.Point(dragItemHome.offset().left, dragItemHome.offset().top);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the outer div for this question.
|
||||
* @returns {jQuery} containing that div.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.getRoot = function() {
|
||||
return $(document.getElementById(this.containerId));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the img that is the background image.
|
||||
* @returns {jQuery} containing that img.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.bgImage = function() {
|
||||
return this.getRoot().find('img.dropbackground');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the DOM node for this choice's home position.
|
||||
* @param {Number} choiceNo
|
||||
* @returns {jQuery} containing the home.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.dragHome = function(choiceNo) {
|
||||
return this.getRoot().find('div.dragitems span.draghome.choice' + choiceNo);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the DOM node for a particular instance of a particular choice.
|
||||
* @param {Number} choiceNo
|
||||
* @param {Number} itemNo
|
||||
* @returns {jQuery} containing the item.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.dragItem = function(choiceNo, itemNo) {
|
||||
return this.getRoot().find('div.dragitems span.dragitem.choice' + choiceNo + '.item' + itemNo);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a draggable copy of the drag item.
|
||||
*
|
||||
* @param {jQuery} dragHome to clone
|
||||
* @param {Number} itemNo new item number
|
||||
* @return {jQuery} drag
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.cloneNewDragItem = function(dragHome, itemNo) {
|
||||
var drag = dragHome.clone(true);
|
||||
drag.removeClass('draghome').addClass('dragitem').addClass('item' + itemNo);
|
||||
dragHome.after(drag);
|
||||
drag.attr('tabIndex', 0);
|
||||
return drag;
|
||||
};
|
||||
|
||||
DragDropMarkersQuestion.prototype.handleDragStart = function(e) {
|
||||
var thisQ = this,
|
||||
dragged = $(e.target).closest('.dragitem');
|
||||
|
||||
var info = dragDrop.prepare(e);
|
||||
if (!info.start) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragged.addClass('beingdragged');
|
||||
dragDrop.start(e, dragged, function() {
|
||||
void (1); // Nothing to do, but we need a function.
|
||||
}, function(x, y, dragged) {
|
||||
thisQ.dragEnd(dragged);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Functionality at the end of a drag drop.
|
||||
* @param {jQuery} dragged the marker that was dragged.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.dragEnd = function(dragged) {
|
||||
dragged.removeClass('beingdragged');
|
||||
var choiceNo = this.getChoiceNoFromElement(dragged);
|
||||
this.saveCoordsForChoice(choiceNo, dragged);
|
||||
this.repositionDrags();
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the coordinates for a dropped item in the form field.
|
||||
* @param {Number} choiceNo which copy of the choice this was.
|
||||
* @param {jQuery} dropped the choice that was dropped here.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo, dropped) {
|
||||
var coords = [],
|
||||
numItems = this.getRoot().find('span.dragitem.choice' + choiceNo).length,
|
||||
bgImgXY,
|
||||
addme = true;
|
||||
|
||||
// Re-build the coords array based on data in the ddform inputs.
|
||||
// While long winded and unnecessary if there is only one drop item
|
||||
// for a choice, it does account for moving any one of several drop items
|
||||
// within a choice that have already been placed.
|
||||
for (var i = 0; i <= numItems; i++) {
|
||||
var drag = this.dragItem(choiceNo, i);
|
||||
if (drag.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!drag.hasClass('beingdragged')) {
|
||||
bgImgXY = this.convertToBgImgXY(new Shapes.Point(drag.offset().left, drag.offset().top));
|
||||
if (this.coordsInBgImg(bgImgXY)) {
|
||||
coords[coords.length] = bgImgXY;
|
||||
}
|
||||
}
|
||||
|
||||
if (dropped && dropped.length !== 0 && (dropped[0].innerText === drag[0].innerText)) {
|
||||
addme = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If dropped has been passed it is because a new item has been dropped onto the background image
|
||||
// so add its coordinates to the array.
|
||||
if (addme) {
|
||||
bgImgXY = this.convertToBgImgXY(new Shapes.Point(dropped.offset().left, dropped.offset().top));
|
||||
if (this.coordsInBgImg(bgImgXY)) {
|
||||
coords[coords.length] = bgImgXY;
|
||||
}
|
||||
}
|
||||
|
||||
this.getRoot().find('input.choice' + choiceNo).val(coords.join(';'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle key down / press events on markers.
|
||||
* @param {KeyboardEvent} e
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {
|
||||
var drag = $(e.target).closest('.dragitem'),
|
||||
point = new Shapes.Point(drag.offset().left, drag.offset().top),
|
||||
choiceNo = this.getChoiceNoFromElement(drag);
|
||||
|
||||
switch (e.keyCode) {
|
||||
case keys.arrowLeft:
|
||||
case 65: // A.
|
||||
point.x -= 1;
|
||||
break;
|
||||
case keys.arrowRight:
|
||||
case 68: // D.
|
||||
point.x += 1;
|
||||
break;
|
||||
case keys.arrowDown:
|
||||
case 83: // S.
|
||||
point.y += 1;
|
||||
break;
|
||||
case keys.arrowUp:
|
||||
case 87: // W.
|
||||
point.y -= 1;
|
||||
break;
|
||||
case keys.space:
|
||||
case keys.escape:
|
||||
point = null;
|
||||
break;
|
||||
default:
|
||||
return; // Ingore other keys.
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
if (point !== null) {
|
||||
point = this.constrainToBgImg(point);
|
||||
} else {
|
||||
point = this.dragHomeXY(choiceNo);
|
||||
}
|
||||
drag.offset({'left': point.x, 'top': point.y});
|
||||
this.saveCoordsForChoice(choiceNo, drag);
|
||||
this.repositionDrags();
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes sure the dragged item always exists within the background image area.
|
||||
*
|
||||
* @param {Point} windowxy
|
||||
* @returns {Point} coordinates
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.constrainToBgImg = function(windowxy) {
|
||||
var bgImg = this.bgImage(),
|
||||
bgImgXY = this.convertToBgImgXY(windowxy);
|
||||
bgImgXY.x = Math.max(0, bgImgXY.x);
|
||||
bgImgXY.y = Math.max(0, bgImgXY.y);
|
||||
bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);
|
||||
bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);
|
||||
return this.convertToWindowXY(bgImgXY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the choice number for a node.
|
||||
*
|
||||
* @param {Element|jQuery} node
|
||||
* @returns {Number}
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.getChoiceNoFromElement = function(node) {
|
||||
return Number(this.getClassnameNumericSuffix(node, 'choice'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the numeric part of a class with the given prefix.
|
||||
*
|
||||
* @param {Element|jQuery} node
|
||||
* @param {String} prefix
|
||||
* @returns {Number|null}
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {
|
||||
var classes = $(node).attr('class');
|
||||
if (classes !== undefined && 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle when the window is resized.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.handleResize = function() {
|
||||
this.repositionDrags();
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton that tracks all the DragDropToTextQuestions on this page, and deals
|
||||
* with event dispatching.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
var questionManager = {
|
||||
|
||||
/**
|
||||
* {boolean} ensures that the event handlers are only initialised once per page.
|
||||
*/
|
||||
eventHandlersInitialised: false,
|
||||
|
||||
/**
|
||||
* {Object} all the questions on this page, indexed by containerId (id on the .que div).
|
||||
*/
|
||||
questions: {}, // An object containing all the information about each question on the page.
|
||||
|
||||
/**
|
||||
* Initialise one question.
|
||||
*
|
||||
* @param {String} containerId the id of the div.que that contains this question.
|
||||
* @param {String} bgImgUrl URL fo the background image.
|
||||
* @param {boolean} readOnly whether the question is read-only.
|
||||
* @param {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.
|
||||
*/
|
||||
init: function(containerId, bgImgUrl, readOnly, visibleDropZones) {
|
||||
questionManager.questions[containerId] =
|
||||
new DragDropMarkersQuestion(containerId, bgImgUrl, readOnly, visibleDropZones);
|
||||
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.ddmarker:not(.qtype_ddmarker-readonly) div.dragitems .dragitem',
|
||||
questionManager.handleDragStart)
|
||||
.on('keydown keypress',
|
||||
'.que.ddmarker:not(.qtype_ddmarker-readonly) div.dragitems .dragitem',
|
||||
questionManager.handleKeyPress);
|
||||
$(window).on('resize', questionManager.handleWindowResize);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle mouse down / touch start events on markers.
|
||||
* @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 events on markers.
|
||||
* @param {Event} 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].handleResize();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an event, work out which question it effects.
|
||||
* @param {Event} e the event.
|
||||
* @returns {DragDropMarkersQuestion|undefined} The question, or undefined.
|
||||
*/
|
||||
getQuestionForEvent: function(e) {
|
||||
var containerId = $(e.currentTarget).closest('.que.ddmarker').attr('id');
|
||||
return questionManager.questions[containerId];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @alias module:qtype_ddmarker/question
|
||||
*/
|
||||
return {
|
||||
/**
|
||||
* Initialise one drag-drop markers question.
|
||||
*
|
||||
* @param {String} containerId id of the outer div for this question.
|
||||
* @param {String} bgImgUrl the URL of the background image.
|
||||
* @param {boolean} readOnly whether the question is being displayed read-only.
|
||||
* @param {String[]} visibleDropZones the geometry of any drop-zones to show.
|
||||
*/
|
||||
init: questionManager.init
|
||||
};
|
||||
});
|
833
question/type/ddmarker/amd/src/shapes.js
Normal file
833
question/type/ddmarker/amd/src/shapes.js
Normal file
@ -0,0 +1,833 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/* eslint max-depth: ["error", 8] */
|
||||
|
||||
/**
|
||||
* Library of classes for handling simple shapes.
|
||||
*
|
||||
* These classes can represent shapes, let you alter them, can go to and from a string
|
||||
* representation, and can give you an SVG representation.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @subpackage shapes
|
||||
* @copyright 2018 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(function() {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A point, with x and y coordinates.
|
||||
*
|
||||
* @param {int} x centre X.
|
||||
* @param {int} y centre Y.
|
||||
* @constructor
|
||||
*/
|
||||
function Point(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard toString method.
|
||||
* @returns {string} "x;y";
|
||||
*/
|
||||
Point.prototype.toString = function() {
|
||||
return this.x + ',' + this.y;
|
||||
};
|
||||
|
||||
/**
|
||||
* Move a point
|
||||
* @param {int} dx x offset
|
||||
* @param {int} dy y offset
|
||||
*/
|
||||
Point.prototype.move = function(dx, dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a new point that is a certain position relative to this one.
|
||||
*
|
||||
* @param {(int|Point)} offsetX if a point, offset by this points coordinates, else and int x offset.
|
||||
* @param {int} [offsetY] used if offsetX is an int, the corresponding y offset.
|
||||
* @return {Point} the new point.
|
||||
*/
|
||||
Point.prototype.offset = function(offsetX, offsetY) {
|
||||
if (offsetX instanceof Point) {
|
||||
offsetY = offsetX.y;
|
||||
offsetX = offsetX.x;
|
||||
}
|
||||
return new Point(this.x + offsetX, this.y + offsetY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a point from the string representation.
|
||||
*
|
||||
* @param {String} coordinates "x,y".
|
||||
* @return {Point} the point. Throws an exception if input is not valid.
|
||||
*/
|
||||
Point.parse = function(coordinates) {
|
||||
var bits = coordinates.split(',');
|
||||
if (bits.length !== 2) {
|
||||
throw new Error(coordinates + ' is not a valid point');
|
||||
}
|
||||
return new Point(Math.round(bits[0]), Math.round(bits[1]));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Shape constructor. Abstract class to represent the different types of drop zone shapes.
|
||||
*
|
||||
* @param {String} [label] name of this area.
|
||||
* @param {int} [x] centre X.
|
||||
* @param {int} [y] centre Y.
|
||||
* @constructor
|
||||
*/
|
||||
function Shape(label, x, y) {
|
||||
this.label = label;
|
||||
this.centre = new Point(x || 0, y || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of shape.
|
||||
*
|
||||
* @return {String} 'circle', 'rectangle' or 'polygon';
|
||||
*/
|
||||
Shape.prototype.getType = function() {
|
||||
throw new Error('Not implemented.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the string representation of this shape.
|
||||
*
|
||||
* @return {String} coordinates as they need to be typed into the form.
|
||||
*/
|
||||
Shape.prototype.getCoordinates = function() {
|
||||
throw new Error('Not implemented.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the shape from the string representation.
|
||||
*
|
||||
* @param {String} coordinates in the form returned by getCoordinates.
|
||||
* @return {boolean} true if the string could be parsed and the shape updated, else false.
|
||||
*/
|
||||
Shape.prototype.parse = function(coordinates) {
|
||||
void (coordinates);
|
||||
throw new Error('Not implemented.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Move the entire shape by this offset.
|
||||
*
|
||||
* @param {int} dx x offset.
|
||||
* @param {int} dy y offset.
|
||||
* @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.
|
||||
* @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.
|
||||
*/
|
||||
Shape.prototype.move = function(dx, dy, maxX, maxY) {
|
||||
void (maxY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Move one of the edit handles by this offset.
|
||||
*
|
||||
* @param {int} handleIndex which handle was moved.
|
||||
* @param {int} dx x offset.
|
||||
* @param {int} dy y offset.
|
||||
* @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.
|
||||
* @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.
|
||||
*/
|
||||
Shape.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {
|
||||
void (maxY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the properties of this shape after a sequence of edits.
|
||||
*
|
||||
* For example make sure the circle radius is positive, of the polygon centre is centred.
|
||||
*/
|
||||
Shape.prototype.normalizeShape = function() {
|
||||
void (1); // To make CiBoT happy.
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the string representation of this shape.
|
||||
*
|
||||
* @param {SVGElement} svg the SVG graphic to add this shape to.
|
||||
* @return {SVGElement} SVG representation of this shape.
|
||||
*/
|
||||
Shape.prototype.makeSvg = function(svg) {
|
||||
void (svg);
|
||||
throw new Error('Not implemented.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the SVG representation of this shape.
|
||||
*
|
||||
* @param {SVGElement} svgEl the SVG representation of this shape.
|
||||
*/
|
||||
Shape.prototype.updateSvg = function(svgEl) {
|
||||
void (svgEl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a circle similar to this shape.
|
||||
*
|
||||
* @return {Circle} a circle that is about the same size and position as this shape.
|
||||
*/
|
||||
Shape.prototype.makeSimilarCircle = function() {
|
||||
throw new Error('Not implemented.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a rectangle similar to this shape.
|
||||
*
|
||||
* @return {Rectangle} a rectangle that is about the same size and position as this shape.
|
||||
*/
|
||||
Shape.prototype.makeSimilarRectangle = function() {
|
||||
throw new Error('Not implemented.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a polygon similar to this shape.
|
||||
*
|
||||
* @return {Polygon} a polygon that is about the same size and position as this shape.
|
||||
*/
|
||||
Shape.prototype.makeSimilarPolygon = function() {
|
||||
throw new Error('Not implemented.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the handles that should be offered to edit this shape, or null if not appropriate.
|
||||
*
|
||||
* @return {[Object]} with properties moveHandle {Point} and editHandles {Point[]}
|
||||
*/
|
||||
Shape.prototype.getHandlePositions = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A shape that is a circle.
|
||||
*
|
||||
* @param {String} label name of this area.
|
||||
* @param {int} [x] centre X.
|
||||
* @param {int} [y] centre Y.
|
||||
* @param {int} [radius] radius.
|
||||
* @constructor
|
||||
*/
|
||||
function Circle(label, x, y, radius) {
|
||||
Shape.call(this, label, x, y);
|
||||
this.radius = radius || 15;
|
||||
}
|
||||
Circle.prototype = new Shape();
|
||||
|
||||
Circle.prototype.getType = function() {
|
||||
return 'circle';
|
||||
};
|
||||
|
||||
Circle.prototype.getCoordinates = function() {
|
||||
return this.centre + ';' + Math.abs(this.radius);
|
||||
};
|
||||
|
||||
Circle.prototype.makeSvg = function(svg) {
|
||||
var svgEl = createSvgShapeGroup(svg, 'circle');
|
||||
this.updateSvg(svgEl);
|
||||
return svgEl;
|
||||
};
|
||||
|
||||
Circle.prototype.updateSvg = function(svgEl) {
|
||||
svgEl.childNodes[0].setAttribute('cx', this.centre.x);
|
||||
svgEl.childNodes[0].setAttribute('cy', this.centre.y);
|
||||
svgEl.childNodes[0].setAttribute('r', Math.abs(this.radius));
|
||||
svgEl.childNodes[1].setAttribute('x', this.centre.x);
|
||||
svgEl.childNodes[1].setAttribute('y', this.centre.y + 15);
|
||||
svgEl.childNodes[1].textContent = this.label;
|
||||
};
|
||||
|
||||
Circle.prototype.parse = function(coordinates) {
|
||||
if (!coordinates.match(/^\d+,\d+;\d+$/)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var bits = coordinates.split(';');
|
||||
this.centre = Point.parse(bits[0]);
|
||||
this.radius = Math.round(bits[1]);
|
||||
return true;
|
||||
};
|
||||
|
||||
Circle.prototype.move = function(dx, dy, maxX, maxY) {
|
||||
this.centre.move(dx, dy);
|
||||
if (this.centre.x < this.radius) {
|
||||
this.centre.x = this.radius;
|
||||
}
|
||||
if (this.centre.x > maxX - this.radius) {
|
||||
this.centre.x = maxX - this.radius;
|
||||
}
|
||||
if (this.centre.y < this.radius) {
|
||||
this.centre.y = this.radius;
|
||||
}
|
||||
if (this.centre.y > maxY - this.radius) {
|
||||
this.centre.y = maxY - this.radius;
|
||||
}
|
||||
};
|
||||
|
||||
Circle.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {
|
||||
this.radius += dx;
|
||||
var limit = Math.min(this.centre.x, this.centre.y, maxX - this.centre.x, maxY - this.centre.y);
|
||||
if (this.radius > limit) {
|
||||
this.radius = limit;
|
||||
}
|
||||
if (this.radius < -limit) {
|
||||
this.radius = -limit;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the properties of this shape after a sequence of edits.
|
||||
*
|
||||
* For example make sure the circle radius is positive, of the polygon centre is centred.
|
||||
*/
|
||||
Circle.prototype.normalizeShape = function() {
|
||||
this.radius = Math.abs(this.radius);
|
||||
};
|
||||
|
||||
Circle.prototype.makeSimilarRectangle = function() {
|
||||
return new Rectangle(this.label,
|
||||
this.centre.x - this.radius, this.centre.y - this.radius,
|
||||
this.radius * 2, this.radius * 2);
|
||||
};
|
||||
|
||||
Circle.prototype.makeSimilarPolygon = function() {
|
||||
// We make a similar square, so if you go to and from Rectangle afterwards, it is loss-less.
|
||||
return new Polygon(this.label, [
|
||||
this.centre.offset(-this.radius, -this.radius), this.centre.offset(-this.radius, this.radius),
|
||||
this.centre.offset(this.radius, this.radius), this.centre.offset(this.radius, -this.radius)]);
|
||||
};
|
||||
|
||||
Circle.prototype.getHandlePositions = function() {
|
||||
return {
|
||||
moveHandle: this.centre,
|
||||
editHandles: [this.centre.offset(this.radius, 0)]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A shape that is a rectangle.
|
||||
*
|
||||
* @param {String} label name of this area.
|
||||
* @param {int} [x] top left X.
|
||||
* @param {int} [y] top left Y.
|
||||
* @param {int} [width] width.
|
||||
* @param {int} [height] height.
|
||||
* @constructor
|
||||
*/
|
||||
function Rectangle(label, x, y, width, height) {
|
||||
Shape.call(this, label, x, y);
|
||||
this.width = width || 30;
|
||||
this.height = height || 30;
|
||||
}
|
||||
Rectangle.prototype = new Shape();
|
||||
|
||||
Rectangle.prototype.getType = function() {
|
||||
return 'rectangle';
|
||||
};
|
||||
|
||||
Rectangle.prototype.getCoordinates = function() {
|
||||
return this.centre + ';' + this.width + ',' + this.height;
|
||||
};
|
||||
|
||||
Rectangle.prototype.makeSvg = function(svg) {
|
||||
var svgEl = createSvgShapeGroup(svg, 'rect');
|
||||
this.updateSvg(svgEl);
|
||||
return svgEl;
|
||||
};
|
||||
|
||||
Rectangle.prototype.updateSvg = function(svgEl) {
|
||||
if (this.width >= 0) {
|
||||
svgEl.childNodes[0].setAttribute('x', this.centre.x);
|
||||
svgEl.childNodes[0].setAttribute('width', this.width);
|
||||
} else {
|
||||
svgEl.childNodes[0].setAttribute('x', this.centre.x + this.width);
|
||||
svgEl.childNodes[0].setAttribute('width', -this.width);
|
||||
}
|
||||
if (this.height >= 0) {
|
||||
svgEl.childNodes[0].setAttribute('y', this.centre.y);
|
||||
svgEl.childNodes[0].setAttribute('height', this.height);
|
||||
} else {
|
||||
svgEl.childNodes[0].setAttribute('y', this.centre.y + this.height);
|
||||
svgEl.childNodes[0].setAttribute('height', -this.height);
|
||||
}
|
||||
|
||||
svgEl.childNodes[1].setAttribute('x', this.centre.x + this.width / 2);
|
||||
svgEl.childNodes[1].setAttribute('y', this.centre.y + this.height / 2 + 15);
|
||||
svgEl.childNodes[1].textContent = this.label;
|
||||
};
|
||||
|
||||
Rectangle.prototype.parse = function(coordinates) {
|
||||
if (!coordinates.match(/^\d+,\d+;\d+,\d+$/)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var bits = coordinates.split(';');
|
||||
this.centre = Point.parse(bits[0]);
|
||||
var size = Point.parse(bits[1]);
|
||||
this.width = size.x;
|
||||
this.height = size.y;
|
||||
return true;
|
||||
};
|
||||
|
||||
Rectangle.prototype.move = function(dx, dy, maxX, maxY) {
|
||||
this.centre.move(dx, dy);
|
||||
if (this.centre.x < 0) {
|
||||
this.centre.x = 0;
|
||||
}
|
||||
if (this.centre.x > maxX - this.width) {
|
||||
this.centre.x = maxX - this.width;
|
||||
}
|
||||
if (this.centre.y < 0) {
|
||||
this.centre.y = 0;
|
||||
}
|
||||
if (this.centre.y > maxY - this.height) {
|
||||
this.centre.y = maxY - this.height;
|
||||
}
|
||||
};
|
||||
|
||||
Rectangle.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {
|
||||
this.width += dx;
|
||||
this.height += dy;
|
||||
if (this.width < -this.centre.x) {
|
||||
this.width = -this.centre.x;
|
||||
}
|
||||
if (this.width > maxX - this.centre.x) {
|
||||
this.width = maxX - this.centre.x;
|
||||
}
|
||||
if (this.height < -this.centre.y) {
|
||||
this.height = -this.centre.y;
|
||||
}
|
||||
if (this.height > maxY - this.centre.y) {
|
||||
this.height = maxY - this.centre.y;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the properties of this shape after a sequence of edits.
|
||||
*
|
||||
* For example make sure the circle radius is positive, of the polygon centre is centred.
|
||||
*/
|
||||
Rectangle.prototype.normalizeShape = function() {
|
||||
if (this.width < 0) {
|
||||
this.centre.x += this.width;
|
||||
this.width = -this.width;
|
||||
}
|
||||
if (this.height < 0) {
|
||||
this.centre.y += this.height;
|
||||
this.height = -this.height;
|
||||
}
|
||||
};
|
||||
|
||||
Rectangle.prototype.makeSimilarCircle = function() {
|
||||
return new Circle(this.label,
|
||||
Math.round(this.centre.x + this.width / 2),
|
||||
Math.round(this.centre.y + this.height / 2),
|
||||
Math.round((this.width + this.height) / 4));
|
||||
};
|
||||
|
||||
Rectangle.prototype.makeSimilarPolygon = function() {
|
||||
return new Polygon(this.label, [
|
||||
this.centre, this.centre.offset(0, this.height),
|
||||
this.centre.offset(this.width, this.height), this.centre.offset(this.width, 0)]);
|
||||
};
|
||||
|
||||
Rectangle.prototype.getHandlePositions = function() {
|
||||
return {
|
||||
moveHandle: this.centre.offset(this.width / 2, this.height / 2),
|
||||
editHandles: [this.centre.offset(this.width, this.height)]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A shape that is a polygon.
|
||||
*
|
||||
* @param {String} label name of this area.
|
||||
* @param {Point[]} [points] position of the vertices relative to (centreX, centreY).
|
||||
* each object in the array should have two
|
||||
* @constructor
|
||||
*/
|
||||
function Polygon(label, points) {
|
||||
Shape.call(this, label, 0, 0);
|
||||
this.points = points ? Array.from(points) : [new Point(10, 10), new Point(40, 10), new Point(10, 40)];
|
||||
this.normalizeShape();
|
||||
}
|
||||
Polygon.prototype = new Shape();
|
||||
|
||||
Polygon.prototype.getType = function() {
|
||||
return 'polygon';
|
||||
};
|
||||
|
||||
Polygon.prototype.getCoordinates = function() {
|
||||
var coordinates = '';
|
||||
for (var i = 0; i < this.points.length; i++) {
|
||||
coordinates += this.centre.offset(this.points[i]) + ';';
|
||||
}
|
||||
return coordinates.slice(0, coordinates.length - 1); // Strip off the last ';'.
|
||||
};
|
||||
|
||||
Polygon.prototype.makeSvg = function(svg) {
|
||||
var svgEl = createSvgShapeGroup(svg, 'polygon');
|
||||
this.updateSvg(svgEl);
|
||||
return svgEl;
|
||||
};
|
||||
|
||||
Polygon.prototype.updateSvg = function(svgEl) {
|
||||
svgEl.childNodes[0].setAttribute('points', this.getCoordinates().replace(/[,;]/g, ' '));
|
||||
svgEl.childNodes[1].setAttribute('x', this.centre.x);
|
||||
svgEl.childNodes[1].setAttribute('y', this.centre.y + 15);
|
||||
svgEl.childNodes[1].textContent = this.label;
|
||||
};
|
||||
|
||||
Polygon.prototype.parse = function(coordinates) {
|
||||
if (!coordinates.match(/^\d+,\d+(?:;\d+,\d+)*$/)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var bits = coordinates.split(';');
|
||||
var points = [];
|
||||
for (var i = 0; i < bits.length; i++) {
|
||||
points.push(Point.parse(bits[i]));
|
||||
}
|
||||
|
||||
this.points = points;
|
||||
this.centre.x = 0;
|
||||
this.centre.y = 0;
|
||||
this.normalizeShape();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Polygon.prototype.move = function(dx, dy, maxX, maxY) {
|
||||
this.centre.move(dx, dy);
|
||||
var bbXMin = maxX,
|
||||
bbXMax = 0,
|
||||
bbYMin = maxY,
|
||||
bbYMax = 0;
|
||||
// Computer centre.
|
||||
for (var i = 0; i < this.points.length; i++) {
|
||||
bbXMin = Math.min(bbXMin, this.points[i].x);
|
||||
bbXMax = Math.max(bbXMax, this.points[i].x);
|
||||
bbYMin = Math.min(bbYMin, this.points[i].y);
|
||||
bbYMax = Math.max(bbYMax, this.points[i].y);
|
||||
}
|
||||
if (this.centre.x < -bbXMin) {
|
||||
this.centre.x = -bbXMin;
|
||||
}
|
||||
if (this.centre.x > maxX - bbXMax) {
|
||||
this.centre.x = maxX - bbXMax;
|
||||
}
|
||||
if (this.centre.y < -bbYMin) {
|
||||
this.centre.y = -bbYMin;
|
||||
}
|
||||
if (this.centre.y > maxY - bbYMax) {
|
||||
this.centre.y = maxY - bbYMax;
|
||||
}
|
||||
};
|
||||
|
||||
Polygon.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {
|
||||
this.points[handleIndex].move(dx, dy);
|
||||
if (this.points[handleIndex].x < -this.centre.x) {
|
||||
this.points[handleIndex].x = -this.centre.x;
|
||||
}
|
||||
if (this.points[handleIndex].x > maxX - this.centre.x) {
|
||||
this.points[handleIndex].x = maxX - this.centre.x;
|
||||
}
|
||||
if (this.points[handleIndex].y < -this.centre.y) {
|
||||
this.points[handleIndex].y = -this.centre.y;
|
||||
}
|
||||
if (this.points[handleIndex].y > maxY - this.centre.y) {
|
||||
this.points[handleIndex].y = maxY - this.centre.y;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new point after the given point, with the same co-ordinates.
|
||||
*
|
||||
* This does not automatically normalise.
|
||||
*
|
||||
* @param {int} pointIndex the index of the vertex after which to insert this new one.
|
||||
*/
|
||||
Polygon.prototype.addNewPointAfter = function(pointIndex) {
|
||||
this.points.splice(pointIndex, 0,
|
||||
new Point(this.points[pointIndex].x, this.points[pointIndex].y));
|
||||
};
|
||||
|
||||
Polygon.prototype.normalizeShape = function() {
|
||||
var i,
|
||||
x = 0,
|
||||
y = 0;
|
||||
|
||||
if (this.points.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Computer centre.
|
||||
for (i = 0; i < this.points.length; i++) {
|
||||
x += this.points[i].x;
|
||||
y += this.points[i].y;
|
||||
}
|
||||
x = Math.round(x / this.points.length);
|
||||
y = Math.round(y / this.points.length);
|
||||
|
||||
if (x === 0 && y === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < this.points.length; i++) {
|
||||
this.points[i].move(-x, -y);
|
||||
}
|
||||
this.centre.move(x, y);
|
||||
};
|
||||
|
||||
Polygon.prototype.makeSimilarCircle = function() {
|
||||
return this.makeSimilarRectangle().makeSimilarCircle();
|
||||
};
|
||||
|
||||
Polygon.prototype.makeSimilarRectangle = function() {
|
||||
var p,
|
||||
minX = 0,
|
||||
maxX = 0,
|
||||
minY = 0,
|
||||
maxY = 0;
|
||||
for (var i = 0; i < this.points.length; i++) {
|
||||
p = this.points[i];
|
||||
minX = Math.min(minX, p.x);
|
||||
maxX = Math.max(maxX, p.x);
|
||||
minY = Math.min(minY, p.y);
|
||||
maxY = Math.max(maxY, p.y);
|
||||
}
|
||||
return new Rectangle(this.label,
|
||||
this.centre.x + minX, this.centre.y + minY,
|
||||
Math.max(maxX - minX, 10), Math.max(maxY - minY, 10));
|
||||
};
|
||||
|
||||
Polygon.prototype.getHandlePositions = function() {
|
||||
var editHandles = [];
|
||||
for (var i = 0; i < this.points.length; i++) {
|
||||
editHandles.push(this.points[i].offset(this.centre.x, this.centre.y));
|
||||
}
|
||||
|
||||
return {
|
||||
moveHandle: this.centre,
|
||||
editHandles: editHandles
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Not a shape (null object pattern).
|
||||
*
|
||||
* @param {String} label name of this area.
|
||||
* @constructor
|
||||
*/
|
||||
function NullShape(label) {
|
||||
Shape.call(this, label);
|
||||
}
|
||||
NullShape.prototype = new Shape();
|
||||
|
||||
NullShape.prototype.getType = function() {
|
||||
return 'null';
|
||||
};
|
||||
|
||||
NullShape.prototype.getCoordinates = function() {
|
||||
return '';
|
||||
};
|
||||
|
||||
NullShape.prototype.makeSvg = function(svg) {
|
||||
void (svg);
|
||||
return null;
|
||||
};
|
||||
|
||||
NullShape.prototype.updateSvg = function(svgEl) {
|
||||
void (svgEl);
|
||||
};
|
||||
|
||||
NullShape.prototype.parse = function(coordinates) {
|
||||
void (coordinates);
|
||||
return false;
|
||||
};
|
||||
|
||||
NullShape.prototype.makeSimilarCircle = function() {
|
||||
return new Circle(this.label);
|
||||
};
|
||||
|
||||
NullShape.prototype.makeSimilarRectangle = function() {
|
||||
return new Rectangle(this.label);
|
||||
};
|
||||
|
||||
NullShape.prototype.makeSimilarPolygon = function() {
|
||||
return new Polygon(this.label);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Make a new SVG DOM element as a child of svg.
|
||||
*
|
||||
* @param {SVGElement} svg the parent node.
|
||||
* @param {String} tagName the tag name.
|
||||
* @return {SVGElement} the newly created node.
|
||||
*/
|
||||
function createSvgElement(svg, tagName) {
|
||||
var svgEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', tagName);
|
||||
svg.appendChild(svgEl);
|
||||
return svgEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a group SVG DOM elements containing a shape of the given type as first child,
|
||||
* and a text label as the second child.
|
||||
*
|
||||
* @param {SVGElement} svg the parent node.
|
||||
* @param {String} tagName the tag name.
|
||||
* @return {SVGElement} the newly created g element.
|
||||
*/
|
||||
function createSvgShapeGroup(svg, tagName) {
|
||||
var svgEl = createSvgElement(svg, 'g');
|
||||
createSvgElement(svgEl, tagName).setAttribute('class', 'shape');
|
||||
createSvgElement(svgEl, 'text').setAttribute('class', 'shapeLabel');
|
||||
return svgEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alias module:qtype_ddmarker/shapes
|
||||
*/
|
||||
return {
|
||||
/**
|
||||
* A point, with x and y coordinates.
|
||||
*
|
||||
* @param {int} x centre X.
|
||||
* @param {int} y centre Y.
|
||||
* @constructor
|
||||
*/
|
||||
Point: Point,
|
||||
|
||||
/**
|
||||
* A point, with x and y coordinates.
|
||||
*
|
||||
* @param {int} x centre X.
|
||||
* @param {int} y centre Y.
|
||||
* @constructor
|
||||
*/
|
||||
Shape: Shape,
|
||||
|
||||
/**
|
||||
* A shape that is a circle.
|
||||
*
|
||||
* @param {String} label name of this area.
|
||||
* @param {int} [x] centre X.
|
||||
* @param {int} [y] centre Y.
|
||||
* @param {int} [radius] radius.
|
||||
* @constructor
|
||||
*/
|
||||
Circle: Circle,
|
||||
|
||||
/**
|
||||
* A shape that is a rectangle.
|
||||
*
|
||||
* @param {String} label name of this area.
|
||||
* @param {int} [x] top left X.
|
||||
* @param {int} [y] top left Y.
|
||||
* @param {int} [width] width.
|
||||
* @param {int} [height] height.
|
||||
* @constructor
|
||||
*/
|
||||
Rectangle: Rectangle,
|
||||
|
||||
/**
|
||||
* A shape that is a polygon.
|
||||
*
|
||||
* @param {String} label name of this area.
|
||||
* @param {Point[]} [points] position of the vertices relative to (centreX, centreY).
|
||||
* each object in the array should have two
|
||||
* @constructor
|
||||
*/
|
||||
Polygon: Polygon,
|
||||
|
||||
/**
|
||||
* Not a shape (null object pattern).
|
||||
*
|
||||
* @param {String} label name of this area.
|
||||
* @constructor
|
||||
*/
|
||||
NullShape: NullShape,
|
||||
|
||||
/**
|
||||
* Make a new SVG DOM element as a child of svg.
|
||||
*
|
||||
* @param {SVGElement} svg the parent node.
|
||||
* @param {String} tagName the tag name.
|
||||
* @return {SVGElement} the newly created node.
|
||||
*/
|
||||
createSvgElement: createSvgElement,
|
||||
|
||||
/**
|
||||
* Make a shape of the given type.
|
||||
*
|
||||
* @param {String} shapeType
|
||||
* @param {String} label
|
||||
* @return {Shape} the requested shape.
|
||||
*/
|
||||
make: function(shapeType, label) {
|
||||
switch (shapeType) {
|
||||
case 'circle':
|
||||
return new Circle(label);
|
||||
case 'rectangle':
|
||||
return new Rectangle(label);
|
||||
case 'polygon':
|
||||
return new Polygon(label);
|
||||
default:
|
||||
return new NullShape(label);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a shape of the given type that is similar to the shape of the original type.
|
||||
*
|
||||
* @param {String} shapeType the new type of shape to make
|
||||
* @param {Shape} shape the shape to copy
|
||||
* @return {Shape} the similar shape of a different type.
|
||||
*/
|
||||
getSimilar: function(shapeType, shape) {
|
||||
if (shapeType === shape.getType()) {
|
||||
return shape;
|
||||
}
|
||||
switch (shapeType) {
|
||||
case 'circle':
|
||||
return shape.makeSimilarCircle();
|
||||
case 'rectangle':
|
||||
return shape.makeSimilarRectangle();
|
||||
case 'polygon':
|
||||
return shape.makeSimilarPolygon();
|
||||
default:
|
||||
return new NullShape(shape.label);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
@ -53,17 +53,10 @@ class qtype_ddmarker_edit_form extends qtype_ddtoimage_edit_form_base {
|
||||
|
||||
public function js_call() {
|
||||
global $PAGE;
|
||||
$maxsizes = new stdClass();
|
||||
$maxsizes->bgimage = new stdClass();
|
||||
$maxsizes->bgimage->width = QTYPE_DDMARKER_BGIMAGE_MAXWIDTH;
|
||||
$maxsizes->bgimage->height = QTYPE_DDMARKER_BGIMAGE_MAXHEIGHT;
|
||||
$maxsize = ['width' => QTYPE_DDMARKER_BGIMAGE_MAXWIDTH,
|
||||
'height' => QTYPE_DDMARKER_BGIMAGE_MAXHEIGHT];
|
||||
|
||||
$params = array('maxsizes' => $maxsizes,
|
||||
'topnode' => 'fieldset#id_previewareaheader');
|
||||
|
||||
$PAGE->requires->yui_module('moodle-qtype_ddmarker-form',
|
||||
'M.qtype_ddmarker.init_form',
|
||||
array($params));
|
||||
$PAGE->requires->js_call_amd('qtype_ddmarker/form', 'init', [$maxsize]);
|
||||
}
|
||||
|
||||
|
||||
@ -105,16 +98,10 @@ class qtype_ddmarker_edit_form extends qtype_ddtoimage_edit_form_base {
|
||||
}
|
||||
|
||||
protected function drop_zone($mform, $imagerepeats) {
|
||||
$dropzoneitem = array();
|
||||
|
||||
$grouparray = array();
|
||||
$shapearray = qtype_ddmarker_shape::shape_options();
|
||||
$grouparray[] = $mform->createElement('select', 'shape',
|
||||
get_string('shape', 'qtype_ddmarker'), $shapearray);
|
||||
$grouparray[] = $mform->createElement('text', 'coords',
|
||||
get_string('coords', 'qtype_ddmarker'),
|
||||
array('size' => 50, 'class' => 'tweakcss'));
|
||||
$mform->setType('coords', PARAM_RAW); // These are validated manually.
|
||||
$markernos = array();
|
||||
$markernos[0] = '';
|
||||
for ($i = 1; $i <= $imagerepeats; $i += 1) {
|
||||
@ -122,6 +109,10 @@ class qtype_ddmarker_edit_form extends qtype_ddtoimage_edit_form_base {
|
||||
}
|
||||
$grouparray[] = $mform->createElement('select', 'choice',
|
||||
get_string('marker', 'qtype_ddmarker'), $markernos);
|
||||
$grouparray[] = $mform->createElement('text', 'coords',
|
||||
get_string('coords', 'qtype_ddmarker'),
|
||||
array('size' => 30, 'class' => 'tweakcss'));
|
||||
$mform->setType('coords', PARAM_RAW); // These are validated manually.
|
||||
$dropzone = $mform->createElement('group', 'drops',
|
||||
get_string('dropzone', 'qtype_ddmarker', '{no}'), $grouparray);
|
||||
return array($dropzone);
|
||||
@ -228,7 +219,6 @@ class qtype_ddmarker_edit_form extends qtype_ddtoimage_edit_form_base {
|
||||
$errors["bgimage"] = get_string('formerror_nobgimage', 'qtype_ddmarker');
|
||||
}
|
||||
|
||||
$allchoices = array();
|
||||
for ($i = 0; $i < $data['nodropzone']; $i++) {
|
||||
$choice = $data['drops'][$i]['choice'];
|
||||
$choicepresent = ($choice !== '0');
|
||||
|
@ -36,11 +36,20 @@ $string['dropbackground'] = 'Background image for dragging markers onto';
|
||||
$string['dropzone'] = 'Drop zone {$a}';
|
||||
$string['dropzoneheader'] = 'Drop zones';
|
||||
$string['dropzones'] = 'Drop zones';
|
||||
$string['dropzones_help'] = 'The drop zones are defined by typing coordinates. As you type, the preview above is immediately updated, so you can position things by trial and improvement.
|
||||
$string['dropzones_help'] = 'Drop zones may be defined by coordinates, or dragged into position in the preview above.
|
||||
|
||||
* Circle: centre_x, centre_y; radius<br>for example: <code>80, 100; 50</code>
|
||||
* Polygon: x1, y1; x2, y2; ...; xn, yn<br>for example: <code>20, 60; 100, 60; 20, 100</code>
|
||||
* Rectangle: top_left_x, top_left_y; width, height<br>for example: <code>20, 60; 80, 40</code>';
|
||||
First selecting a shape (circle, rectangle or polygon) will add a new drop zone shape to the top left of the preview. It may be useful to minimise the Markers section so you can see the preview while editing the Drop zones.
|
||||
|
||||
Editing a shape starts with a click on the shape in the preview to show the editing handles. You can move the shape using the center handle, or adjust the shape\'s dimensions with the vertex handles.
|
||||
|
||||
For polygons only, holding the control button (command button on a Mac) while clicking on a vertex handle will add a new vertex to the polygon. Please keep a polygon shape as simple as possible, without crossing lines.
|
||||
|
||||
For information the three shapes use coordinates in this way:<br />
|
||||
* Circle: centre_x, centre_y; radius<br />for example: <code>80,100;50</code><br />
|
||||
* Rectangle: top_left_x, top_left_y; width, height<br />for example: <code>20,60;80,40</code><br />
|
||||
* Polygon: x1, y1; x2, y2; ...; xn, yn<br />for example: <code>20,60;100,60;20,100</code>
|
||||
|
||||
Selecting a Marker text will add that text to the shape in the preview.';
|
||||
$string['followingarewrong'] = 'The following markers have been placed in the wrong area : {$a}.';
|
||||
$string['followingarewrongandhighlighted'] = 'The following markers were incorrectly placed : {$a}. Highlighted marker(s) are now shown with the correct placement(s).<br /> Click on the marker to highlight the allowed area.';
|
||||
$string['formerror_nobgimage'] = 'You need to select an image to use as the background for the drag and drop area.';
|
||||
|
@ -51,7 +51,7 @@ class qtype_ddmarker_renderer extends qtype_ddtoimage_renderer_base {
|
||||
$bgimage = self::get_url_for_image($qa, 'bgimage');
|
||||
|
||||
$img = html_writer::empty_tag('img', array(
|
||||
'src' => $bgimage, 'class' => 'dropbackground',
|
||||
'class' => 'dropbackground',
|
||||
'alt' => get_string('dropbackground', 'qtype_ddmarker')));
|
||||
|
||||
$droparea = html_writer::tag('div', $img, array('class' => 'droparea'));
|
||||
@ -96,14 +96,8 @@ class qtype_ddmarker_renderer extends qtype_ddtoimage_renderer_base {
|
||||
$visibledropzones = array();
|
||||
}
|
||||
|
||||
$topnode = 'div#q'.$qa->get_slot();
|
||||
$params = array('dropzones' => $visibledropzones,
|
||||
'topnode' => $topnode,
|
||||
'readonly' => $options->readonly);
|
||||
|
||||
$PAGE->requires->yui_module('moodle-qtype_ddmarker-dd',
|
||||
'M.qtype_ddmarker.init_question',
|
||||
array($params));
|
||||
$PAGE->requires->js_call_amd('qtype_ddmarker/question', 'init',
|
||||
['q' . $qa->get_slot(), $bgimage, $options->readonly, $visibledropzones]);
|
||||
|
||||
if ($qa->get_state() == question_state::$invalid) {
|
||||
$output .= html_writer::nonempty_tag('div',
|
||||
|
@ -42,7 +42,8 @@ abstract class qtype_ddmarker_shape {
|
||||
}
|
||||
public function inside_width_height($widthheight) {
|
||||
foreach ($this->outlying_coords_to_test() as $coordsxy) {
|
||||
if ($coordsxy[0] > $widthheight[0] || $coordsxy[1] > $widthheight[1]) {
|
||||
if ($coordsxy[0] < 0 || $coordsxy[0] > $widthheight[0] ||
|
||||
$coordsxy[1] < 0 || $coordsxy[1] > $widthheight[1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -153,6 +154,7 @@ abstract class qtype_ddmarker_shape {
|
||||
foreach ($shapes as $shape) {
|
||||
$shapearray[$shape::name()] = $shape::human_readable_name();
|
||||
}
|
||||
$shapearray['0'] = '';
|
||||
asort($shapearray);
|
||||
return $shapearray;
|
||||
}
|
||||
@ -235,7 +237,7 @@ class qtype_ddmarker_shape_rectangle extends qtype_ddmarker_shape {
|
||||
|
||||
}
|
||||
protected function outlying_coords_to_test() {
|
||||
return array($this->xleft + $this->width, $this->ytop + $this->height);
|
||||
return [[$this->xleft, $this->ytop], [$this->xleft + $this->width, $this->ytop + $this->height]];
|
||||
}
|
||||
public function is_point_in_shape($xy) {
|
||||
return $this->is_point_in_bounding_box($xy, array($this->xleft, $this->ytop),
|
||||
@ -295,7 +297,8 @@ class qtype_ddmarker_shape_circle extends qtype_ddmarker_shape {
|
||||
}
|
||||
|
||||
protected function outlying_coords_to_test() {
|
||||
return array($this->xcentre + $this->radius, $this->ycentre + $this->radius);
|
||||
return [[$this->xcentre - $this->radius, $this->ycentre - $this->radius],
|
||||
[$this->xcentre + $this->radius, $this->ycentre + $this->radius]];
|
||||
}
|
||||
|
||||
public function is_point_in_shape($xy) {
|
||||
|
@ -3,12 +3,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.que.ddmarker div.droparea img,
|
||||
form.mform fieldset#id_previewareaheader div.droparea img {
|
||||
border: 1px solid #000;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.que.ddmarker .draghome img,
|
||||
.que.ddmarker .draghome span {
|
||||
visibility: hidden;
|
||||
@ -44,6 +38,8 @@ form.mform fieldset#id_previewareaheader div.ddarea .markertexts {
|
||||
.que.ddmarker .dropbackground,
|
||||
form.mform fieldset#id_previewareaheader .dropbackground {
|
||||
margin: 0 auto;
|
||||
border: 1px solid black;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.que.ddmarker div.dragitems div.draghome,
|
||||
@ -64,21 +60,20 @@ form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
border-radius: 10px;
|
||||
color: black;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.que.ddmarker div.markertexts span.markertext {
|
||||
z-index: 2;
|
||||
background-color: yellow;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: khaki;
|
||||
border: 2px solid khaki;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.que.ddmarker span.wrongpart {
|
||||
background-color: yellow;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: khaki;
|
||||
border: 2px solid khaki;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
filter: alpha(opacity=60);
|
||||
@ -97,15 +92,67 @@ form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.que.ddmarker .dragitem.yui3-dd-dragging span.markertext {
|
||||
.que.ddmarker .dragitem.beingdragged span.markertext {
|
||||
z-index: 3;
|
||||
box-shadow: 3px 3px 4px #000;
|
||||
}
|
||||
|
||||
#page-question-type-ddmarker .ddarea .grid {
|
||||
position: absolute;
|
||||
background: url([[pix:qtype_ddmarker|grid]]) repeat scroll 0 0;
|
||||
/* Styles for the preview on the editing form. */
|
||||
.que.ddmarker .dropzone .shape {
|
||||
fill: #fff;
|
||||
fill-opacity: 0.5;
|
||||
stroke: black;
|
||||
stroke-width: 1;
|
||||
}
|
||||
.que.ddmarker .dropzone.active .shape {
|
||||
stroke-width: 2;
|
||||
}
|
||||
.que.ddmarker .dropzone.color0 .shape {
|
||||
fill: #fff;
|
||||
}
|
||||
.que.ddmarker .dropzone.color1 .shape {
|
||||
fill: #b0c4de;
|
||||
}
|
||||
.que.ddmarker .dropzone.color2 .shape {
|
||||
fill: #dcdcdc;
|
||||
}
|
||||
.que.ddmarker .dropzone.color3 .shape {
|
||||
fill: #d8bfd8;
|
||||
}
|
||||
.que.ddmarker .dropzone.color4 .shape {
|
||||
fill: #87cefa;
|
||||
}
|
||||
.que.ddmarker .dropzone.color5 .shape {
|
||||
fill: #daa520;
|
||||
}
|
||||
.que.ddmarker .dropzone.color6 .shape {
|
||||
fill: #ffd700;
|
||||
}
|
||||
.que.ddmarker .dropzone.color7 .shape {
|
||||
fill: #f0e68c;
|
||||
}
|
||||
|
||||
.que.ddmarker .dropzone .shapeLabel {
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.que.ddmarker .dropzone .handle {
|
||||
fill: #fff;
|
||||
fill-opacity: 0.1; /* Need a small amount of opacity of the handle can't be grabbed. */
|
||||
stroke-width: 1;
|
||||
display: none;
|
||||
cursor: move;
|
||||
}
|
||||
.que.ddmarker .dropzone .handle.move {
|
||||
stroke: #800;
|
||||
}
|
||||
.que.ddmarker .dropzone .handle.edit {
|
||||
stroke: #008;
|
||||
}
|
||||
.que.ddmarker .dropzone.active .handle {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
/* Editing form. Style repeated elements*/
|
||||
/*Top*/
|
||||
body#page-question-type-ddmarker div[id^=fitem_id_][id*=hint_] {
|
||||
|
@ -48,10 +48,10 @@ Feature: Preview a drag-drop marker 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 type "up" "89" times on marker "Railway station" in the drag and drop markers question
|
||||
And I type "right" "21" times on marker "Railway station" in the drag and drop markers question
|
||||
And I type "up" "44" times on marker "Railway station" in the drag and drop markers question
|
||||
And I type "right" "13" times on marker "Railway station" in the drag and drop markers question
|
||||
And I press "Submit and finish"
|
||||
Then the state of "Please place the markers on the map of Milton Keynes" question is shown as "Partially correct"
|
||||
And I should see "Mark 0.25 out of 1.00"
|
||||
|
@ -170,10 +170,10 @@ class qtype_ddmarker_test_helper extends question_test_helper {
|
||||
);
|
||||
|
||||
$fromform->drops = array(
|
||||
array('shape' => 'Circle', 'coords' => '322,213;10', 'choice' => 1),
|
||||
array('shape' => 'Circle', 'coords' => '144,84;10', 'choice' => 2),
|
||||
array('shape' => 'Circle', 'coords' => '195,180;10', 'choice' => 2),
|
||||
array('shape' => 'Circle', 'coords' => '267,302;10', 'choice' => 2),
|
||||
array('shape' => 'circle', 'coords' => '322,213;10', 'choice' => 1),
|
||||
array('shape' => 'circle', 'coords' => '144,84;10', 'choice' => 2),
|
||||
array('shape' => 'circle', 'coords' => '195,180;10', 'choice' => 2),
|
||||
array('shape' => 'circle', 'coords' => '267,302;10', 'choice' => 2),
|
||||
);
|
||||
|
||||
test_question_maker::set_standard_combined_feedback_form_data($fromform);
|
||||
|
@ -1,616 +0,0 @@
|
||||
YUI.add('moodle-qtype_ddmarker-dd', function (Y, NAME) {
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
var DDMARKERDDNAME = 'moodle-qtype_ddmarker-dd';
|
||||
var DDMARKER_DD = function() {
|
||||
DDMARKER_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDMARKER_DD, Y.Base, {
|
||||
doc: null,
|
||||
polltimer: null,
|
||||
afterimageloaddone: false,
|
||||
graphics: null,
|
||||
poll_for_image_load: function(e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
if (bgdone) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure: function() {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node: function() {
|
||||
return topnode;
|
||||
},
|
||||
bg_img: function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img: function(url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
drag_items: function() {
|
||||
return dragitemsarea.all('.dragitem');
|
||||
},
|
||||
drag_items_for_choice: function(choiceno) {
|
||||
return dragitemsarea.all('span.dragitem.choice' + choiceno);
|
||||
},
|
||||
drag_item_for_choice: function(choiceno, itemno) {
|
||||
return dragitemsarea.one('span.dragitem.choice' + choiceno +
|
||||
'.item' + itemno);
|
||||
},
|
||||
drag_item_being_dragged: function(choiceno) {
|
||||
return dragitemsarea.one('span.dragitem.beingdragged.choice' + choiceno);
|
||||
},
|
||||
drag_item_home: function(choiceno) {
|
||||
return dragitemsarea.one('span.draghome.choice' + choiceno);
|
||||
},
|
||||
drag_item_homes: function() {
|
||||
return dragitemsarea.all('span.draghome');
|
||||
},
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inputs_for_choices: function() {
|
||||
return topnode.all('input.choices');
|
||||
},
|
||||
input_for_choice: function(choiceno) {
|
||||
return topnode.one('input.choice' + choiceno);
|
||||
},
|
||||
marker_texts: function() {
|
||||
return topnode.one('div.markertexts');
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
colours: ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8',
|
||||
'#87CEFA', '#DAA520', '#FFD700', '#F0E68C'],
|
||||
nextcolourindex: 0,
|
||||
restart_colours: function() {
|
||||
this.nextcolourindex = 0;
|
||||
},
|
||||
get_next_colour: function() {
|
||||
var colour = this.colours[this.nextcolourindex];
|
||||
this.nextcolourindex++;
|
||||
if (this.nextcolourindex === this.colours.length) {
|
||||
this.nextcolourindex = 0;
|
||||
}
|
||||
return colour;
|
||||
},
|
||||
convert_to_window_xy: function(bgimgxy) {
|
||||
// The +1 seems rather odd, but seems to give the best results in
|
||||
// the three main browsers at a range of zoom levels.
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
},
|
||||
shapes: [],
|
||||
draw_drop_zone: function(dropzoneno, markertext, shape, coords, colour, link) {
|
||||
var existingmarkertext;
|
||||
if (link) {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno + ' a');
|
||||
} else {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno);
|
||||
}
|
||||
|
||||
if (existingmarkertext) {
|
||||
if (markertext !== '') {
|
||||
existingmarkertext.setContent(markertext);
|
||||
} else {
|
||||
existingmarkertext.remove(true);
|
||||
}
|
||||
} else if (markertext !== '') {
|
||||
var classnames = 'markertext markertext' + dropzoneno;
|
||||
if (link) {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '"><a href="#">' +
|
||||
markertext + '</a></span>');
|
||||
} else {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '">' +
|
||||
markertext + '</span>');
|
||||
}
|
||||
}
|
||||
var drawfunc = 'draw_shape_' + shape;
|
||||
if (this[drawfunc] instanceof Function) {
|
||||
var xyfortext = this[drawfunc](dropzoneno, coords, colour);
|
||||
if (xyfortext !== null) {
|
||||
var markerspan = this.doc.top_node().one('div.ddarea div.markertexts span.markertext' + dropzoneno);
|
||||
if (markerspan !== null) {
|
||||
markerspan.setStyle('opacity', '0.6');
|
||||
xyfortext[0] -= markerspan.get('offsetWidth') / 2;
|
||||
xyfortext[1] -= markerspan.get('offsetHeight') / 2;
|
||||
markerspan.setXY(this.convert_to_window_xy(xyfortext));
|
||||
var markerspananchor = markerspan.one('a');
|
||||
if (markerspananchor !== null) {
|
||||
markerspananchor.once('click', function(e, dropzoneno) {
|
||||
var fill = this.shapes[dropzoneno].get('fill');
|
||||
fill.opacity = 1;
|
||||
this.shapes[dropzoneno].set('fill', fill);
|
||||
},
|
||||
this,
|
||||
dropzoneno
|
||||
);
|
||||
markerspananchor.set('tabIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
draw_shape_circle: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 4) {
|
||||
var xy = [Number(coordsparts[1]) - coordsparts[3], Number(coordsparts[2]) - coordsparts[3]];
|
||||
if (this.coords_in_img(xy)) {
|
||||
var widthheight = [Number(coordsparts[3]) * 2, Number(coordsparts[3]) * 2];
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'circle',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
draw_shape_rectangle: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+),(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 5) {
|
||||
var xy = [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
var widthheight = [Number(coordsparts[3]), Number(coordsparts[4])];
|
||||
if (this.coords_in_img([xy[0] + widthheight[0], xy[1] + widthheight[1]])) {
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'rect',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(xy[0]) + widthheight[0] / 2, Number(xy[1]) + widthheight[1] / 2];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
},
|
||||
draw_shape_polygon: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.split(';');
|
||||
var xy = [];
|
||||
for (var i in coordsparts) {
|
||||
var parts = coordsparts[i].match(/^(\d+),(\d+)$/);
|
||||
if (parts !== null && this.coords_in_img([parts[1], parts[2]])) {
|
||||
xy[xy.length] = [parts[1], parts[2]];
|
||||
}
|
||||
}
|
||||
if (xy.length > 2) {
|
||||
var polygon = this.graphics.addShape({
|
||||
type: "path",
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
},
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
}
|
||||
});
|
||||
var maxxy = [0, 0];
|
||||
var minxy = [this.doc.bg_img().get('width'), this.doc.bg_img().get('height')];
|
||||
for (i = 0; i < xy.length; i++) {
|
||||
// calculate min and max points to find center to show marker on
|
||||
minxy[0] = Math.min(xy[i][0], minxy[0]);
|
||||
minxy[1] = Math.min(xy[i][1], minxy[1]);
|
||||
maxxy[0] = Math.max(xy[i][0], maxxy[0]);
|
||||
maxxy[1] = Math.max(xy[i][1], maxxy[1]);
|
||||
if (i === 0) {
|
||||
polygon.moveTo(xy[i][0], xy[i][1]);
|
||||
} else {
|
||||
polygon.lineTo(xy[i][0], xy[i][1]);
|
||||
}
|
||||
}
|
||||
if (Number(xy[0][0]) !== Number(xy[xy.length - 1][0]) || Number(xy[0][1]) !== Number(xy[xy.length - 1][1])) {
|
||||
polygon.lineTo(xy[0][0], xy[0][1]); // Close polygon if not already closed.
|
||||
}
|
||||
polygon.end();
|
||||
polygon.setXY(this.doc.bg_img().getXY());
|
||||
this.shapes[dropzoneno] = polygon;
|
||||
return [(minxy[0] + maxxy[0]) / 2, (minxy[1] + maxxy[1]) / 2];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
coords_in_img: function(coords) {
|
||||
return (coords[0] <= this.doc.bg_img().get('width') &&
|
||||
coords[1] <= this.doc.bg_img().get('height'));
|
||||
}
|
||||
}, {
|
||||
NAME: DDMARKERDDNAME,
|
||||
ATTRS: {
|
||||
drops: {value: null},
|
||||
readonly: {value: false},
|
||||
topnode: {value: null}
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.dd_base_class = DDMARKER_DD;
|
||||
|
||||
var DDMARKERQUESTIONNAME = 'ddmarker_question';
|
||||
var DDMARKER_QUESTION = function() {
|
||||
DDMARKER_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDMARKER_QUESTION, M.qtype_ddmarker.dd_base_class, {
|
||||
passiveSupported: false,
|
||||
pendingid: '',
|
||||
initializer: function() {
|
||||
this.pendingid = 'qtype_ddmarker-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.after_image_load);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.after_image_load);
|
||||
this.checkPassiveSupported();
|
||||
},
|
||||
after_image_load: function() {
|
||||
this.redraw_drags_and_drops();
|
||||
M.util.js_complete(this.pendingid);
|
||||
Y.later(2000, this, this.redraw_drags_and_drops, [], true);
|
||||
},
|
||||
clone_new_drag_item: function(draghome, itemno) {
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('dragitem');
|
||||
drag.addClass('item' + itemno);
|
||||
drag.one('span.markertext').setStyle('opacity', 0.6);
|
||||
draghome.insert(drag, 'after');
|
||||
if (!this.get('readonly')) {
|
||||
this.draggable(drag);
|
||||
}
|
||||
return 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;
|
||||
}
|
||||
},
|
||||
|
||||
draggable: function(drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'intersect'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: this.doc.top_node()});
|
||||
dd.after('drag:start', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.addClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
var itemno = this.get_itemno_for_node(dragnode);
|
||||
if (itemno !== null) {
|
||||
dragnode.removeClass('item' + dragnode);
|
||||
}
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
dd.after('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.removeClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
this.save_all_xy_for_choice(choiceno, dragnode);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
// --- keyboard accessibility
|
||||
drag.set('tabIndex', 0);
|
||||
drag.on('dragchange', this.drop_zone_key_press, this);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(drag);
|
||||
},
|
||||
|
||||
save_all_xy_for_choice: function(choiceno, dropped) {
|
||||
var coords = [];
|
||||
var bgimgxy;
|
||||
for (var i = 0; i <= this.doc.drag_items_for_choice(choiceno).size(); i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (dragitem) {
|
||||
dragitem.removeClass('item' + i);
|
||||
if (!dragitem.hasClass('beingdragged')) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dragitem.getXY());
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
dragitem.removeClass('item' + i);
|
||||
dragitem.addClass('item' + coords.length);
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dropped !== null) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dropped.getXY());
|
||||
dropped.addClass('item' + coords.length);
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
this.set_form_value(choiceno, coords.join(';'));
|
||||
},
|
||||
reset_drag_xy: function(choiceno) {
|
||||
this.set_form_value(choiceno, '');
|
||||
},
|
||||
set_form_value: function(choiceno, value) {
|
||||
this.doc.input_for_choice(choiceno).set('value', value);
|
||||
},
|
||||
// make sure xy value is not out of bounds of bg image
|
||||
xy_in_bgimg: function(bgimgxy) {
|
||||
if ((bgimgxy[0] < 0) ||
|
||||
(bgimgxy[1] < 0) ||
|
||||
(bgimgxy[0] > this.doc.bg_img().get('width')) ||
|
||||
(bgimgxy[1] > this.doc.bg_img().get('height'))) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
constrain_to_bgimg: function(windowxy) {
|
||||
var bgimgxy = this.convert_to_bg_img_xy(windowxy);
|
||||
bgimgxy[0] = Math.max(0, bgimgxy[0]);
|
||||
bgimgxy[1] = Math.max(0, bgimgxy[1]);
|
||||
bgimgxy[0] = Math.min(this.doc.bg_img().get('width'), bgimgxy[0]);
|
||||
bgimgxy[1] = Math.min(this.doc.bg_img().get('height'), bgimgxy[1]);
|
||||
return this.convert_to_window_xy(bgimgxy);
|
||||
},
|
||||
convert_to_bg_img_xy: function(windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
redraw_drags_and_drops: function() {
|
||||
this.doc.drag_items().each(function(item) {
|
||||
// if (!item.hasClass('beingdragged')){
|
||||
item.addClass('unneeded');
|
||||
// }
|
||||
}, this);
|
||||
this.doc.inputs_for_choices().each(function(input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var coords = this.get_coords(input);
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (!dragitem || dragitem.hasClass('beingdragged')) {
|
||||
dragitem = this.clone_new_drag_item(dragitemhome, i);
|
||||
} else {
|
||||
dragitem.removeClass('unneeded');
|
||||
}
|
||||
dragitem.setXY(coords[i]);
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(item) {
|
||||
if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
|
||||
item.remove(true);
|
||||
}
|
||||
}, this);
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.clear();
|
||||
} else {
|
||||
this.graphics = new Y.Graphic(
|
||||
{render: this.doc.top_node().one("div.ddarea div.dropzones")}
|
||||
);
|
||||
}
|
||||
if (this.get('dropzones').length !== 0) {
|
||||
this.restart_colours();
|
||||
for (var dropzoneno in this.get('dropzones')) {
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
var d = this.get('dropzones')[dropzoneno];
|
||||
this.draw_drop_zone(dropzoneno, d.markertext,
|
||||
d.shape, d.coords, colourfordropzone, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine what drag items need to be shown and
|
||||
* return coords of all drag items except any that are currently being dragged
|
||||
* based on contents of hidden inputs and whether drags are 'infinite' or how many drags should be shown.
|
||||
*/
|
||||
get_coords: function(input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var fv = input.get('value');
|
||||
var infinite = input.hasClass('infinite');
|
||||
var noofdrags = this.get_noofdrags_for_node(input);
|
||||
var dragging = (null !== this.doc.drag_item_being_dragged(choiceno));
|
||||
var coords = [];
|
||||
if (fv !== '') {
|
||||
var coordsstrings = fv.split(';');
|
||||
for (var i = 0; i < coordsstrings.length; i++) {
|
||||
coords[coords.length] = this.convert_to_window_xy(coordsstrings[i].split(','));
|
||||
}
|
||||
}
|
||||
var displayeddrags = coords.length + (dragging ? 1 : 0);
|
||||
if (infinite || (displayeddrags < noofdrags)) {
|
||||
coords[coords.length] = this.drag_home_xy(choiceno);
|
||||
}
|
||||
return coords;
|
||||
},
|
||||
drag_home_xy: function(choiceno) {
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
return [dragitemhome.getX(), dragitemhome.getY() - 12];
|
||||
},
|
||||
get_choiceno_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'choice'));
|
||||
},
|
||||
get_itemno_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'item'));
|
||||
},
|
||||
get_noofdrags_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'noofdrags'));
|
||||
},
|
||||
|
||||
// Keyboard accessibility stuff below here.
|
||||
drop_zone_key_press: function(e) {
|
||||
var dragitem = e.target;
|
||||
var xy = dragitem.getXY();
|
||||
switch (e.direction) {
|
||||
case 'left' :
|
||||
xy[0] -= 1;
|
||||
break;
|
||||
case 'right' :
|
||||
xy[0] += 1;
|
||||
break;
|
||||
case 'down' :
|
||||
xy[1] += 1;
|
||||
break;
|
||||
case 'up' :
|
||||
xy[1] -= 1;
|
||||
break;
|
||||
case 'remove' :
|
||||
xy = null;
|
||||
break;
|
||||
}
|
||||
var choiceno = this.get_choiceno_for_node(dragitem);
|
||||
if (xy !== null) {
|
||||
xy = this.constrain_to_bgimg(xy);
|
||||
} else {
|
||||
xy = this.drag_home_xy(choiceno);
|
||||
}
|
||||
e.preventDefault();
|
||||
dragitem.setXY(xy);
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
}
|
||||
}, {NAME: DDMARKERQUESTIONNAME, ATTRS: {dropzones: {value: []}}});
|
||||
|
||||
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': 'remove', // Space
|
||||
'37': 'left', // Left arrow
|
||||
'38': 'up', // Up arrow
|
||||
'39': 'right', // Right arrow
|
||||
'40': 'down', // Down arrow
|
||||
'65': 'left', // a
|
||||
'87': 'up', // w
|
||||
'68': 'right', // d
|
||||
'83': 'down', // s
|
||||
'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_ddmarker.init_question = function(config) {
|
||||
return new DDMARKER_QUESTION(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["node", "event-resize", "dd", "dd-drop", "dd-constrain", "graphics"]});
|
File diff suppressed because one or more lines are too long
@ -1,616 +0,0 @@
|
||||
YUI.add('moodle-qtype_ddmarker-dd', function (Y, NAME) {
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
var DDMARKERDDNAME = 'moodle-qtype_ddmarker-dd';
|
||||
var DDMARKER_DD = function() {
|
||||
DDMARKER_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDMARKER_DD, Y.Base, {
|
||||
doc: null,
|
||||
polltimer: null,
|
||||
afterimageloaddone: false,
|
||||
graphics: null,
|
||||
poll_for_image_load: function(e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
if (bgdone) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure: function() {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node: function() {
|
||||
return topnode;
|
||||
},
|
||||
bg_img: function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img: function(url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
drag_items: function() {
|
||||
return dragitemsarea.all('.dragitem');
|
||||
},
|
||||
drag_items_for_choice: function(choiceno) {
|
||||
return dragitemsarea.all('span.dragitem.choice' + choiceno);
|
||||
},
|
||||
drag_item_for_choice: function(choiceno, itemno) {
|
||||
return dragitemsarea.one('span.dragitem.choice' + choiceno +
|
||||
'.item' + itemno);
|
||||
},
|
||||
drag_item_being_dragged: function(choiceno) {
|
||||
return dragitemsarea.one('span.dragitem.beingdragged.choice' + choiceno);
|
||||
},
|
||||
drag_item_home: function(choiceno) {
|
||||
return dragitemsarea.one('span.draghome.choice' + choiceno);
|
||||
},
|
||||
drag_item_homes: function() {
|
||||
return dragitemsarea.all('span.draghome');
|
||||
},
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inputs_for_choices: function() {
|
||||
return topnode.all('input.choices');
|
||||
},
|
||||
input_for_choice: function(choiceno) {
|
||||
return topnode.one('input.choice' + choiceno);
|
||||
},
|
||||
marker_texts: function() {
|
||||
return topnode.one('div.markertexts');
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
colours: ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8',
|
||||
'#87CEFA', '#DAA520', '#FFD700', '#F0E68C'],
|
||||
nextcolourindex: 0,
|
||||
restart_colours: function() {
|
||||
this.nextcolourindex = 0;
|
||||
},
|
||||
get_next_colour: function() {
|
||||
var colour = this.colours[this.nextcolourindex];
|
||||
this.nextcolourindex++;
|
||||
if (this.nextcolourindex === this.colours.length) {
|
||||
this.nextcolourindex = 0;
|
||||
}
|
||||
return colour;
|
||||
},
|
||||
convert_to_window_xy: function(bgimgxy) {
|
||||
// The +1 seems rather odd, but seems to give the best results in
|
||||
// the three main browsers at a range of zoom levels.
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
},
|
||||
shapes: [],
|
||||
draw_drop_zone: function(dropzoneno, markertext, shape, coords, colour, link) {
|
||||
var existingmarkertext;
|
||||
if (link) {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno + ' a');
|
||||
} else {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno);
|
||||
}
|
||||
|
||||
if (existingmarkertext) {
|
||||
if (markertext !== '') {
|
||||
existingmarkertext.setContent(markertext);
|
||||
} else {
|
||||
existingmarkertext.remove(true);
|
||||
}
|
||||
} else if (markertext !== '') {
|
||||
var classnames = 'markertext markertext' + dropzoneno;
|
||||
if (link) {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '"><a href="#">' +
|
||||
markertext + '</a></span>');
|
||||
} else {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '">' +
|
||||
markertext + '</span>');
|
||||
}
|
||||
}
|
||||
var drawfunc = 'draw_shape_' + shape;
|
||||
if (this[drawfunc] instanceof Function) {
|
||||
var xyfortext = this[drawfunc](dropzoneno, coords, colour);
|
||||
if (xyfortext !== null) {
|
||||
var markerspan = this.doc.top_node().one('div.ddarea div.markertexts span.markertext' + dropzoneno);
|
||||
if (markerspan !== null) {
|
||||
markerspan.setStyle('opacity', '0.6');
|
||||
xyfortext[0] -= markerspan.get('offsetWidth') / 2;
|
||||
xyfortext[1] -= markerspan.get('offsetHeight') / 2;
|
||||
markerspan.setXY(this.convert_to_window_xy(xyfortext));
|
||||
var markerspananchor = markerspan.one('a');
|
||||
if (markerspananchor !== null) {
|
||||
markerspananchor.once('click', function(e, dropzoneno) {
|
||||
var fill = this.shapes[dropzoneno].get('fill');
|
||||
fill.opacity = 1;
|
||||
this.shapes[dropzoneno].set('fill', fill);
|
||||
},
|
||||
this,
|
||||
dropzoneno
|
||||
);
|
||||
markerspananchor.set('tabIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
draw_shape_circle: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 4) {
|
||||
var xy = [Number(coordsparts[1]) - coordsparts[3], Number(coordsparts[2]) - coordsparts[3]];
|
||||
if (this.coords_in_img(xy)) {
|
||||
var widthheight = [Number(coordsparts[3]) * 2, Number(coordsparts[3]) * 2];
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'circle',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
draw_shape_rectangle: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+),(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 5) {
|
||||
var xy = [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
var widthheight = [Number(coordsparts[3]), Number(coordsparts[4])];
|
||||
if (this.coords_in_img([xy[0] + widthheight[0], xy[1] + widthheight[1]])) {
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'rect',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(xy[0]) + widthheight[0] / 2, Number(xy[1]) + widthheight[1] / 2];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
},
|
||||
draw_shape_polygon: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.split(';');
|
||||
var xy = [];
|
||||
for (var i in coordsparts) {
|
||||
var parts = coordsparts[i].match(/^(\d+),(\d+)$/);
|
||||
if (parts !== null && this.coords_in_img([parts[1], parts[2]])) {
|
||||
xy[xy.length] = [parts[1], parts[2]];
|
||||
}
|
||||
}
|
||||
if (xy.length > 2) {
|
||||
var polygon = this.graphics.addShape({
|
||||
type: "path",
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
},
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
}
|
||||
});
|
||||
var maxxy = [0, 0];
|
||||
var minxy = [this.doc.bg_img().get('width'), this.doc.bg_img().get('height')];
|
||||
for (i = 0; i < xy.length; i++) {
|
||||
// calculate min and max points to find center to show marker on
|
||||
minxy[0] = Math.min(xy[i][0], minxy[0]);
|
||||
minxy[1] = Math.min(xy[i][1], minxy[1]);
|
||||
maxxy[0] = Math.max(xy[i][0], maxxy[0]);
|
||||
maxxy[1] = Math.max(xy[i][1], maxxy[1]);
|
||||
if (i === 0) {
|
||||
polygon.moveTo(xy[i][0], xy[i][1]);
|
||||
} else {
|
||||
polygon.lineTo(xy[i][0], xy[i][1]);
|
||||
}
|
||||
}
|
||||
if (Number(xy[0][0]) !== Number(xy[xy.length - 1][0]) || Number(xy[0][1]) !== Number(xy[xy.length - 1][1])) {
|
||||
polygon.lineTo(xy[0][0], xy[0][1]); // Close polygon if not already closed.
|
||||
}
|
||||
polygon.end();
|
||||
polygon.setXY(this.doc.bg_img().getXY());
|
||||
this.shapes[dropzoneno] = polygon;
|
||||
return [(minxy[0] + maxxy[0]) / 2, (minxy[1] + maxxy[1]) / 2];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
coords_in_img: function(coords) {
|
||||
return (coords[0] <= this.doc.bg_img().get('width') &&
|
||||
coords[1] <= this.doc.bg_img().get('height'));
|
||||
}
|
||||
}, {
|
||||
NAME: DDMARKERDDNAME,
|
||||
ATTRS: {
|
||||
drops: {value: null},
|
||||
readonly: {value: false},
|
||||
topnode: {value: null}
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.dd_base_class = DDMARKER_DD;
|
||||
|
||||
var DDMARKERQUESTIONNAME = 'ddmarker_question';
|
||||
var DDMARKER_QUESTION = function() {
|
||||
DDMARKER_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDMARKER_QUESTION, M.qtype_ddmarker.dd_base_class, {
|
||||
passiveSupported: false,
|
||||
pendingid: '',
|
||||
initializer: function() {
|
||||
this.pendingid = 'qtype_ddmarker-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.after_image_load);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.after_image_load);
|
||||
this.checkPassiveSupported();
|
||||
},
|
||||
after_image_load: function() {
|
||||
this.redraw_drags_and_drops();
|
||||
M.util.js_complete(this.pendingid);
|
||||
Y.later(2000, this, this.redraw_drags_and_drops, [], true);
|
||||
},
|
||||
clone_new_drag_item: function(draghome, itemno) {
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('dragitem');
|
||||
drag.addClass('item' + itemno);
|
||||
drag.one('span.markertext').setStyle('opacity', 0.6);
|
||||
draghome.insert(drag, 'after');
|
||||
if (!this.get('readonly')) {
|
||||
this.draggable(drag);
|
||||
}
|
||||
return 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;
|
||||
}
|
||||
},
|
||||
|
||||
draggable: function(drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'intersect'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: this.doc.top_node()});
|
||||
dd.after('drag:start', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.addClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
var itemno = this.get_itemno_for_node(dragnode);
|
||||
if (itemno !== null) {
|
||||
dragnode.removeClass('item' + dragnode);
|
||||
}
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
dd.after('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.removeClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
this.save_all_xy_for_choice(choiceno, dragnode);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
// --- keyboard accessibility
|
||||
drag.set('tabIndex', 0);
|
||||
drag.on('dragchange', this.drop_zone_key_press, this);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(drag);
|
||||
},
|
||||
|
||||
save_all_xy_for_choice: function(choiceno, dropped) {
|
||||
var coords = [];
|
||||
var bgimgxy;
|
||||
for (var i = 0; i <= this.doc.drag_items_for_choice(choiceno).size(); i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (dragitem) {
|
||||
dragitem.removeClass('item' + i);
|
||||
if (!dragitem.hasClass('beingdragged')) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dragitem.getXY());
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
dragitem.removeClass('item' + i);
|
||||
dragitem.addClass('item' + coords.length);
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dropped !== null) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dropped.getXY());
|
||||
dropped.addClass('item' + coords.length);
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
this.set_form_value(choiceno, coords.join(';'));
|
||||
},
|
||||
reset_drag_xy: function(choiceno) {
|
||||
this.set_form_value(choiceno, '');
|
||||
},
|
||||
set_form_value: function(choiceno, value) {
|
||||
this.doc.input_for_choice(choiceno).set('value', value);
|
||||
},
|
||||
// make sure xy value is not out of bounds of bg image
|
||||
xy_in_bgimg: function(bgimgxy) {
|
||||
if ((bgimgxy[0] < 0) ||
|
||||
(bgimgxy[1] < 0) ||
|
||||
(bgimgxy[0] > this.doc.bg_img().get('width')) ||
|
||||
(bgimgxy[1] > this.doc.bg_img().get('height'))) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
constrain_to_bgimg: function(windowxy) {
|
||||
var bgimgxy = this.convert_to_bg_img_xy(windowxy);
|
||||
bgimgxy[0] = Math.max(0, bgimgxy[0]);
|
||||
bgimgxy[1] = Math.max(0, bgimgxy[1]);
|
||||
bgimgxy[0] = Math.min(this.doc.bg_img().get('width'), bgimgxy[0]);
|
||||
bgimgxy[1] = Math.min(this.doc.bg_img().get('height'), bgimgxy[1]);
|
||||
return this.convert_to_window_xy(bgimgxy);
|
||||
},
|
||||
convert_to_bg_img_xy: function(windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
redraw_drags_and_drops: function() {
|
||||
this.doc.drag_items().each(function(item) {
|
||||
// if (!item.hasClass('beingdragged')){
|
||||
item.addClass('unneeded');
|
||||
// }
|
||||
}, this);
|
||||
this.doc.inputs_for_choices().each(function(input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var coords = this.get_coords(input);
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (!dragitem || dragitem.hasClass('beingdragged')) {
|
||||
dragitem = this.clone_new_drag_item(dragitemhome, i);
|
||||
} else {
|
||||
dragitem.removeClass('unneeded');
|
||||
}
|
||||
dragitem.setXY(coords[i]);
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(item) {
|
||||
if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
|
||||
item.remove(true);
|
||||
}
|
||||
}, this);
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.clear();
|
||||
} else {
|
||||
this.graphics = new Y.Graphic(
|
||||
{render: this.doc.top_node().one("div.ddarea div.dropzones")}
|
||||
);
|
||||
}
|
||||
if (this.get('dropzones').length !== 0) {
|
||||
this.restart_colours();
|
||||
for (var dropzoneno in this.get('dropzones')) {
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
var d = this.get('dropzones')[dropzoneno];
|
||||
this.draw_drop_zone(dropzoneno, d.markertext,
|
||||
d.shape, d.coords, colourfordropzone, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine what drag items need to be shown and
|
||||
* return coords of all drag items except any that are currently being dragged
|
||||
* based on contents of hidden inputs and whether drags are 'infinite' or how many drags should be shown.
|
||||
*/
|
||||
get_coords: function(input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var fv = input.get('value');
|
||||
var infinite = input.hasClass('infinite');
|
||||
var noofdrags = this.get_noofdrags_for_node(input);
|
||||
var dragging = (null !== this.doc.drag_item_being_dragged(choiceno));
|
||||
var coords = [];
|
||||
if (fv !== '') {
|
||||
var coordsstrings = fv.split(';');
|
||||
for (var i = 0; i < coordsstrings.length; i++) {
|
||||
coords[coords.length] = this.convert_to_window_xy(coordsstrings[i].split(','));
|
||||
}
|
||||
}
|
||||
var displayeddrags = coords.length + (dragging ? 1 : 0);
|
||||
if (infinite || (displayeddrags < noofdrags)) {
|
||||
coords[coords.length] = this.drag_home_xy(choiceno);
|
||||
}
|
||||
return coords;
|
||||
},
|
||||
drag_home_xy: function(choiceno) {
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
return [dragitemhome.getX(), dragitemhome.getY() - 12];
|
||||
},
|
||||
get_choiceno_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'choice'));
|
||||
},
|
||||
get_itemno_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'item'));
|
||||
},
|
||||
get_noofdrags_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'noofdrags'));
|
||||
},
|
||||
|
||||
// Keyboard accessibility stuff below here.
|
||||
drop_zone_key_press: function(e) {
|
||||
var dragitem = e.target;
|
||||
var xy = dragitem.getXY();
|
||||
switch (e.direction) {
|
||||
case 'left' :
|
||||
xy[0] -= 1;
|
||||
break;
|
||||
case 'right' :
|
||||
xy[0] += 1;
|
||||
break;
|
||||
case 'down' :
|
||||
xy[1] += 1;
|
||||
break;
|
||||
case 'up' :
|
||||
xy[1] -= 1;
|
||||
break;
|
||||
case 'remove' :
|
||||
xy = null;
|
||||
break;
|
||||
}
|
||||
var choiceno = this.get_choiceno_for_node(dragitem);
|
||||
if (xy !== null) {
|
||||
xy = this.constrain_to_bgimg(xy);
|
||||
} else {
|
||||
xy = this.drag_home_xy(choiceno);
|
||||
}
|
||||
e.preventDefault();
|
||||
dragitem.setXY(xy);
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
}
|
||||
}, {NAME: DDMARKERQUESTIONNAME, ATTRS: {dropzones: {value: []}}});
|
||||
|
||||
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': 'remove', // Space
|
||||
'37': 'left', // Left arrow
|
||||
'38': 'up', // Up arrow
|
||||
'39': 'right', // Right arrow
|
||||
'40': 'down', // Down arrow
|
||||
'65': 'left', // a
|
||||
'87': 'up', // w
|
||||
'68': 'right', // d
|
||||
'83': 'down', // s
|
||||
'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_ddmarker.init_question = function(config) {
|
||||
return new DDMARKER_QUESTION(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["node", "event-resize", "dd", "dd-drop", "dd-constrain", "graphics"]});
|
@ -1,290 +0,0 @@
|
||||
YUI.add('moodle-qtype_ddmarker-form', function (Y, NAME) {
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDMARKERFORMNAME = 'moodle-qtype_ddmarker-form';
|
||||
var DDMARKER_FORM = function() {
|
||||
DDMARKER_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.extend(DDMARKER_FORM, M.qtype_ddmarker.dd_base_class, {
|
||||
fp: null,
|
||||
|
||||
initializer: function() {
|
||||
var pendingid = 'qtype_ddmarker-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append(
|
||||
'<div class="ddarea">' +
|
||||
'<div class="markertexts"></div>' +
|
||||
'<div class="droparea"></div>' +
|
||||
'<div class="dropzones"></div>' +
|
||||
'<ul class="pager unstyled list-unstyled">' +
|
||||
'<li><span id="xcoordpreview">X = </span></li>' +
|
||||
'<li><span id="ycoordpreview">Y = </span></li>' +
|
||||
'</ul>' +
|
||||
'<div class="grid"></div>' +
|
||||
'</div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.update_drop_zones, [pendingid], true);
|
||||
Y.after(this.load_bg_image, M.form_filepicker, 'callback', this);
|
||||
this.load_bg_image();
|
||||
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
topnode.one('.grid').on('mousemove', function (e) {
|
||||
var img = topnode.one('.dropbackground');
|
||||
var x = Math.round(Number(e.pageX) - img.getX() - 1);
|
||||
var y = Math.round(Number(e.pageY) - img.getY() - 1);
|
||||
topnode.one('#xcoordpreview').setHTML("X = " + x);
|
||||
topnode.one('#ycoordpreview').setHTML("Y = " + y);
|
||||
});
|
||||
},
|
||||
|
||||
load_bg_image: function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
|
||||
// Listen for a drop:hit on the background image.
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this);
|
||||
}
|
||||
},
|
||||
|
||||
constrain_image_size: function(e) {
|
||||
var maxsize = this.get('maxsizes').bgimage;
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
update_drop_zones: function(pendingid) {
|
||||
|
||||
// Set up drop zones.
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.destroy();
|
||||
}
|
||||
this.restart_colours();
|
||||
this.graphics = new Y.Graphic({render: "div.ddarea div.dropzones"});
|
||||
var noofdropzones = this.form.get_form_value('nodropzone', []);
|
||||
for (var dropzoneno = 0; dropzoneno < noofdropzones; dropzoneno++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [dropzoneno, 'choice']);
|
||||
var markertext = this.get_marker_text(dragitemno);
|
||||
var shape = this.form.get_form_value('drops', [dropzoneno, 'shape']);
|
||||
var coords = this.get_coords(dropzoneno);
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
Y.one('input#id_drops_' + dropzoneno + '_coords')
|
||||
.setStyle('background-color', colourfordropzone);
|
||||
this.draw_drop_zone(dropzoneno, markertext,
|
||||
shape, coords, colourfordropzone, false);
|
||||
}
|
||||
if (this.doc.bg_img()) {
|
||||
Y.one('div.ddarea .grid')
|
||||
.setXY(this.convert_to_window_xy([0, 0]))
|
||||
.setStyle('width', this.doc.bg_img().get('width'))
|
||||
.setStyle('height', this.doc.bg_img().get('height'));
|
||||
}
|
||||
M.util.js_complete(pendingid);
|
||||
},
|
||||
|
||||
get_coords: function(dropzoneno) {
|
||||
var coords = this.form.get_form_value('drops', [dropzoneno, 'coords']);
|
||||
return coords.replace(new RegExp("\\s*", 'g'), '');
|
||||
},
|
||||
get_marker_text: function(markerno) {
|
||||
if (Number(markerno) !== 0) {
|
||||
var label = this.form.get_form_value('drags', [markerno - 1, 'label']);
|
||||
return label.replace(new RegExp("^\\s*(.*)\\s*$"), "$1");
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors: function() {
|
||||
var dragitemsoptions = {'0': ''};
|
||||
for (var i = 1; i <= this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.get_marker_text(i);
|
||||
if (label !== "") {
|
||||
dragitemsoptions[i] = Y.Escape.html(label);
|
||||
}
|
||||
}
|
||||
// Get all the currently selected drags for each drop.
|
||||
var selectedvalues = [];
|
||||
var selector;
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
selectedvalues[i] = Number(selector.get('value'));
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
// Remove all options for drag choice.
|
||||
selector.all('option').remove(true);
|
||||
// And recreate the options.
|
||||
for (var value in dragitemsoptions) {
|
||||
value = Number(value);
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
// Is this the currently selected value?
|
||||
if (value === selectedvalues[i]) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
// It is not the currently selected value, is it selectable?
|
||||
if (value !== 0) { // The 'no item' option is always selectable.
|
||||
// Variables to hold form values about this drag item.
|
||||
var noofdrags = this.form.get_form_value('drags', [value - 1, 'noofdrags']);
|
||||
if (Number(noofdrags) !== 0) { // 'noofdrags == 0' means infinite.
|
||||
// Go through all selected values in drop downs.
|
||||
for (var k in selectedvalues) {
|
||||
// Count down 'noofdrags' and if reach zero then set disabled option for this drag item.
|
||||
if (Number(selectedvalues[k]) === value) {
|
||||
if (Number(noofdrags) === 1) {
|
||||
optionnode.set('disabled', true);
|
||||
break;
|
||||
} else {
|
||||
noofdrags--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events: function() {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events: function() {
|
||||
// events triggered by changes to form data
|
||||
|
||||
// Changes to labels.
|
||||
Y.all('fieldset#id_draggableitemheader input').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Changes to selected drag item.
|
||||
Y.all('fieldset#id_draggableitemheader select').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form: {
|
||||
to_name_with_index: function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el: function(name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value: function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value: function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index: function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers: function() {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file: function(name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href: fileanchor.get('href'), name: fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href: null, name: null};
|
||||
}
|
||||
},
|
||||
name: function(draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
}, {NAME: DDMARKERFORMNAME, ATTRS: {maxsizes: {value: null}}});
|
||||
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.init_form = function(config) {
|
||||
return new DDMARKER_FORM(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["moodle-qtype_ddmarker-dd", "form_filepicker", "graphics", "escape"]});
|
File diff suppressed because one or more lines are too long
@ -1,290 +0,0 @@
|
||||
YUI.add('moodle-qtype_ddmarker-form', function (Y, NAME) {
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDMARKERFORMNAME = 'moodle-qtype_ddmarker-form';
|
||||
var DDMARKER_FORM = function() {
|
||||
DDMARKER_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.extend(DDMARKER_FORM, M.qtype_ddmarker.dd_base_class, {
|
||||
fp: null,
|
||||
|
||||
initializer: function() {
|
||||
var pendingid = 'qtype_ddmarker-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append(
|
||||
'<div class="ddarea">' +
|
||||
'<div class="markertexts"></div>' +
|
||||
'<div class="droparea"></div>' +
|
||||
'<div class="dropzones"></div>' +
|
||||
'<ul class="pager unstyled list-unstyled">' +
|
||||
'<li><span id="xcoordpreview">X = </span></li>' +
|
||||
'<li><span id="ycoordpreview">Y = </span></li>' +
|
||||
'</ul>' +
|
||||
'<div class="grid"></div>' +
|
||||
'</div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.update_drop_zones, [pendingid], true);
|
||||
Y.after(this.load_bg_image, M.form_filepicker, 'callback', this);
|
||||
this.load_bg_image();
|
||||
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
topnode.one('.grid').on('mousemove', function (e) {
|
||||
var img = topnode.one('.dropbackground');
|
||||
var x = Math.round(Number(e.pageX) - img.getX() - 1);
|
||||
var y = Math.round(Number(e.pageY) - img.getY() - 1);
|
||||
topnode.one('#xcoordpreview').setHTML("X = " + x);
|
||||
topnode.one('#ycoordpreview').setHTML("Y = " + y);
|
||||
});
|
||||
},
|
||||
|
||||
load_bg_image: function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
|
||||
// Listen for a drop:hit on the background image.
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this);
|
||||
}
|
||||
},
|
||||
|
||||
constrain_image_size: function(e) {
|
||||
var maxsize = this.get('maxsizes').bgimage;
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
update_drop_zones: function(pendingid) {
|
||||
|
||||
// Set up drop zones.
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.destroy();
|
||||
}
|
||||
this.restart_colours();
|
||||
this.graphics = new Y.Graphic({render: "div.ddarea div.dropzones"});
|
||||
var noofdropzones = this.form.get_form_value('nodropzone', []);
|
||||
for (var dropzoneno = 0; dropzoneno < noofdropzones; dropzoneno++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [dropzoneno, 'choice']);
|
||||
var markertext = this.get_marker_text(dragitemno);
|
||||
var shape = this.form.get_form_value('drops', [dropzoneno, 'shape']);
|
||||
var coords = this.get_coords(dropzoneno);
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
Y.one('input#id_drops_' + dropzoneno + '_coords')
|
||||
.setStyle('background-color', colourfordropzone);
|
||||
this.draw_drop_zone(dropzoneno, markertext,
|
||||
shape, coords, colourfordropzone, false);
|
||||
}
|
||||
if (this.doc.bg_img()) {
|
||||
Y.one('div.ddarea .grid')
|
||||
.setXY(this.convert_to_window_xy([0, 0]))
|
||||
.setStyle('width', this.doc.bg_img().get('width'))
|
||||
.setStyle('height', this.doc.bg_img().get('height'));
|
||||
}
|
||||
M.util.js_complete(pendingid);
|
||||
},
|
||||
|
||||
get_coords: function(dropzoneno) {
|
||||
var coords = this.form.get_form_value('drops', [dropzoneno, 'coords']);
|
||||
return coords.replace(new RegExp("\\s*", 'g'), '');
|
||||
},
|
||||
get_marker_text: function(markerno) {
|
||||
if (Number(markerno) !== 0) {
|
||||
var label = this.form.get_form_value('drags', [markerno - 1, 'label']);
|
||||
return label.replace(new RegExp("^\\s*(.*)\\s*$"), "$1");
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors: function() {
|
||||
var dragitemsoptions = {'0': ''};
|
||||
for (var i = 1; i <= this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.get_marker_text(i);
|
||||
if (label !== "") {
|
||||
dragitemsoptions[i] = Y.Escape.html(label);
|
||||
}
|
||||
}
|
||||
// Get all the currently selected drags for each drop.
|
||||
var selectedvalues = [];
|
||||
var selector;
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
selectedvalues[i] = Number(selector.get('value'));
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
// Remove all options for drag choice.
|
||||
selector.all('option').remove(true);
|
||||
// And recreate the options.
|
||||
for (var value in dragitemsoptions) {
|
||||
value = Number(value);
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
// Is this the currently selected value?
|
||||
if (value === selectedvalues[i]) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
// It is not the currently selected value, is it selectable?
|
||||
if (value !== 0) { // The 'no item' option is always selectable.
|
||||
// Variables to hold form values about this drag item.
|
||||
var noofdrags = this.form.get_form_value('drags', [value - 1, 'noofdrags']);
|
||||
if (Number(noofdrags) !== 0) { // 'noofdrags == 0' means infinite.
|
||||
// Go through all selected values in drop downs.
|
||||
for (var k in selectedvalues) {
|
||||
// Count down 'noofdrags' and if reach zero then set disabled option for this drag item.
|
||||
if (Number(selectedvalues[k]) === value) {
|
||||
if (Number(noofdrags) === 1) {
|
||||
optionnode.set('disabled', true);
|
||||
break;
|
||||
} else {
|
||||
noofdrags--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events: function() {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events: function() {
|
||||
// events triggered by changes to form data
|
||||
|
||||
// Changes to labels.
|
||||
Y.all('fieldset#id_draggableitemheader input').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Changes to selected drag item.
|
||||
Y.all('fieldset#id_draggableitemheader select').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form: {
|
||||
to_name_with_index: function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el: function(name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value: function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value: function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index: function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers: function() {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file: function(name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href: fileanchor.get('href'), name: fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href: null, name: null};
|
||||
}
|
||||
},
|
||||
name: function(draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
}, {NAME: DDMARKERFORMNAME, ATTRS: {maxsizes: {value: null}}});
|
||||
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.init_form = function(config) {
|
||||
return new DDMARKER_FORM(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["moodle-qtype_ddmarker-dd", "form_filepicker", "graphics", "escape"]});
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "moodle-qtype_ddmarker-dd",
|
||||
"builds": {
|
||||
"moodle-qtype_ddmarker-dd": {
|
||||
"jsfiles": [
|
||||
"ddmarker.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,611 +0,0 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
var DDMARKERDDNAME = 'moodle-qtype_ddmarker-dd';
|
||||
var DDMARKER_DD = function() {
|
||||
DDMARKER_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDMARKER_DD, Y.Base, {
|
||||
doc: null,
|
||||
polltimer: null,
|
||||
afterimageloaddone: false,
|
||||
graphics: null,
|
||||
poll_for_image_load: function(e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
if (bgdone) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure: function() {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node: function() {
|
||||
return topnode;
|
||||
},
|
||||
bg_img: function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img: function(url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
drag_items: function() {
|
||||
return dragitemsarea.all('.dragitem');
|
||||
},
|
||||
drag_items_for_choice: function(choiceno) {
|
||||
return dragitemsarea.all('span.dragitem.choice' + choiceno);
|
||||
},
|
||||
drag_item_for_choice: function(choiceno, itemno) {
|
||||
return dragitemsarea.one('span.dragitem.choice' + choiceno +
|
||||
'.item' + itemno);
|
||||
},
|
||||
drag_item_being_dragged: function(choiceno) {
|
||||
return dragitemsarea.one('span.dragitem.beingdragged.choice' + choiceno);
|
||||
},
|
||||
drag_item_home: function(choiceno) {
|
||||
return dragitemsarea.one('span.draghome.choice' + choiceno);
|
||||
},
|
||||
drag_item_homes: function() {
|
||||
return dragitemsarea.all('span.draghome');
|
||||
},
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inputs_for_choices: function() {
|
||||
return topnode.all('input.choices');
|
||||
},
|
||||
input_for_choice: function(choiceno) {
|
||||
return topnode.one('input.choice' + choiceno);
|
||||
},
|
||||
marker_texts: function() {
|
||||
return topnode.one('div.markertexts');
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
colours: ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8',
|
||||
'#87CEFA', '#DAA520', '#FFD700', '#F0E68C'],
|
||||
nextcolourindex: 0,
|
||||
restart_colours: function() {
|
||||
this.nextcolourindex = 0;
|
||||
},
|
||||
get_next_colour: function() {
|
||||
var colour = this.colours[this.nextcolourindex];
|
||||
this.nextcolourindex++;
|
||||
if (this.nextcolourindex === this.colours.length) {
|
||||
this.nextcolourindex = 0;
|
||||
}
|
||||
return colour;
|
||||
},
|
||||
convert_to_window_xy: function(bgimgxy) {
|
||||
// The +1 seems rather odd, but seems to give the best results in
|
||||
// the three main browsers at a range of zoom levels.
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
},
|
||||
shapes: [],
|
||||
draw_drop_zone: function(dropzoneno, markertext, shape, coords, colour, link) {
|
||||
var existingmarkertext;
|
||||
if (link) {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno + ' a');
|
||||
} else {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno);
|
||||
}
|
||||
|
||||
if (existingmarkertext) {
|
||||
if (markertext !== '') {
|
||||
existingmarkertext.setContent(markertext);
|
||||
} else {
|
||||
existingmarkertext.remove(true);
|
||||
}
|
||||
} else if (markertext !== '') {
|
||||
var classnames = 'markertext markertext' + dropzoneno;
|
||||
if (link) {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '"><a href="#">' +
|
||||
markertext + '</a></span>');
|
||||
} else {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '">' +
|
||||
markertext + '</span>');
|
||||
}
|
||||
}
|
||||
var drawfunc = 'draw_shape_' + shape;
|
||||
if (this[drawfunc] instanceof Function) {
|
||||
var xyfortext = this[drawfunc](dropzoneno, coords, colour);
|
||||
if (xyfortext !== null) {
|
||||
var markerspan = this.doc.top_node().one('div.ddarea div.markertexts span.markertext' + dropzoneno);
|
||||
if (markerspan !== null) {
|
||||
markerspan.setStyle('opacity', '0.6');
|
||||
xyfortext[0] -= markerspan.get('offsetWidth') / 2;
|
||||
xyfortext[1] -= markerspan.get('offsetHeight') / 2;
|
||||
markerspan.setXY(this.convert_to_window_xy(xyfortext));
|
||||
var markerspananchor = markerspan.one('a');
|
||||
if (markerspananchor !== null) {
|
||||
markerspananchor.once('click', function(e, dropzoneno) {
|
||||
var fill = this.shapes[dropzoneno].get('fill');
|
||||
fill.opacity = 1;
|
||||
this.shapes[dropzoneno].set('fill', fill);
|
||||
},
|
||||
this,
|
||||
dropzoneno
|
||||
);
|
||||
markerspananchor.set('tabIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
draw_shape_circle: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 4) {
|
||||
var xy = [Number(coordsparts[1]) - coordsparts[3], Number(coordsparts[2]) - coordsparts[3]];
|
||||
if (this.coords_in_img(xy)) {
|
||||
var widthheight = [Number(coordsparts[3]) * 2, Number(coordsparts[3]) * 2];
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'circle',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
draw_shape_rectangle: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+),(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 5) {
|
||||
var xy = [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
var widthheight = [Number(coordsparts[3]), Number(coordsparts[4])];
|
||||
if (this.coords_in_img([xy[0] + widthheight[0], xy[1] + widthheight[1]])) {
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'rect',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(xy[0]) + widthheight[0] / 2, Number(xy[1]) + widthheight[1] / 2];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
},
|
||||
draw_shape_polygon: function(dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.split(';');
|
||||
var xy = [];
|
||||
for (var i in coordsparts) {
|
||||
var parts = coordsparts[i].match(/^(\d+),(\d+)$/);
|
||||
if (parts !== null && this.coords_in_img([parts[1], parts[2]])) {
|
||||
xy[xy.length] = [parts[1], parts[2]];
|
||||
}
|
||||
}
|
||||
if (xy.length > 2) {
|
||||
var polygon = this.graphics.addShape({
|
||||
type: "path",
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
},
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
}
|
||||
});
|
||||
var maxxy = [0, 0];
|
||||
var minxy = [this.doc.bg_img().get('width'), this.doc.bg_img().get('height')];
|
||||
for (i = 0; i < xy.length; i++) {
|
||||
// calculate min and max points to find center to show marker on
|
||||
minxy[0] = Math.min(xy[i][0], minxy[0]);
|
||||
minxy[1] = Math.min(xy[i][1], minxy[1]);
|
||||
maxxy[0] = Math.max(xy[i][0], maxxy[0]);
|
||||
maxxy[1] = Math.max(xy[i][1], maxxy[1]);
|
||||
if (i === 0) {
|
||||
polygon.moveTo(xy[i][0], xy[i][1]);
|
||||
} else {
|
||||
polygon.lineTo(xy[i][0], xy[i][1]);
|
||||
}
|
||||
}
|
||||
if (Number(xy[0][0]) !== Number(xy[xy.length - 1][0]) || Number(xy[0][1]) !== Number(xy[xy.length - 1][1])) {
|
||||
polygon.lineTo(xy[0][0], xy[0][1]); // Close polygon if not already closed.
|
||||
}
|
||||
polygon.end();
|
||||
polygon.setXY(this.doc.bg_img().getXY());
|
||||
this.shapes[dropzoneno] = polygon;
|
||||
return [(minxy[0] + maxxy[0]) / 2, (minxy[1] + maxxy[1]) / 2];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
coords_in_img: function(coords) {
|
||||
return (coords[0] <= this.doc.bg_img().get('width') &&
|
||||
coords[1] <= this.doc.bg_img().get('height'));
|
||||
}
|
||||
}, {
|
||||
NAME: DDMARKERDDNAME,
|
||||
ATTRS: {
|
||||
drops: {value: null},
|
||||
readonly: {value: false},
|
||||
topnode: {value: null}
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.dd_base_class = DDMARKER_DD;
|
||||
|
||||
var DDMARKERQUESTIONNAME = 'ddmarker_question';
|
||||
var DDMARKER_QUESTION = function() {
|
||||
DDMARKER_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDMARKER_QUESTION, M.qtype_ddmarker.dd_base_class, {
|
||||
passiveSupported: false,
|
||||
pendingid: '',
|
||||
initializer: function() {
|
||||
this.pendingid = 'qtype_ddmarker-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.after_image_load);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.after_image_load);
|
||||
this.checkPassiveSupported();
|
||||
},
|
||||
after_image_load: function() {
|
||||
this.redraw_drags_and_drops();
|
||||
M.util.js_complete(this.pendingid);
|
||||
Y.later(2000, this, this.redraw_drags_and_drops, [], true);
|
||||
},
|
||||
clone_new_drag_item: function(draghome, itemno) {
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('dragitem');
|
||||
drag.addClass('item' + itemno);
|
||||
drag.one('span.markertext').setStyle('opacity', 0.6);
|
||||
draghome.insert(drag, 'after');
|
||||
if (!this.get('readonly')) {
|
||||
this.draggable(drag);
|
||||
}
|
||||
return 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;
|
||||
}
|
||||
},
|
||||
|
||||
draggable: function(drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'intersect'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: this.doc.top_node()});
|
||||
dd.after('drag:start', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.addClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
var itemno = this.get_itemno_for_node(dragnode);
|
||||
if (itemno !== null) {
|
||||
dragnode.removeClass('item' + dragnode);
|
||||
}
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
dd.after('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.removeClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
this.save_all_xy_for_choice(choiceno, dragnode);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
// --- keyboard accessibility
|
||||
drag.set('tabIndex', 0);
|
||||
drag.on('dragchange', this.drop_zone_key_press, this);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(drag);
|
||||
},
|
||||
|
||||
save_all_xy_for_choice: function(choiceno, dropped) {
|
||||
var coords = [];
|
||||
var bgimgxy;
|
||||
for (var i = 0; i <= this.doc.drag_items_for_choice(choiceno).size(); i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (dragitem) {
|
||||
dragitem.removeClass('item' + i);
|
||||
if (!dragitem.hasClass('beingdragged')) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dragitem.getXY());
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
dragitem.removeClass('item' + i);
|
||||
dragitem.addClass('item' + coords.length);
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dropped !== null) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dropped.getXY());
|
||||
dropped.addClass('item' + coords.length);
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
this.set_form_value(choiceno, coords.join(';'));
|
||||
},
|
||||
reset_drag_xy: function(choiceno) {
|
||||
this.set_form_value(choiceno, '');
|
||||
},
|
||||
set_form_value: function(choiceno, value) {
|
||||
this.doc.input_for_choice(choiceno).set('value', value);
|
||||
},
|
||||
// make sure xy value is not out of bounds of bg image
|
||||
xy_in_bgimg: function(bgimgxy) {
|
||||
if ((bgimgxy[0] < 0) ||
|
||||
(bgimgxy[1] < 0) ||
|
||||
(bgimgxy[0] > this.doc.bg_img().get('width')) ||
|
||||
(bgimgxy[1] > this.doc.bg_img().get('height'))) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
constrain_to_bgimg: function(windowxy) {
|
||||
var bgimgxy = this.convert_to_bg_img_xy(windowxy);
|
||||
bgimgxy[0] = Math.max(0, bgimgxy[0]);
|
||||
bgimgxy[1] = Math.max(0, bgimgxy[1]);
|
||||
bgimgxy[0] = Math.min(this.doc.bg_img().get('width'), bgimgxy[0]);
|
||||
bgimgxy[1] = Math.min(this.doc.bg_img().get('height'), bgimgxy[1]);
|
||||
return this.convert_to_window_xy(bgimgxy);
|
||||
},
|
||||
convert_to_bg_img_xy: function(windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
redraw_drags_and_drops: function() {
|
||||
this.doc.drag_items().each(function(item) {
|
||||
// if (!item.hasClass('beingdragged')){
|
||||
item.addClass('unneeded');
|
||||
// }
|
||||
}, this);
|
||||
this.doc.inputs_for_choices().each(function(input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var coords = this.get_coords(input);
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (!dragitem || dragitem.hasClass('beingdragged')) {
|
||||
dragitem = this.clone_new_drag_item(dragitemhome, i);
|
||||
} else {
|
||||
dragitem.removeClass('unneeded');
|
||||
}
|
||||
dragitem.setXY(coords[i]);
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(item) {
|
||||
if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
|
||||
item.remove(true);
|
||||
}
|
||||
}, this);
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.clear();
|
||||
} else {
|
||||
this.graphics = new Y.Graphic(
|
||||
{render: this.doc.top_node().one("div.ddarea div.dropzones")}
|
||||
);
|
||||
}
|
||||
if (this.get('dropzones').length !== 0) {
|
||||
this.restart_colours();
|
||||
for (var dropzoneno in this.get('dropzones')) {
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
var d = this.get('dropzones')[dropzoneno];
|
||||
this.draw_drop_zone(dropzoneno, d.markertext,
|
||||
d.shape, d.coords, colourfordropzone, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine what drag items need to be shown and
|
||||
* return coords of all drag items except any that are currently being dragged
|
||||
* based on contents of hidden inputs and whether drags are 'infinite' or how many drags should be shown.
|
||||
*/
|
||||
get_coords: function(input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var fv = input.get('value');
|
||||
var infinite = input.hasClass('infinite');
|
||||
var noofdrags = this.get_noofdrags_for_node(input);
|
||||
var dragging = (null !== this.doc.drag_item_being_dragged(choiceno));
|
||||
var coords = [];
|
||||
if (fv !== '') {
|
||||
var coordsstrings = fv.split(';');
|
||||
for (var i = 0; i < coordsstrings.length; i++) {
|
||||
coords[coords.length] = this.convert_to_window_xy(coordsstrings[i].split(','));
|
||||
}
|
||||
}
|
||||
var displayeddrags = coords.length + (dragging ? 1 : 0);
|
||||
if (infinite || (displayeddrags < noofdrags)) {
|
||||
coords[coords.length] = this.drag_home_xy(choiceno);
|
||||
}
|
||||
return coords;
|
||||
},
|
||||
drag_home_xy: function(choiceno) {
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
return [dragitemhome.getX(), dragitemhome.getY() - 12];
|
||||
},
|
||||
get_choiceno_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'choice'));
|
||||
},
|
||||
get_itemno_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'item'));
|
||||
},
|
||||
get_noofdrags_for_node: function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'noofdrags'));
|
||||
},
|
||||
|
||||
// Keyboard accessibility stuff below here.
|
||||
drop_zone_key_press: function(e) {
|
||||
var dragitem = e.target;
|
||||
var xy = dragitem.getXY();
|
||||
switch (e.direction) {
|
||||
case 'left' :
|
||||
xy[0] -= 1;
|
||||
break;
|
||||
case 'right' :
|
||||
xy[0] += 1;
|
||||
break;
|
||||
case 'down' :
|
||||
xy[1] += 1;
|
||||
break;
|
||||
case 'up' :
|
||||
xy[1] -= 1;
|
||||
break;
|
||||
case 'remove' :
|
||||
xy = null;
|
||||
break;
|
||||
}
|
||||
var choiceno = this.get_choiceno_for_node(dragitem);
|
||||
if (xy !== null) {
|
||||
xy = this.constrain_to_bgimg(xy);
|
||||
} else {
|
||||
xy = this.drag_home_xy(choiceno);
|
||||
}
|
||||
e.preventDefault();
|
||||
dragitem.setXY(xy);
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
}
|
||||
}, {NAME: DDMARKERQUESTIONNAME, ATTRS: {dropzones: {value: []}}});
|
||||
|
||||
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': 'remove', // Space
|
||||
'37': 'left', // Left arrow
|
||||
'38': 'up', // Up arrow
|
||||
'39': 'right', // Right arrow
|
||||
'40': 'down', // Down arrow
|
||||
'65': 'left', // a
|
||||
'87': 'up', // w
|
||||
'68': 'right', // d
|
||||
'83': 'down', // s
|
||||
'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_ddmarker.init_question = function(config) {
|
||||
return new DDMARKER_QUESTION(config);
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"moodle-qtype_ddmarker-dd": {
|
||||
"requires": [
|
||||
"node",
|
||||
"event-resize",
|
||||
"dd",
|
||||
"dd-drop",
|
||||
"dd-constrain",
|
||||
"graphics"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "moodle-qtype_ddmarker-form",
|
||||
"builds": {
|
||||
"moodle-qtype_ddmarker-form": {
|
||||
"jsfiles": [
|
||||
"form.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
285
question/type/ddmarker/yui/src/form/js/form.js
vendored
285
question/type/ddmarker/yui/src/form/js/form.js
vendored
@ -1,285 +0,0 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDMARKERFORMNAME = 'moodle-qtype_ddmarker-form';
|
||||
var DDMARKER_FORM = function() {
|
||||
DDMARKER_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.extend(DDMARKER_FORM, M.qtype_ddmarker.dd_base_class, {
|
||||
fp: null,
|
||||
|
||||
initializer: function() {
|
||||
var pendingid = 'qtype_ddmarker-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append(
|
||||
'<div class="ddarea">' +
|
||||
'<div class="markertexts"></div>' +
|
||||
'<div class="droparea"></div>' +
|
||||
'<div class="dropzones"></div>' +
|
||||
'<ul class="pager unstyled list-unstyled">' +
|
||||
'<li><span id="xcoordpreview">X = </span></li>' +
|
||||
'<li><span id="ycoordpreview">Y = </span></li>' +
|
||||
'</ul>' +
|
||||
'<div class="grid"></div>' +
|
||||
'</div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.update_drop_zones, [pendingid], true);
|
||||
Y.after(this.load_bg_image, M.form_filepicker, 'callback', this);
|
||||
this.load_bg_image();
|
||||
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
topnode.one('.grid').on('mousemove', function (e) {
|
||||
var img = topnode.one('.dropbackground');
|
||||
var x = Math.round(Number(e.pageX) - img.getX() - 1);
|
||||
var y = Math.round(Number(e.pageY) - img.getY() - 1);
|
||||
topnode.one('#xcoordpreview').setHTML("X = " + x);
|
||||
topnode.one('#ycoordpreview').setHTML("Y = " + y);
|
||||
});
|
||||
},
|
||||
|
||||
load_bg_image: function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
|
||||
// Listen for a drop:hit on the background image.
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this);
|
||||
}
|
||||
},
|
||||
|
||||
constrain_image_size: function(e) {
|
||||
var maxsize = this.get('maxsizes').bgimage;
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
update_drop_zones: function(pendingid) {
|
||||
|
||||
// Set up drop zones.
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.destroy();
|
||||
}
|
||||
this.restart_colours();
|
||||
this.graphics = new Y.Graphic({render: "div.ddarea div.dropzones"});
|
||||
var noofdropzones = this.form.get_form_value('nodropzone', []);
|
||||
for (var dropzoneno = 0; dropzoneno < noofdropzones; dropzoneno++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [dropzoneno, 'choice']);
|
||||
var markertext = this.get_marker_text(dragitemno);
|
||||
var shape = this.form.get_form_value('drops', [dropzoneno, 'shape']);
|
||||
var coords = this.get_coords(dropzoneno);
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
Y.one('input#id_drops_' + dropzoneno + '_coords')
|
||||
.setStyle('background-color', colourfordropzone);
|
||||
this.draw_drop_zone(dropzoneno, markertext,
|
||||
shape, coords, colourfordropzone, false);
|
||||
}
|
||||
if (this.doc.bg_img()) {
|
||||
Y.one('div.ddarea .grid')
|
||||
.setXY(this.convert_to_window_xy([0, 0]))
|
||||
.setStyle('width', this.doc.bg_img().get('width'))
|
||||
.setStyle('height', this.doc.bg_img().get('height'));
|
||||
}
|
||||
M.util.js_complete(pendingid);
|
||||
},
|
||||
|
||||
get_coords: function(dropzoneno) {
|
||||
var coords = this.form.get_form_value('drops', [dropzoneno, 'coords']);
|
||||
return coords.replace(new RegExp("\\s*", 'g'), '');
|
||||
},
|
||||
get_marker_text: function(markerno) {
|
||||
if (Number(markerno) !== 0) {
|
||||
var label = this.form.get_form_value('drags', [markerno - 1, 'label']);
|
||||
return label.replace(new RegExp("^\\s*(.*)\\s*$"), "$1");
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors: function() {
|
||||
var dragitemsoptions = {'0': ''};
|
||||
for (var i = 1; i <= this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.get_marker_text(i);
|
||||
if (label !== "") {
|
||||
dragitemsoptions[i] = Y.Escape.html(label);
|
||||
}
|
||||
}
|
||||
// Get all the currently selected drags for each drop.
|
||||
var selectedvalues = [];
|
||||
var selector;
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
selectedvalues[i] = Number(selector.get('value'));
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
// Remove all options for drag choice.
|
||||
selector.all('option').remove(true);
|
||||
// And recreate the options.
|
||||
for (var value in dragitemsoptions) {
|
||||
value = Number(value);
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
// Is this the currently selected value?
|
||||
if (value === selectedvalues[i]) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
// It is not the currently selected value, is it selectable?
|
||||
if (value !== 0) { // The 'no item' option is always selectable.
|
||||
// Variables to hold form values about this drag item.
|
||||
var noofdrags = this.form.get_form_value('drags', [value - 1, 'noofdrags']);
|
||||
if (Number(noofdrags) !== 0) { // 'noofdrags == 0' means infinite.
|
||||
// Go through all selected values in drop downs.
|
||||
for (var k in selectedvalues) {
|
||||
// Count down 'noofdrags' and if reach zero then set disabled option for this drag item.
|
||||
if (Number(selectedvalues[k]) === value) {
|
||||
if (Number(noofdrags) === 1) {
|
||||
optionnode.set('disabled', true);
|
||||
break;
|
||||
} else {
|
||||
noofdrags--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events: function() {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events: function() {
|
||||
// events triggered by changes to form data
|
||||
|
||||
// Changes to labels.
|
||||
Y.all('fieldset#id_draggableitemheader input').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Changes to selected drag item.
|
||||
Y.all('fieldset#id_draggableitemheader select').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function() {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form: {
|
||||
to_name_with_index: function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el: function(name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value: function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value: function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index: function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers: function() {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file: function(name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href: fileanchor.get('href'), name: fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href: null, name: null};
|
||||
}
|
||||
},
|
||||
name: function(draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
}, {NAME: DDMARKERFORMNAME, ATTRS: {maxsizes: {value: null}}});
|
||||
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.init_form = function(config) {
|
||||
return new DDMARKER_FORM(config);
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"moodle-qtype_ddmarker-form": {
|
||||
"requires": [
|
||||
"moodle-qtype_ddmarker-dd",
|
||||
"form_filepicker",
|
||||
"graphics",
|
||||
"escape"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user