Merge branch 'MDL-68454-master' of git://github.com/andrewnicols/moodle
authorAdrian Greeve <abgreeve@gmail.com>
Mon, 4 May 2020 04:18:54 +0000 (12:18 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Mon, 4 May 2020 04:18:54 +0000 (12:18 +0800)
20 files changed:
.travis.yml
course/renderer.php
h5p/tests/editor_test.php
lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php
lib/plist/readme_moodle.txt
media/player/videojs/classes/plugin.php
question/type/ddmarker/amd/build/form.min.js
question/type/ddmarker/amd/build/form.min.js.map
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/build/question.min.js.map
question/type/ddmarker/amd/build/shapes.min.js
question/type/ddmarker/amd/build/shapes.min.js.map
question/type/ddmarker/amd/src/form.js
question/type/ddmarker/amd/src/question.js
question/type/ddmarker/amd/src/shapes.js
question/type/ddmarker/renderer.php
question/type/ddmarker/styles.css
question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php
question/type/ddmarker/tests/behat/preview.feature
question/type/ddmarker/tests/walkthrough_test.php

index 245d7fc..f60032e 100644 (file)
@@ -18,7 +18,7 @@ services:
 
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
-    - 7.3
+    - 7.4
     - 7.2
 
 addons:
@@ -48,11 +48,11 @@ jobs:
     fast_finish: true
 
     include:
-          # Run mysql only on 7.3 - it's just too slow
-        - php: 7.3
+          # Run mysql only on highest - it's just too slow
+        - php: 7.4
           env: DB=mysqli   TASK=PHPUNIT
-          # Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
-        - php: 7.2
+          # Run grunt/npm install on highest version too ('node' is an alias for the latest node.js version.)
+        - php: 7.4
           env: DB=none     TASK=GRUNT   NVM_VERSION='lts/carbon'
 
 cache:
index 784d6ee..6f1a956 100644 (file)
@@ -903,7 +903,7 @@ class core_course_renderer extends plugin_renderer_base {
             $output .= course_get_cm_move($mod, $sectionreturn);
         }
 
-        $output .= html_writer::start_tag('div', array('class' => 'mod-indent-outer'));
+        $output .= html_writer::start_tag('div', array('class' => 'mod-indent-outer w-100'));
 
         // This div is used to indent the content.
         $output .= html_writer::div('', $indentclasses);
index 05f5877..fb007bf 100644 (file)
  * @copyright  2020 Victor Deniz <victor@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+
 namespace core_h5p;
 
+defined('MOODLE_INTERNAL') || die();
+
 use advanced_testcase;
 use core_h5p\local\library\autoloader;
-use moodleform;
 use MoodleQuickForm;
 use page_requirements_manager;
 
@@ -42,6 +44,34 @@ use page_requirements_manager;
  */
 class editor_testcase extends advanced_testcase {
 
+    /**
+     * Form object to be used in test case.
+     */
+    protected function get_test_form() {
+        global $CFG;
+
+        require_once($CFG->libdir . '/formslib.php');
+
+        return new class extends \moodleform {
+            /**
+             * Form definition.
+             */
+            public function definition(): void {
+                // No definition required.
+            }
+
+            /**
+             * Returns form reference.
+             *
+             * @return MoodleQuickForm
+             */
+            public function getform() {
+                $mform = $this->_form;
+                return $mform;
+            }
+        };
+    }
+
     /**
      * Test that existing content is properly set.
      */
@@ -153,7 +183,7 @@ class editor_testcase extends advanced_testcase {
         global $PAGE, $CFG;
 
         // Get form data.
-        $form = new temp_form();
+        $form = $this->get_test_form();
         $mform = $form->getform();
 
         // Call method.
@@ -249,27 +279,3 @@ class editor_testcase extends advanced_testcase {
         $this->assertNotEmpty($out);
     }
 }
-
-/**
- * Form object to be used in test case.
- */
-class temp_form extends moodleform {
-    /**
-     * Form definition.
-     */
-    public function definition(): void {
-        // No definition required.
-    }
-
-    /**
-     * Returns form reference.
-     *
-     * @return MoodleQuickForm
-     */
-    public function getform() {
-        $mform = $this->_form;
-        // Set submitted flag, to simulate submission.
-        $mform->_flagSubmitted = true;
-        return $mform;
-    }
-}
index 34cb708..4c34006 100644 (file)
@@ -801,7 +801,7 @@ abstract class CFBinaryPropertyList {
    */
   protected static function binaryStrlen($val) {
     for($i=0;$i<strlen($val);++$i) {
-      if(ord($val{$i}) >= 128) {
+      if(ord($val[$i]) >= 128) {
         $val = self::convertCharset($val, 'UTF-8', 'UTF-16BE');
         return strlen($val);
       }
@@ -824,7 +824,7 @@ abstract class CFBinaryPropertyList {
       $utf16 = false;
 
       for($i=0;$i<strlen($val);++$i) {
-        if(ord($val{$i}) >= 128) {
+        if(ord($val[$i]) >= 128) {
           $utf16 = true;
           break;
         }
index 664f4b5..27003bc 100644 (file)
@@ -14,6 +14,11 @@ Removed:
  * examples
  * tests
 
+Local changes:
+(always verify if the changes below are already fixed by the
+next version to import or they need to be re-applied manually)
+  * PHP 7.4 comp: bf527c8 - Partially applied https://github.com/TECLIB/CFPropertyList/pull/61
+
 Added:
  * readme_moodle.txt
 
index 1f29e2e..dfcf80a 100644 (file)
@@ -201,7 +201,7 @@ class media_videojs_plugin extends core_media_player_native {
             }
         }
 
-        return html_writer::div($text, 'mediaplugin mediaplugin_videojs');
+        return html_writer::div($text, 'mediaplugin mediaplugin_videojs d-block');
     }
 
     /**
index 35fb91b..2607883 100644 (file)
Binary files a/question/type/ddmarker/amd/build/form.min.js and b/question/type/ddmarker/amd/build/form.min.js differ
index 5eb2a66..3fc5453 100644 (file)
Binary files a/question/type/ddmarker/amd/build/form.min.js.map and b/question/type/ddmarker/amd/build/form.min.js.map differ
index a44cd93..4144b19 100644 (file)
Binary files a/question/type/ddmarker/amd/build/question.min.js and b/question/type/ddmarker/amd/build/question.min.js differ
index 9af4a60..8621ca8 100644 (file)
Binary files a/question/type/ddmarker/amd/build/question.min.js.map and b/question/type/ddmarker/amd/build/question.min.js.map differ
index 521fd2f..fe4a6ff 100644 (file)
Binary files a/question/type/ddmarker/amd/build/shapes.min.js and b/question/type/ddmarker/amd/build/shapes.min.js differ
index 3f3b32f..8bc1858 100644 (file)
Binary files a/question/type/ddmarker/amd/build/shapes.min.js.map and b/question/type/ddmarker/amd/build/shapes.min.js.map differ
index c2951e8..cb8b617 100644 (file)
@@ -51,7 +51,8 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes'], function($, dragDro
         if (this.shape.getCoordinates() === coordinates) {
             return;
         }
-        if (!this.shape.parse(coordinates)) {
+        // We don't need to scale the shape for editing form.
+        if (!this.shape.parse(coordinates, 1)) {
             // Invalid coordinates. Don't update the preview.
             return;
         }
@@ -70,6 +71,8 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes'], function($, dragDro
             // Simple update.
             this.updateSvgEl();
         }
+        // Update the rounded coordinates if needed.
+        this.setCoordinatesInForm();
     };
 
     /**
index ae812d8..7dfa8ff 100644 (file)
@@ -30,56 +30,44 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
      * 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) {
+    function DragDropMarkersQuestion(containerId, readOnly, visibleDropZones) {
+        var thisQ = this;
         this.containerId = containerId;
         this.visibleDropZones = visibleDropZones;
+        this.shapes = [];
+        this.shapeSVGs = [];
+        this.isPrinting = false;
         if (readOnly) {
             this.getRoot().addClass('qtype_ddmarker-readonly');
         }
-        this.loadImage(bgImgUrl);
+        thisQ.cloneDrags();
+        thisQ.repositionDrags();
+        thisQ.drawDropzones();
     }
 
-    /**
-     * 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);
+        if (this.visibleDropZones.length > 0) {
+            var bgImage = this.bgImage();
+
+            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');
+
+            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);
+            }
         }
     };
 
@@ -93,8 +81,9 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
     DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {
         var dropZone = this.visibleDropZones[dropZoneNo],
             shape = Shapes.make(dropZone.shape, ''),
-            existingmarkertext;
-        if (!shape.parse(dropZone.coords)) {
+            existingmarkertext,
+            bgRatio = this.bgRatio();
+        if (!shape.parse(dropZone.coords, bgRatio)) {
             return;
         }
 
@@ -109,40 +98,26 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
             var classnames = 'markertext markertext' + dropZoneNo;
             this.getRoot().find('div.markertexts').append('<span class="' + classnames + '">' +
                 dropZone.markertext + '</span>');
+            var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);
+            if (markerspan.length) {
+                var handles = shape.getHandlePositions();
+                var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;
+                var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);
+                markerspan
+                    .css('left', positionLeft)
+                    .css('top', positionTop);
+                markerspan
+                    .data('originX', markerspan.position().left / bgRatio)
+                    .data('originY', markerspan.position().top / bgRatio);
+                this.handleElementScale(markerspan, 'center');
+            }
         }
 
         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});
-        }
+        this.shapes[this.shapes.length] = shape;
+        this.shapeSVGs[this.shapeSVGs.length] = shapeSVG;
     };
 
     /**
@@ -154,37 +129,25 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
         var root = this.getRoot(),
             thisQ = this;
 
-        root.find('div.dragitems .dragitem').each(function(key, item) {
+        root.find('div.draghomes .marker').not('.dragplaceholder').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');
+                coords = thisQ.getCoords(input);
+            if (coords.length) {
+                var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');
+                drag.remove();
+                for (var i = 0; i < coords.length; i++) {
+                    var dragInDrop = drag.clone();
+                    dragInDrop.data('pagex', coords[i].x).data('pagey', coords[i].y);
+                    thisQ.sendDragToDrop(dragInDrop, false);
                 }
-                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();
+                thisQ.getDragClone(drag).addClass('active');
+                thisQ.cloneDragIfNeeded(drag);
             }
         });
-
-        this.repositionDropZones();
-
-        var bgImage = this.bgImage(),
-            bgPosition = bgImage.offset();
-        bgImage.data('prev-top', bgPosition.top).data('prev-left', bgPosition.left);
     };
 
     /**
@@ -197,11 +160,7 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
      * @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 = [],
+        var coords = [],
             val = $(inputNode).val();
         if (val !== '') {
             var coordsStrings = val.split(';');
@@ -209,10 +168,6 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
                 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;
     };
 
@@ -252,19 +207,10 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
      */
     DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {
         var bgImage = this.bgImage();
-        return point.x > 0 && point.x <= bgImage.width() &&
-                point.y > 0 && point.y <= bgImage.height();
-    };
+        var bgPosition = bgImage.offset();
 
-    /**
-     * 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);
+        return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()
+            && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();
     };
 
     /**
@@ -283,52 +229,28 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
         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');
+            dragged = $(e.target).closest('.marker');
 
         var info = dragDrop.prepare(e);
         if (!info.start) {
             return;
         }
 
-        dragged.addClass('beingdragged');
+        dragged.addClass('beingdragged').css('transform', '');
+
+        var placed = !dragged.hasClass('unneeded');
+        if (!placed) {
+            var hiddenDrag = thisQ.getDragClone(dragged);
+            if (hiddenDrag.length) {
+                hiddenDrag.addClass('active');
+                dragged.offset(hiddenDrag.offset());
+            }
+        }
+
         dragDrop.start(e, dragged, function() {
-            void (1); // Nothing to do, but we need a function.
+            void (1);
         }, function(x, y, dragged) {
             thisQ.dragEnd(dragged);
         });
@@ -339,52 +261,56 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
      * @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();
+        var placed = false,
+            choiceNo = this.getChoiceNoFromElement(dragged),
+            bgRatio = this.bgRatio(),
+            dragXY;
+
+        dragged.data('pagex', dragged.offset().left).data('pagey', dragged.offset().top);
+        dragXY = new Shapes.Point(dragged.data('pagex'), dragged.data('pagey'));
+        if (this.coordsInBgImg(dragXY)) {
+            this.sendDragToDrop(dragged, true);
+            placed = true;
+
+            // It seems that the dragdrop sometimes leaves the drag
+            // one pixel out of position. Put it in exactly the right place.
+            var bgImgXY = this.convertToBgImgXY(dragXY);
+            bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);
+            dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);
+        }
+
+        if (!placed) {
+            this.sendDragHome(dragged);
+            this.removeDragIfNeeded(dragged);
+        } else {
+            this.cloneDragIfNeeded(dragged);
+        }
+
+        this.saveCoordsForChoice(choiceNo);
     };
 
     /**
      * 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) {
+    DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) {
         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;
+            items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),
+            thiQ = this,
+            bgRatio = this.bgRatio();
+
+        if (items.length) {
+            items.each(function() {
+                var drag = $(this);
+                if (!drag.hasClass('beingdragged')) {
+                    var dragXY = new Shapes.Point(drag.data('pagex'), drag.data('pagey'));
+                    if (thiQ.coordsInBgImg(dragXY)) {
+                        var bgImgXY = thiQ.convertToBgImgXY(dragXY);
+                        bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);
+                        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(';'));
@@ -395,7 +321,7 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
      * @param {KeyboardEvent} e
      */
     DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {
-        var drag = $(e.target).closest('.dragitem'),
+        var drag = $(e.target).closest('.marker'),
             point = new Shapes.Point(drag.offset().left, drag.offset().top),
             choiceNo = this.getChoiceNoFromElement(drag);
 
@@ -427,12 +353,28 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
 
         if (point !== null) {
             point = this.constrainToBgImg(point);
+            drag.offset({'left': point.x, 'top': point.y});
+            drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);
+            var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));
+            drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());
+            if (this.coordsInBgImg(new Shapes.Point(drag.offset().left, drag.offset().top))) {
+                if (drag.hasClass('unneeded')) {
+                    this.sendDragToDrop(drag, true);
+                    var hiddenDrag = this.getDragClone(drag);
+                    if (hiddenDrag.length) {
+                        hiddenDrag.addClass('active');
+                    }
+                    this.cloneDragIfNeeded(drag);
+                }
+            }
         } else {
-            point = this.dragHomeXY(choiceNo);
+            drag.css('left', '').css('top', '');
+            drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);
+            this.sendDragHome(drag);
+            this.removeDragIfNeeded(drag);
         }
-        drag.offset({'left': point.x, 'top': point.y});
-        this.saveCoordsForChoice(choiceNo, drag);
-        this.repositionDrags();
+        drag.focus();
+        this.saveCoordsForChoice(choiceNo);
     };
 
     /**
@@ -488,27 +430,204 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
      * Handle when the window is resized.
      */
     DragDropMarkersQuestion.prototype.handleResize = function() {
-        this.repositionDrags();
+        var thisQ = this,
+            bgRatio = this.bgRatio();
+        if (this.isPrinting) {
+            bgRatio = 1;
+        }
+
+        this.getRoot().find('div.droparea .marker').not('.beingdragged').each(function(key, drag) {
+            $(drag)
+                .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio))
+                .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio));
+            thisQ.handleElementScale(drag, 'left top');
+        });
+
+        this.getRoot().find('div.droparea svg.dropzones')
+            .width(this.bgImage().width())
+            .height(this.bgImage().height());
+
+        for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {
+            var dropZone = thisQ.visibleDropZones[dropZoneNo];
+            var originCoords = dropZone.coords;
+            var shape = thisQ.shapes[dropZoneNo];
+            var shapeSVG = thisQ.shapeSVGs[dropZoneNo];
+            shape.parse(originCoords, bgRatio);
+            shape.updateSvg(shapeSVG);
+
+            var handles = shape.getHandlePositions();
+            var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);
+            markerSpan
+                .css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)
+                .css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));
+            thisQ.handleElementScale(markerSpan, 'center');
+        }
     };
 
     /**
-     * Check to see if the background image has moved. If so, refresh the layout.
+     * Clone the drag.
      */
