Commit 66de50c3 authored by Andrew Nicols's avatar Andrew Nicols
Browse files

Merge branch 'wip-MDL-62411-master' of https://github.com/timhunt/moodle

parents daf0b4f0 00f09d8f
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -203,7 +203,7 @@ define(['jquery'], function($) {
*
* @public
*/
stop: autoscroll.stop,
stop: autoscroll.stop
};
});
// 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
};
});
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -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;