-    DragDropMarkersQuestion.prototype.fixLayoutIfBackgroundMoved = function() {
-        var bgImage = this.bgImage(),
-            bgPosition = bgImage.offset(),
-            prevTop = bgImage.data('prev-top'),
-            prevLeft = bgImage.data('prev-left');
-        if (prevLeft === undefined || prevTop === undefined) {
-            // Question is not set up yet. Nothing to do.
-            return;
+    DragDropMarkersQuestion.prototype.cloneDrags = function() {
+        var thisQ = this;
+        this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {
+            var drag = $(draghome);
+            var placeHolder = drag.clone();
+            placeHolder.removeClass();
+            placeHolder.addClass('marker choice' +
+                thisQ.getChoiceNoFromElement(drag) + ' dragno' + thisQ.getDragNo(drag) + ' dragplaceholder');
+            drag.before(placeHolder);
+        });
+    };
+
+    /**
+     * Get the drag number of a drag.
+     *
+     * @param {jQuery} drag the drag.
+     * @returns {Number} the drag number.
+     */
+    DragDropMarkersQuestion.prototype.getDragNo = function(drag) {
+        return this.getClassnameNumericSuffix(drag, 'dragno');
+    };
+
+    /**
+     * Get drag clone for a given drag.
+     *
+     * @param {jQuery} drag the drag.
+     * @returns {jQuery} the drag's clone.
+     */
+    DragDropMarkersQuestion.prototype.getDragClone = function(drag) {
+        return this.getRoot().find('.draghomes' + ' span.marker' +
+            '.choice' + this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag) + '.dragplaceholder');
+    };
+
+    /**
+     * Get the drop area element.
+     * @returns {jQuery} droparea element.
+     */
+    DragDropMarkersQuestion.prototype.dropArea = function() {
+        return this.getRoot().find('div.droparea');
+    };
+
+    /**
+     * Animate a drag back to its home.
+     *
+     * @param {jQuery} drag the item being moved.
+     */
+    DragDropMarkersQuestion.prototype.sendDragHome = function(drag) {
+        drag.removeClass('beingdragged')
+            .addClass('unneeded')
+            .css('top', '')
+            .css('left', '')
+            .css('transform', '');
+        var placeHolder = this.getDragClone(drag);
+        placeHolder.after(drag);
+        placeHolder.removeClass('active');
+    };
+
+    /**
+     * Animate a drag item into a given place.
+     *
+     * @param {jQuery} drag the item to place.
+     * @param {boolean} isScaling Scaling or not
+     */
+    DragDropMarkersQuestion.prototype.sendDragToDrop = function(drag, isScaling) {
+        var dropArea = this.dropArea(),
+            bgRatio = this.bgRatio();
+        drag.removeClass('beingdragged').removeClass('unneeded');
+        var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));
+        if (isScaling) {
+            drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);
+            drag.css('left', dragXY.x).css('top', dragXY.y);
+        } else {
+            drag.data('originX', dragXY.x).data('originY', dragXY.y);
+            drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);
         }
-        if (prevTop === bgPosition.top && prevLeft === bgPosition.left) {
-            // Things have not moved.
-            return;
+        dropArea.append(drag);
+        this.handleElementScale(drag, 'left top');
+    };
+
+    /**
+     * Clone the drag at the draghome area if needed.
+     *
+     * @param {jQuery} drag the item to place.
+     */
+    DragDropMarkersQuestion.prototype.cloneDragIfNeeded = function(drag) {
+        var inputNode = this.getInput(drag),
+            noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),
+            displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +
+                this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).length,
+            displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +
+                this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').length;
+
+        if (displayedDragsInDropArea < noOfDrags && displayedDragsInDragHomes === 0) {
+            var dragclone = drag.clone();
+            dragclone.addClass('unneeded')
+                .css('top', '')
+                .css('left', '')
+                .css('transform', '');
+            this.getDragClone(drag)
+                .removeClass('active')
+                .after(dragclone);
+        }
+    };
+
+    /**
+     * Remove the clone drag at the draghome area if needed.
+     *
+     * @param {jQuery} drag the item to place.
+     */
+    DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {
+        var displayeddrags = this.getRoot().find('div.draghomes .marker.choice' +
+            this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').length;
+        if (displayeddrags > 1) {
+            this.getRoot().find('div.draghomes .marker.choice' +
+                this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').first().remove();
         }
-        // We need to reposition things.
-        this.repositionDrags();
+    };
+
+    /**
+     * Get the input belong to drag.
+     *
+     * @param {jQuery} drag the item to place.
+     * @returns {jQuery} input element.
+     */
+    DragDropMarkersQuestion.prototype.getInput = function(drag) {
+        var choiceNo = this.getChoiceNoFromElement(drag);
+        return this.getRoot().find('input.choices.choice' + choiceNo);
+    };
+
+    /**
+     * Return the background ratio.
+     *
+     * @returns {number} Background ratio.
+     */
+    DragDropMarkersQuestion.prototype.bgRatio = function() {
+        var bgImg = this.bgImage();
+        var bgImgNaturalWidth = bgImg.get(0).naturalWidth;
+        var bgImgClientWidth = bgImg.width();
+
+        return bgImgClientWidth / bgImgNaturalWidth;
+    };
+
+    /**
+     * Scale the drag if needed.
+     *
+     * @param {jQuery} element the item to place.
+     * @param {String} type scaling type
+     */
+    DragDropMarkersQuestion.prototype.handleElementScale = function(element, type) {
+        var bgRatio = parseFloat(this.bgRatio());
+        if (this.isPrinting) {
+            bgRatio = 1;
+        }
+        $(element).css({
+            '-webkit-transform': 'scale(' + bgRatio + ')',
+            '-moz-transform': 'scale(' + bgRatio + ')',
+            '-ms-transform': 'scale(' + bgRatio + ')',
+            '-o-transform': 'scale(' + bgRatio + ')',
+            'transform': 'scale(' + bgRatio + ')',
+            'transform-origin': type
+        });
     };
 
     /**
@@ -524,6 +643,16 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
          */
         eventHandlersInitialised: false,
 
+        /**
+         * {boolean} is printing or not.
+         */
+        isPrinting: false,
+
+        /**
+         * {boolean} is keyboard navigation.
+         */
+        isKeyboardNavigation: false,
+
         /**
          * {Object} all the questions on this page, indexed by containerId (id on the .que div).
          */
@@ -533,13 +662,12 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
          * 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) {
+        init: function(containerId, readOnly, visibleDropZones) {
             questionManager.questions[containerId] =
-                new DragDropMarkersQuestion(containerId, bgImgUrl, readOnly, visibleDropZones);
+                new DragDropMarkersQuestion(containerId, readOnly, visibleDropZones);
             if (!questionManager.eventHandlersInitialised) {
                 questionManager.setupEventHandlers();
                 questionManager.eventHandlersInitialised = true;
@@ -550,14 +678,45 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
          * 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)
+            $('body')
+                .on('mousedown touchstart',
+                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', questionManager.handleDragStart)
+                .on('mousedown touchstart',
+                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', questionManager.handleDragStart)
+                .on('keydown keypress',
+                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', questionManager.handleKeyPress)
                 .on('keydown keypress',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.dragitems .dragitem',
-                    questionManager.handleKeyPress);
-            $(window).on('resize', questionManager.handleWindowResize);
-            setTimeout(questionManager.fixLayoutIfThingsMoved, 100);
+                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', questionManager.handleKeyPress)
+                .on('focusin',
+                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', function(e) {
+                        questionManager.handleKeyboardFocus(e, true);
+                    })
+                .on('focusin',
+                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', function(e) {
+                        questionManager.handleKeyboardFocus(e, true);
+                    })
+                .on('focusout',
+                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', function(e) {
+                        questionManager.handleKeyboardFocus(e, false);
+                    })
+                .on('focusout',
+                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', function(e) {
+                        questionManager.handleKeyboardFocus(e, false);
+                    });
+            $(window).on('resize', function() {
+                questionManager.handleWindowResize(false);
+            });
+            window.addEventListener('beforeprint', function() {
+                questionManager.isPrinting = true;
+                questionManager.handleWindowResize(questionManager.isPrinting);
+            });
+            window.addEventListener('afterprint', function() {
+                questionManager.isPrinting = false;
+                questionManager.handleWindowResize(questionManager.isPrinting);
+            });
+            setTimeout(function() {
+                questionManager.fixLayoutIfThingsMoved();
+            }, 100);
         },
 
         /**
@@ -585,31 +744,41 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
 
         /**
          * Handle when the window is resized.
+         * @param {boolean} isPrinting
          */
-        handleWindowResize: function() {
+        handleWindowResize: function(isPrinting) {
             for (var containerId in questionManager.questions) {
                 if (questionManager.questions.hasOwnProperty(containerId)) {
+                    questionManager.questions[containerId].isPrinting = isPrinting;
                     questionManager.questions[containerId].handleResize();
                 }
             }
         },
 
+        /**
+         * Handle focus lost events on markers.
+         * @param {Event} e
+         * @param {boolean} isNavigating
+         */
+        handleKeyboardFocus: function(e, isNavigating) {
+            questionManager.isKeyboardNavigation = isNavigating;
+        },
+
         /**
          * Sometimes, despite our best efforts, things change in a way that cannot
          * be specifically caught (e.g. dock expanding or collapsing in Boost).
          * Therefore, we need to periodically check everything is in the right position.
          */
         fixLayoutIfThingsMoved: function() {
-            for (var containerId in questionManager.questions) {
-                if (questionManager.questions.hasOwnProperty(containerId)) {
-                    questionManager.questions[containerId].fixLayoutIfBackgroundMoved();
-                }
+            if (!questionManager.isKeyboardNavigation) {
+                this.handleWindowResize(questionManager.isPrinting);
             }
-
             // We use setTimeout after finishing work, rather than setInterval,
             // in case positioning things is slow. We want 100 ms gap
             // between executions, not what setInterval does.
-            setTimeout(questionManager.fixLayoutIfThingsMoved, 100);
+            setTimeout(function() {
+                questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);
+            }, 100);
         },
 
         /**
index dec8c09..5fd765e 100644 (file)
@@ -126,10 +126,11 @@ define(function() {
      * Update the shape from the string representation.
      *
      * @param {String} coordinates in the form returned by getCoordinates.
+     * @param {number} ratio Ratio to scale.
      * @return {boolean} true if the string could be parsed and the shape updated, else false.
      */
-    Shape.prototype.parse = function(coordinates) {
-        void (coordinates);
+    Shape.prototype.parse = function(coordinates, ratio) {
+        void (coordinates, ratio);
         throw new Error('Not implemented.');
     };
 
@@ -264,14 +265,16 @@ define(function() {
         svgEl.childNodes[1].textContent = this.label;
     };
 
-    Circle.prototype.parse = function(coordinates) {
-        if (!coordinates.match(/^\d+,\d+;\d+$/)) {
+    Circle.prototype.parse = function(coordinates, ratio) {
+        if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?$/)) {
             return false;
         }
 
         var bits = coordinates.split(';');
         this.centre = Point.parse(bits[0]);
-        this.radius = Math.round(bits[1]);
+        this.centre.x = this.centre.x * parseFloat(ratio);
+        this.centre.y = this.centre.y * parseFloat(ratio);
+        this.radius = Math.round(bits[1]) * parseFloat(ratio);
         return true;
     };
 
@@ -384,16 +387,18 @@ define(function() {
         svgEl.childNodes[1].textContent = this.label;
     };
 
-    Rectangle.prototype.parse = function(coordinates) {
-        if (!coordinates.match(/^\d+,\d+;\d+,\d+$/)) {
+    Rectangle.prototype.parse = function(coordinates, ratio) {
+        if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?,\d+(\.\d+)?$/)) {
             return false;
         }
 
         var bits = coordinates.split(';');
         this.centre = Point.parse(bits[0]);
+        this.centre.x = this.centre.x * parseFloat(ratio);
+        this.centre.y = this.centre.y * parseFloat(ratio);
         var size = Point.parse(bits[1]);
-        this.width = size.x;
-        this.height = size.y;
+        this.width = size.x * parseFloat(ratio);
+        this.height = size.y * parseFloat(ratio);
         return true;
     };
 
@@ -479,6 +484,7 @@ define(function() {
         Shape.call(this, label, 0, 0);
         this.points = points ? points.slice() : [new Point(10, 10), new Point(40, 10), new Point(10, 40)];
         this.normalizeShape();
+        this.ratio = 1;
     }
     Polygon.prototype = new Shape();
 
@@ -502,13 +508,14 @@ define(function() {
 
     Polygon.prototype.updateSvg = function(svgEl) {
         svgEl.childNodes[0].setAttribute('points', this.getCoordinates().replace(/[,;]/g, ' '));
+        svgEl.childNodes[0].setAttribute('transform', 'scale(' + parseFloat(this.ratio) + ')');
         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+)*$/)) {
+    Polygon.prototype.parse = function(coordinates, ratio) {
+        if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?(?:;\d+(\.\d+)?,\d+(\.\d+)?)*$/)) {
             return false;
         }
 
@@ -521,6 +528,7 @@ define(function() {
         this.points = points;
         this.centre.x = 0;
         this.centre.y = 0;
+        this.ratio = ratio;
         this.normalizeShape();
 
         return true;
@@ -636,6 +644,9 @@ define(function() {
             editHandles.push(this.points[i].offset(this.centre.x, this.centre.y));
         }
 
+        this.centre.x = this.centre.x * parseFloat(this.ratio);
+        this.centre.y = this.centre.y * parseFloat(this.ratio);
+
         return {
             moveHandle: this.centre,
             editHandles: editHandles
index 3bf692d..180faa5 100644 (file)
@@ -42,88 +42,86 @@ class qtype_ddmarker_renderer extends qtype_ddtoimage_renderer_base {
 
         $question = $qa->get_question();
         $response = $qa->get_last_qt_data();
+        $componentname = $question->qtype->plugin_name();
 
         $questiontext = $question->format_questiontext($qa);
 
-        $output = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
+        $dropareaclass = 'droparea';
+        $draghomesclass = 'draghomes';
+        if ($options->readonly) {
+            $dropareaclass .= ' readonly';
+            $draghomesclass .= ' readonly';
+        }
 
-        $bgimage = self::get_url_for_image($qa, 'bgimage');
+        $output = html_writer::div($questiontext, 'qtext');
 
-        $img = html_writer::empty_tag('img', array(
-                'class' => 'dropbackground',
-                'alt' => get_string('dropbackground', 'qtype_ddmarker')));
+        $output .= html_writer::start_div('ddarea');
+        $output .= html_writer::start_div($dropareaclass);
+        $output .= html_writer::img(self::get_url_for_image($qa, 'bgimage'), get_string('dropbackground', 'qtype_ddmarker'),
+                ['class' => 'dropbackground img-responsive img-fluid']);
 
-        $droparea = html_writer::tag('div', $img, array('class' => 'droparea'));
+        $output .= html_writer::div('', 'dropzones');
+        $output .= html_writer::div('', 'markertexts');
+
+        $output .= html_writer::end_div();
+        $output .= html_writer::start_div($draghomesclass);
 
-        $draghomes = '';
         $orderedgroup = $question->get_ordered_choices(1);
-        $componentname = $question->qtype->plugin_name();
         $hiddenfields = '';
         foreach ($orderedgroup as $choiceno => $drag) {
-            $classes = array('draghome',
-                             "choice{$choiceno}");
+            $classes = ['marker', 'choice' . $choiceno];
+            $attr = [];
             if ($drag->infinite) {
                 $classes[] = 'infinite';
             } else {
-                $classes[] = 'dragno'.$drag->noofdrags;
+                $classes[] = 'dragno' . $drag->noofdrags;
             }
-            $targeticonhtml =
-                    $this->output->image_icon('crosshairs', '', $componentname, array('class' => 'target'));
-
-            $markertextattrs = array('class' => 'markertext');
-            $markertext = html_writer::tag('span', $drag->text, $markertextattrs);
-            $draghomesattrs = array('class' => join(' ', $classes));
-            $draghomes .= html_writer::tag('span', $targeticonhtml . $markertext, $draghomesattrs);
+            if (!$options->readonly) {
+                $attr['tabindex'] = 0;
+            }
+            $dragoutput = html_writer::start_span(join(' ', $classes), $attr);
+            $targeticonhtml = $this->output->image_icon('crosshairs', '', $componentname, ['class' => 'target']);
+            $markertext = html_writer::span($drag->text, 'markertext');
+            $dragoutput .= $targeticonhtml . $markertext;
+            $dragoutput .= html_writer::end_span();
+            $output .= $dragoutput;
             $hiddenfields .= $this->hidden_field_choice($qa, $choiceno, $drag->infinite, $drag->noofdrags);
         }
 
-        $dragitemsclass = 'dragitems';
-        if ($options->readonly) {
-            $dragitemsclass .= ' readonly';
-        }
-
-        $dragitems = html_writer::tag('div', $draghomes, array('class' => $dragitemsclass));
-        $dropzones = html_writer::tag('div', '', array('class' => 'dropzones'));
-        $texts = html_writer::tag('div', '', array('class' => 'markertexts'));
-        $output .= html_writer::tag('div',
-                                    $droparea.$dragitems.$dropzones . $texts,
-                                    array('class' => 'ddarea'));
+        $output .= html_writer::end_div();
+        $output .= html_writer::end_div();
 
         if ($question->showmisplaced && $qa->get_state()->is_finished()) {
             $visibledropzones = $question->get_drop_zones_without_hit($response);
         } else {
-            $visibledropzones = array();
+            $visibledropzones = [];
         }
 
-        $this->page->requires->js_call_amd('qtype_ddmarker/question', 'init',
-                [$qa->get_outer_question_div_unique_id(), $bgimage, $options->readonly, $visibledropzones]);
-
         if ($qa->get_state() == question_state::$invalid) {
-            $output .= html_writer::nonempty_tag('div',
-                                        $question->get_validation_error($qa->get_last_qt_data()),
-                                        array('class' => 'validationerror'));
+            $output .= html_writer::div($question->get_validation_error($qa->get_last_qt_data()), 'validationerror');
         }
 
         if ($question->showmisplaced && $qa->get_state()->is_finished()) {
             $wrongparts = $question->get_drop_zones_without_hit($response);
             if (count($wrongparts) !== 0) {
-                $wrongpartsstringspans = array();
+                $wrongpartsstringspans = [];
                 foreach ($wrongparts as $wrongpart) {
-                    $wrongpartsstringspans[] = html_writer::nonempty_tag('span',
-                                    $wrongpart->markertext, array('class' => 'wrongpart'));
+                    $wrongpartsstringspans[] = html_writer::span($wrongpart->markertext, 'wrongpart');
                 }
                 $wrongpartsstring = join(', ', $wrongpartsstringspans);
-                $output .= html_writer::nonempty_tag('span',
-                                                    get_string('followingarewrongandhighlighted',
-                                                                'qtype_ddmarker',
-                                                                $wrongpartsstring),
-                                                    array('class' => 'wrongparts'));
+                $output .= html_writer::span(get_string('followingarewrongandhighlighted', 'qtype_ddmarker', $wrongpartsstring),
+                        'wrongparts');
             }
         }
 
-        $output .= html_writer::tag('div', $hiddenfields, array('class' => 'ddform'));
+        $output .= html_writer::div($hiddenfields, 'ddform');
+
+        $this->page->requires->js_call_amd('qtype_ddmarker/question', 'init',
+                [$qa->get_outer_question_div_unique_id(), $options->readonly, $visibledropzones]);
+
         return $output;
     }
+
     protected function hidden_field_choice(question_attempt $qa, $choiceno, $infinite, $noofdrags, $value = null) {
         $varname = 'c'.$choiceno;
         $classes = array('choices', 'choice'.$choiceno, 'noofdrags'.$noofdrags);
index b826e1d..65bcb07 100644 (file)
@@ -3,29 +3,46 @@
     display: block;
 }
 
-.que.ddmarker .draghome img,
-.que.ddmarker .draghome span {
-    visibility: hidden;
+.que.ddmarker .droparea {
+    display: inline-block;
+    position: relative;
+}
+
+.que.ddmarker .droparea .dropzones,
+.que.ddmarker .droparea .markertexts {
+    position: absolute;
+    top: 0;
+    left: 0;
 }
 
-.que.ddmarker .dragitems .dragitem {
+.que.ddmarker .draghomes .marker,
+.que.ddmarker .droparea .marker {
+    vertical-align: top;
     cursor: move;
+}
+
+.que.ddmarker .draghomes.readonly .marker,
+.que.ddmarker .droparea.readonly .marker {
+    cursor: auto;
+}
+
+.que.ddmarker .droparea .marker {
     position: absolute;
-    z-index: 2;
 }
 
-.que.ddmarker .dragitems .draghome {
+.que.ddmarker .draghomes .marker {
+    position: relative;
     display: inline-block;
     margin: 10px;
-    vertical-align: top;
 }
 
-.que.ddmarker .dragitems {
-    margin-top: 10px;
+.que.ddmarker .draghomes .marker.dragplaceholder {
+    display: none;
 }
 
-.que.ddmarker .dragitems.readonly .dragitem {
-    cursor: auto;
+.que.ddmarker .draghomes .marker.dragplaceholder.active {
+    visibility: hidden;
+    display: inline-block;
 }
 
 .que.ddmarker div.ddarea,
@@ -41,9 +58,16 @@ form.mform fieldset#id_previewareaheader div.ddarea .markertexts {
 form.mform fieldset#id_previewareaheader .dropbackground {
     margin: 0 auto;
     border: 1px solid black;
+}
+
+form.mform fieldset#id_previewareaheader .dropbackground {
     max-width: none;
 }
 
+.que.ddmarker .dropbackground.img-responsive.img-fluid {
+    width: 100%;
+}
+
 .que.ddmarker div.dragitems div.draghome,
 .que.ddmarker div.dragitems div.dragitem,
 form.mform fieldset#id_previewareaheader div.draghome,
@@ -51,7 +75,8 @@ form.mform fieldset#id_previewareaheader div.drag {
     font: 13px/1.231 arial, helvetica, clean, sans-serif;
 }
 
-.que.ddmarker div.dragitems span.markertext,
+.que.ddmarker .droparea .marker span.markertext,
+.que.ddmarker .draghomes .marker span.markertext,
 .que.ddmarker div.markertexts span.markertext,
 form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
     margin: 0 5px;
@@ -66,11 +91,17 @@ form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
     opacity: 0.6;
 }
 
+.que.ddmarker .droparea .marker span.markertext,
+.que.ddmarker .draghomes .marker span.markertext {
+    white-space: nowrap;
+}
+
 .que.ddmarker div.markertexts span.markertext {
     z-index: 2;
     background-color: yellow;
     border: 2px solid khaki;
     position: absolute;
+    white-space: nowrap;
 }
 
 .que.ddmarker span.wrongpart {
@@ -84,7 +115,8 @@ form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
     display: inline-block;
 }
 
-.que.ddmarker div.dragitems img.target {
+.que.ddmarker .droparea .marker img.target,
+.que.ddmarker .draghomes .marker img.target {
     position: absolute;
     left: -7px; /* This must be half the size of the target image, minus 0.5. */
     top: -7px;  /* In other words, this works for a 15x15 cross-hair. */
@@ -94,7 +126,11 @@ form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
     display: none;
 }
 
-.que.ddmarker .dragitem.beingdragged span.markertext {
+.que.ddmarker .marker.beingdragged {
+    position: absolute;
+}
+
+.que.ddmarker .marker.beingdragged span.markertext {
     z-index: 3;
     box-shadow: 3px 3px 4px #000;
 }
@@ -196,3 +232,7 @@ body#page-question-type-ddmarker div[id^=fitem_id_][id*=hintclearwrong_] {
 body#page-question-type-ddmarker #fitem_id_penalty {
     margin-bottom: 2em;
 }
+
+body#page-question-type-ddmarker .ddarea.que.ddmarker {
+    overflow-y: scroll;
+}
index 364f48b..04cabba 100644 (file)
@@ -37,22 +37,20 @@ class behat_qtype_ddmarker extends behat_base {
 
     /**
      * Get the xpath for a given drag item.
-     * @param string $dragitem the text of the item to drag.
+     *
+     * @param string $marker the text of the item to drag.
+     * @param bool $iskeyboard is using keyboard or not.
      * @return string the xpath expression.
      */
-    protected function marker_xpath($marker, $item = 0) {
-        return '//span[contains(@class, " dragitem ") and contains(@class, " item' . $item .
-                '") and span[@class = "markertext" and contains(normalize-space(.), "' .
-                $this->escape($marker) . '")]]';
-    }
-
-    protected function parse_marker_name($marker) {
-        $item = 0;
-        if (preg_match('~,(\d+)$~', $marker, $matches)) {
-            $item = $matches[1];
-            $marker = substr($marker, 0, -1 - strlen($item));
+    protected function marker_xpath($marker, $iskeyboard = false) {
+        if ($iskeyboard) {
+            return '//span[contains(@class, "marker") and not(contains(@class, "dragplaceholder")) ' .
+                    'and span[@class = "markertext" and contains(normalize-space(.), "' .
+                    $this->escape($marker) . '")]]';
         }
-        return array($marker, $item);
+        return '//span[contains(@class, "marker") and contains(@class, "unneeded") ' .
+                'and not(contains(@class, "dragplaceholder")) and span[@class = "markertext" and contains(normalize-space(.), "' .
+                $this->escape($marker) . '")]]';
     }
 
     /**
@@ -64,7 +62,6 @@ class behat_qtype_ddmarker extends behat_base {
      * @Given /^I drag "(?P<marker>[^"]*)" to "(?P<coordinates>\d+,\d+)" in the drag and drop markers question$/
      */
     public function i_drag_to_in_the_drag_and_drop_markers_question($marker, $coordinates) {
-        list($marker, $item) = $this->parse_marker_name($marker);
         list($x, $y) = explode(',', $coordinates);
 
         // This is a bit nasty, but Behat (indeed Selenium) will only drag on
@@ -81,7 +78,6 @@ class behat_qtype_ddmarker extends behat_base {
                     var target = document.createElement('div');
                     target.setAttribute('id', 'target-{$x}-{$y}');
                     var container = document.querySelector('.droparea');
-                    container.style.setProperty('position', 'relative');
                     container.insertBefore(target, image);
                     var xadjusted = {$x} + (container.offsetWidth - image.offsetWidth) / 2;
                     var yadjusted = {$y} + (container.offsetHeight - image.offsetHeight) / 2;
@@ -93,7 +89,7 @@ class behat_qtype_ddmarker extends behat_base {
                 }())");
 
         $generalcontext = behat_context_helper::get('behat_general');
-        $generalcontext->i_drag_and_i_drop_it_in($this->marker_xpath($marker, $item),
+        $generalcontext->i_drag_and_i_drop_it_in($this->marker_xpath($marker),
                 'xpath_element', "#target-{$x}-{$y}", 'css_element');
     }
 
@@ -113,8 +109,7 @@ class behat_qtype_ddmarker extends behat_base {
             'left'  => chr(37),
             'right' => chr(39),
         );
-        list($marker, $item) = $this->parse_marker_name($marker);
-        $node = $this->get_selected_node('xpath_element', $this->marker_xpath($marker, $item));
+        $node = $this->get_selected_node('xpath_element', $this->marker_xpath($marker, true));
         $this->ensure_node_is_visible($node);
         for ($i = 0; $i < $repeats; $i++) {
             $node->keyDown($keycodes[$direction]);
index b95996e..560c688 100644 (file)
@@ -33,10 +33,10 @@ Feature: Preview a drag-drop marker question
     And I change window size to "large"
     And I wait "2" seconds
     # Odd, but the <br>s go to nothing, not a space.
-    And I drag "OU" to "342,230" in the drag and drop markers question
-    And I drag "Railway station" to "254,197" in the drag and drop markers question
-    And I drag "Railway station,1" to "326,319" in the drag and drop markers question
-    And I drag "Railway station,2" to "203,101" in the drag and drop markers question
+    And I drag "OU" to "345,230" in the drag and drop markers question
+    And I drag "Railway station" to "262,197" in the drag and drop markers question
+    And I drag "Railway station" to "334,319" in the drag and drop markers question
+    And I drag "Railway station" to "211,101" 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 "Correct"
     And I should see "Mark 1.00 out of 1.00"
index afaf4a6..527c71d 100644 (file)
@@ -46,7 +46,7 @@ class qtype_ddmarker_walkthrough_test extends qbehaviour_walkthrough_test_base {
      * @return question_contains_tag_with_attributes the expectation.
      */
     protected function get_contains_draggable_marker_home_expectation($choice, $infinite) {
-        $class = 'draghome choice'.$choice;
+        $class = 'marker choice'.$choice;
         if ($infinite) {
             $class .= ' infinite';
         }