diff --git a/amd/build/Line.min.js b/amd/build/Line.min.js index af51371..50dcbae 100644 --- a/amd/build/Line.min.js +++ b/amd/build/Line.min.js @@ -8,6 +8,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_drawlines/Line",(function(){function Point(x,y){this.x=x,this.y=y}function Line(labelstart,x1,y1,startRadius,labelend,x2,y2,endRadius,lineType){this.labelstart=labelstart,this.labelend=labelend,this.x1=x1||15,this.y1=y1||100,this.x2=x2||200,this.y2=y2||250,this.centre1=new Point(x1||0,y1||0),this.centre2=new Point(x2||0,y2||0),this.startRadius=startRadius,this.endRadius=endRadius,this.lineType=lineType}function createSvgElement(svg,tagName){var svgEl=svg.ownerDocument.createElementNS("http://www.w3.org/2000/svg",tagName);return svg.appendChild(svgEl),svgEl}return Point.prototype.toString=function(){return this.x+","+this.y},Point.prototype.move=function(dx,dy){this.x+=dx,this.y+=dy},Point.prototype.offset=function(offsetX,offsetY){return offsetX instanceof Point&&(offsetY=offsetX.y,offsetX=offsetX.x),new Point(this.x+offsetX,this.y+offsetY)},Point.parse=function(coordinates){var bits=coordinates.split(",");if(2!==bits.length)throw new Error(coordinates+" is not a valid point");return new Point(Math.round(bits[0]),Math.round(bits[1]))},Line.prototype=new Line,Line.prototype.getType=function(){return this.lineType},Line.prototype.getCoordinates=function(){return[this.centre1.x+","+this.centre1.y+";"+this.startRadius,this.centre2.x+","+this.centre2.y+";"+this.endRadius]},Line.prototype.makeSvg=function(svg){!function(svg){if(svg.getElementsByTagName("defs")[0])return;var svgdefsEl=svg.ownerDocument.createElementNS("http://www.w3.org/2000/svg","defs"),svgmarkerEl=svg.ownerDocument.createElementNS("http://www.w3.org/2000/svg","marker");svgmarkerEl.setAttribute("id","arrow"),svgmarkerEl.setAttribute("viewBox","0 0 10 10"),svgmarkerEl.setAttribute("refX","7"),svgmarkerEl.setAttribute("refY","5"),svgmarkerEl.setAttribute("markerWidth","4"),svgmarkerEl.setAttribute("markerHeight","4"),svgmarkerEl.setAttribute("orient","auto-start-reverse");var svgPathEl=svg.ownerDocument.createElementNS("http://www.w3.org/2000/svg","path");svgPathEl.setAttribute("d","M 0 0 L 10 5 L 0 10 z"),svgmarkerEl.appendChild(svgPathEl),svgdefsEl.appendChild(svgmarkerEl),svg.appendChild(svgdefsEl)}(svg);var svgEl=function(svg,tagName){var svgEl=createSvgElement(svg,"g");return createSvgElement(svgEl,tagName).setAttribute("class","shape"),createSvgElement(svgEl,"circle").setAttribute("class","startcircle shape"),createSvgElement(svgEl,"circle").setAttribute("class","endcircle shape"),createSvgElement(svgEl,"text").setAttribute("class","labelstart shapeLabel"),createSvgElement(svgEl,"text").setAttribute("class","labelend shapeLabel"),svgEl}(svg,"polyline");return this.updateSvg(svgEl),svgEl},Line.prototype.updateSvg=function(svgEl){this.drawLine(svgEl),svgEl.childNodes[1].setAttribute("cx",this.centre1.x),svgEl.childNodes[1].setAttribute("cy",this.centre1.y),svgEl.childNodes[1].setAttribute("r",Math.abs(this.startRadius)),svgEl.childNodes[2].setAttribute("cx",this.centre2.x),svgEl.childNodes[2].setAttribute("cy",this.centre2.y),svgEl.childNodes[2].setAttribute("r",Math.abs(this.endRadius)),svgEl.childNodes[3].textContent=this.labelstart,svgEl.childNodes[3].setAttribute("x",this.centre1.x),svgEl.childNodes[3].setAttribute("y",this.centre1.y+20),svgEl.childNodes[4].textContent=this.labelend,svgEl.childNodes[4].setAttribute("x",this.centre2.x),svgEl.childNodes[4].setAttribute("y",this.centre2.y+20)},Line.prototype.drawLine=function(svgEl){svgEl.childNodes[0].style.stroke="#000973",svgEl.childNodes[0].style["stroke-width"]="3",svgEl.childNodes[0].style["stroke-dasharray"]="10,3";var points=this.centre1.x+","+this.centre1.y+" "+this.centre2.x+","+this.centre2.y;switch(svgEl.childNodes[0].setAttribute("points",points),this.lineType){case"linesinglearrow":svgEl.childNodes[0].style["marker-end"]="url(#arrow)";break;case"linedoublearrows":svgEl.childNodes[0].style["marker-start"]="url(#arrow)",svgEl.childNodes[0].style["marker-end"]="url(#arrow)";break;case"lineinfinite":var newCoordinates=this.drawInfiniteLine(svgEl.parentNode),infiniteLine=newCoordinates[0]+","+newCoordinates[1]+" "+points+" "+newCoordinates[2]+","+newCoordinates[3];svgEl.childNodes[0].setAttribute("points",infiniteLine)}},Line.prototype.drawInfiniteLine=function(svg){const width=svg.width.baseVal.value,height=svg.height.baseVal.value,dx=this.centre2.x-this.centre1.x,dy=this.centre2.y-this.centre1.y;let xMin,yMin,xMax,yMax;if(0===dx)xMin=xMax=this.centre1.x,yMin=0,yMax=height;else if(0===dy)xMin=0,xMax=width,yMin=yMax=this.centre1.y;else{const slope=dy/dx,intercept=this.centre1.y-slope*this.centre1.x;xMin=-width,yMin=slope*xMin+intercept,xMax=2*width,yMax=slope*xMax+intercept,yMin<0?(yMin=0,xMin=(yMin-intercept)/slope):yMin>height&&(yMin=height,xMin=(yMin-intercept)/slope),yMax<0?(yMax=0,xMax=(yMax-intercept)/slope):yMax>height&&(yMax=height,xMax=(yMax-intercept)/slope)}return[Math.round(xMin),Math.round(yMin),Math.round(xMax),Math.round(yMax)]},Line.prototype.parse=function(startcoordinates,endcoordinates,ratio){var startcoordinatesbits=startcoordinates.split(";"),endcoordinatesbits=endcoordinates.split(";");return this.centre1=Point.parse(startcoordinatesbits[0]),this.centre2=Point.parse(endcoordinatesbits[0]),this.centre1.x=this.centre1.x*parseFloat(ratio),this.centre1.y=this.centre1.y*parseFloat(ratio),this.x1=this.centre1.x*parseFloat(ratio),this.y1=this.centre1.y*parseFloat(ratio),this.x2=this.centre2.x*parseFloat(ratio),this.y2=this.centre2.y*parseFloat(ratio),this.centre2.x=this.centre2.x*parseFloat(ratio),this.centre2.y=this.centre2.y*parseFloat(ratio),this.startRadius=Math.round(startcoordinatesbits[1])*parseFloat(ratio),this.endRadius=Math.round(endcoordinatesbits[1])*parseFloat(ratio),!0},Line.prototype.move=function(handleIndex,dx,dy,maxX,maxY){"0"===handleIndex?(this.centre1.move(dx,dy),this.centre1.xmaxX-this.startRadius&&(this.centre1.x=maxX-this.startRadius,this.x1=maxX-this.startRadius),this.centre1.ymaxY-this.endRadius&&(this.centre1.y=maxY-this.endRadius,this.y1=maxY-this.endRadius)):(this.centre2.move(dx,dy),this.centre2.xmaxX-this.startRadius&&(this.centre2.x=maxX-this.startRadius,this.x2=maxX-this.startRadius),this.centre2.ymaxY-this.endRadius&&(this.centre2.y=maxY-this.endRadius,this.y2=maxY-this.endRadius))},Line.prototype.edit=function(handleIndex,dx,dy,maxX,maxY){var limit=0;"0"===handleIndex?(this.startRadius+=dx,limit=Math.min(this.centre1.x,this.centre1.y,maxX-this.centre1.x,maxY-this.centre1.y),this.startRadius>limit&&(this.startRadius=limit),this.startRadius<-limit&&(this.startRadius=-limit)):(this.endRadius+=dx,limit=Math.min(this.centre2.x,this.centre2.y,maxX-this.centre2.x,maxY-this.centre2.y),this.endRadius>limit&&(this.endRadius=limit),this.endRadius<-limit&&(this.endRadius=-limit))},Line.prototype.getHandlePositions=function(){return{moveHandles:[new Point(this.centre1.x,this.centre1.y),new Point(this.centre2.x,this.centre2.y)],editHandles:[this.centre1.offset(this.startRadius,0),this.centre2.offset(this.endRadius,0)]}},Line.prototype.normalizeShape=function(){this.startRadius=Math.abs(this.startRadius),this.endRadius=Math.abs(this.endRadius)},{Point:Point,Line:Line,createSvgElement:createSvgElement,make:function(linecoordinates,labels,lineType){var startcoordinates=linecoordinates[0].split(";"),endcoordinates=linecoordinates[1].split(";"),linestartbits=startcoordinates[0].split(","),lineendbits=endcoordinates[0].split(",");return new Line(labels[0],linestartbits[0],linestartbits[1],startcoordinates[1],labels[1],lineendbits[0],lineendbits[1],endcoordinates[1],lineType)},getSimilar:function(lineType,line){return new Line(line.labelstart,line.x1,line.y1,line.startRadius,line.labelend,line.x2,line.y2,line.endRadius,lineType)}}})); +define("qtype_drawlines/Line",(function(){function Point(x,y){this.x=x,this.y=y}function Line(labelstart,x1,y1,startRadius,labelend,x2,y2,endRadius,lineType){this.labelstart=labelstart,this.labelend=labelend,this.x1=x1||15,this.y1=y1||100,this.x2=x2||200,this.y2=y2||250,this.centre1=new Point(x1||0,y1||0),this.centre2=new Point(x2||0,y2||0),this.startRadius=startRadius,this.endRadius=endRadius,this.lineType=lineType}function createSvgElement(svg,tagName){var svgEl=svg.ownerDocument.createElementNS("http://www.w3.org/2000/svg",tagName);return svg.appendChild(svgEl),svgEl}return Point.prototype.toString=function(){return this.x+","+this.y},Point.prototype.move=function(dx,dy){this.x+=dx,this.y+=dy},Point.prototype.offset=function(offsetX,offsetY){return offsetX instanceof Point&&(offsetY=offsetX.y,offsetX=offsetX.x),new Point(this.x+offsetX,this.y+offsetY)},Point.parse=function(coordinates){var bits=coordinates.split(",");if(2!==bits.length)throw new Error(coordinates+" is not a valid point");return new Point(Math.round(bits[0]),Math.round(bits[1]))},Line.prototype=new Line,Line.prototype.getType=function(){return this.lineType},Line.prototype.getCoordinates=function(){return[this.centre1.x+","+this.centre1.y+";"+this.startRadius,this.centre2.x+","+this.centre2.y+";"+this.endRadius]},Line.prototype.makeSvg=function(svg){!function(svg){if(svg.getElementsByTagName("defs")[0])return;var svgdefsEl=svg.ownerDocument.createElementNS("http://www.w3.org/2000/svg","defs"),svgmarkerEl=svg.ownerDocument.createElementNS("http://www.w3.org/2000/svg","marker");svgmarkerEl.setAttribute("id","arrow"),svgmarkerEl.setAttribute("viewBox","0 0 10 10"),svgmarkerEl.setAttribute("refX","7"),svgmarkerEl.setAttribute("refY","5"),svgmarkerEl.setAttribute("markerWidth","4"),svgmarkerEl.setAttribute("markerHeight","4"),svgmarkerEl.setAttribute("orient","auto-start-reverse");var svgPathEl=svg.ownerDocument.createElementNS("http://www.w3.org/2000/svg","path");svgPathEl.setAttribute("d","M 0 0 L 10 5 L 0 10 z"),svgmarkerEl.appendChild(svgPathEl),svgdefsEl.appendChild(svgmarkerEl),svg.appendChild(svgdefsEl)}(svg);var svgEl=function(svg,tagName){var svgEl=createSvgElement(svg,"g");return createSvgElement(svgEl,tagName).setAttribute("class","shape"),createSvgElement(svgEl,"circle").setAttribute("class","startcircle shape"),createSvgElement(svgEl,"circle").setAttribute("class","endcircle shape"),createSvgElement(svgEl,"text").setAttribute("class","labelstart shapeLabel"),createSvgElement(svgEl,"text").setAttribute("class","labelend shapeLabel"),svgEl}(svg,"polyline");return this.updateSvg(svgEl),svgEl},Line.prototype.updateSvg=function(svgEl){this.drawLine(svgEl),svgEl.childNodes[1].setAttribute("cx",this.centre1.x),svgEl.childNodes[1].setAttribute("cy",this.centre1.y),svgEl.childNodes[1].setAttribute("r",Math.abs(this.startRadius)),svgEl.childNodes[2].setAttribute("cx",this.centre2.x),svgEl.childNodes[2].setAttribute("cy",this.centre2.y),svgEl.childNodes[2].setAttribute("r",Math.abs(this.endRadius)),svgEl.childNodes[3].textContent=this.labelstart,svgEl.childNodes[3].setAttribute("x",this.centre1.x),svgEl.childNodes[3].setAttribute("y",parseInt(this.centre1.y)+20),svgEl.childNodes[4].textContent=this.labelend,svgEl.childNodes[4].setAttribute("x",this.centre2.x),svgEl.childNodes[4].setAttribute("y",parseInt(this.centre2.y)+20)},Line.prototype.drawLine=function(svgEl){svgEl.childNodes[0].style.stroke="#000973",svgEl.childNodes[0].style["stroke-width"]="3",svgEl.childNodes[0].style["stroke-dasharray"]="10,3";var points=this.centre1.x+","+this.centre1.y+" "+this.centre2.x+","+this.centre2.y;switch(svgEl.childNodes[0].setAttribute("points",points),this.lineType){case"linesinglearrow":svgEl.childNodes[0].style["marker-end"]="url(#arrow)";break;case"linedoublearrows":svgEl.childNodes[0].style["marker-start"]="url(#arrow)",svgEl.childNodes[0].style["marker-end"]="url(#arrow)";break;case"lineinfinite":var newCoordinates=this.drawInfiniteLine(svgEl.parentNode),infiniteLine=newCoordinates[0]+","+newCoordinates[1]+" "+points+" "+newCoordinates[2]+","+newCoordinates[3];svgEl.childNodes[0].setAttribute("points",infiniteLine)}},Line.prototype.drawInfiniteLine=function(svg){const width=svg.width.baseVal.value,height=svg.height.baseVal.value,dx=this.centre2.x-this.centre1.x,dy=this.centre2.y-this.centre1.y;let xMin,yMin,xMax,yMax;if(0===dx)xMin=xMax=this.centre1.x,yMin=0,yMax=height;else if(0===dy)xMin=0,xMax=width,yMin=yMax=this.centre1.y;else{const slope=dy/dx,intercept=this.centre1.y-slope*this.centre1.x;xMin=-width,yMin=slope*xMin+intercept,xMax=2*width,yMax=slope*xMax+intercept,yMin<0?(yMin=0,xMin=(yMin-intercept)/slope):yMin>height&&(yMin=height,xMin=(yMin-intercept)/slope),yMax<0?(yMax=0,xMax=(yMax-intercept)/slope):yMax>height&&(yMax=height,xMax=(yMax-intercept)/slope)}return[Math.round(xMin),Math.round(yMin),Math.round(xMax),Math.round(yMax)]},Line.prototype.parse=function(startcoordinates,endcoordinates,ratio){var startcoordinatesbits=startcoordinates.split(";"),endcoordinatesbits=endcoordinates.split(";");return this.centre1=Point.parse(startcoordinatesbits[0]),this.centre2=Point.parse(endcoordinatesbits[0]),this.centre1.x=this.centre1.x*parseFloat(ratio),this.centre1.y=this.centre1.y*parseFloat(ratio),this.x1=this.centre1.x*parseFloat(ratio),this.y1=this.centre1.y*parseFloat(ratio),this.x2=this.centre2.x*parseFloat(ratio),this.y2=this.centre2.y*parseFloat(ratio),this.centre2.x=this.centre2.x*parseFloat(ratio),this.centre2.y=this.centre2.y*parseFloat(ratio),this.startRadius=Math.round(startcoordinatesbits[1])*parseFloat(ratio),this.endRadius=Math.round(endcoordinatesbits[1])*parseFloat(ratio),!0},Line.prototype.move=function(handleIndex,dx,dy,maxX,maxY){"0"===handleIndex?(this.centre1.move(dx,dy),this.centre1.xmaxX-this.startRadius&&(this.centre1.x=maxX-this.startRadius,this.x1=maxX-this.startRadius),this.centre1.ymaxY-this.endRadius&&(this.centre1.y=maxY-this.endRadius,this.y1=maxY-this.endRadius)):(this.centre2.move(dx,dy),this.centre2.xmaxX-this.startRadius&&(this.centre2.x=maxX-this.startRadius,this.x2=maxX-this.startRadius),this.centre2.ymaxY-this.endRadius&&(this.centre2.y=maxY-this.endRadius,this.y2=maxY-this.endRadius))},Line.prototype.moveDrags=function(dx,dy,maxX,maxY,whichSVG,lineNo){this.centre1.move(dx,dy),this.centre2.move(dx,dy),"svgDropZones"===whichSVG?(this.centre1.x=50,this.centre1.y=25+50*lineNo,this.x1=50,this.y1=25+50*lineNo,this.centre2.x=200,this.x2=200,this.centre2.y=25+50*lineNo,this.y2=25+50*lineNo):(this.centre1.xmaxX-this.startRadius&&(this.centre1.x=maxX-this.startRadius,this.x1=maxX-this.startRadius),this.centre2.xmaxX-this.startRadius&&(this.centre2.x=maxX-this.startRadius,this.x2=maxX-this.startRadius),this.centre1.ymaxY-this.endRadius&&(this.centre1.y=maxY-this.endRadius,this.y1=maxY-this.endRadius),this.centre2.ymaxY-this.endRadius&&(this.centre2.y=maxY-this.endRadius,this.y2=maxY-this.endRadius))},Line.prototype.addToDropZone=function(svgDrags,svgDropZones,selectedElement,dropX,dropY){var maxY=0,dropzoneNo=selectedElement.getAttribute("data-dropzone-no");this.isInsideSVG(svgDrags,dropX,dropY)?(maxY=svgDropZones.height.baseVal.value,svgDropZones.appendChild(selectedElement),selectedElement.getAttribute("data-dropzone-no"),this.centre1.y=maxY-2*this.startRadius-50*dropzoneNo,this.y1=maxY-2*this.startRadius-50*dropzoneNo,this.centre2.y=maxY-2*this.endRadius-50*dropzoneNo,this.y2=maxY-2*this.endRadius-50*dropzoneNo):this.isInsideSVG(svgDropZones,dropX,dropY)&&(svgDrags.appendChild(selectedElement),this.centre1.x=50,this.centre1.y=this.startRadius+50*dropzoneNo,this.y1=this.startRadius+50*dropzoneNo,this.centre2.x=200,this.centre2.y=this.endRadius+50*dropzoneNo,this.y2=this.endRadius+50*dropzoneNo)},Line.prototype.isInsideSVG=function(svg,dropX,dropY){const rect=svg.getBoundingClientRect();return dropX>=rect.left&&dropX<=rect.right&&dropY>=rect.top&&dropY<=rect.bottom},Line.prototype.edit=function(handleIndex,dx,dy,maxX,maxY){var limit=0;"0"===handleIndex?(this.startRadius+=dx,limit=Math.min(this.centre1.x,this.centre1.y,maxX-this.centre1.x,maxY-this.centre1.y),this.startRadius>limit&&(this.startRadius=limit),this.startRadius<-limit&&(this.startRadius=-limit)):(this.endRadius+=dx,limit=Math.min(this.centre2.x,this.centre2.y,maxX-this.centre2.x,maxY-this.centre2.y),this.endRadius>limit&&(this.endRadius=limit),this.endRadius<-limit&&(this.endRadius=-limit))},Line.prototype.getHandlePositions=function(){return{moveHandles:[new Point(this.centre1.x,this.centre1.y),new Point(this.centre2.x,this.centre2.y)],editHandles:[this.centre1.offset(this.startRadius,0),this.centre2.offset(this.endRadius,0)]}},Line.prototype.normalizeShape=function(){this.startRadius=Math.abs(this.startRadius),this.endRadius=Math.abs(this.endRadius)},{Point:Point,Line:Line,createSvgElement:createSvgElement,make:function(linecoordinates,labels,lineType){var startcoordinates=linecoordinates[0].split(";"),endcoordinates=linecoordinates[1].split(";"),linestartbits=startcoordinates[0].split(","),lineendbits=endcoordinates[0].split(",");return new Line(labels[0],linestartbits[0],linestartbits[1],startcoordinates[1],labels[1],lineendbits[0],lineendbits[1],endcoordinates[1],lineType)},getSimilar:function(lineType,line){return new Line(line.labelstart,parseInt(line.x1),parseInt(line.y1),parseInt(line.startRadius),parseInt(line.labelend),parseInt(line.x2),parseInt(line.y2),parseInt(line.endRadius),lineType)}}})); //# sourceMappingURL=Line.min.js.map \ No newline at end of file diff --git a/amd/build/Line.min.js.map b/amd/build/Line.min.js.map index 338b1bd..f7af0b4 100644 --- a/amd/build/Line.min.js.map +++ b/amd/build/Line.min.js.map @@ -1 +1 @@ -{"version":3,"file":"Line.min.js","sources":["../src/Line.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/* eslint max-depth: [\"error\", 8] */\n\n/**\n * Library of classes for handling simple shapes.\n *\n * These classes can represent shapes, let you alter them, can go to and from a string\n * representation, and can give you an SVG representation.\n *\n * @module qtype_drawlines/drawLine\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(function() {\n\n \"use strict\";\n\n /**\n * A point, with x and y coordinates.\n *\n * @param {int} x centre X.\n * @param {int} y centre Y.\n * @constructor\n */\n function Point(x, y) {\n this.x = x;\n this.y = y;\n }\n\n /**\n * Standard toString method.\n * @returns {string} \"x;y\";\n */\n Point.prototype.toString = function() {\n return this.x + ',' + this.y;\n };\n\n /**\n * Move a point\n * @param {int} dx x offset\n * @param {int} dy y offset\n */\n Point.prototype.move = function(dx, dy) {\n this.x += dx;\n this.y += dy;\n };\n\n /**\n * Return a new point that is a certain position relative to this one.\n *\n * @param {(int|Point)} offsetX if a point, offset by this points coordinates, else and int x offset.\n * @param {int} [offsetY] used if offsetX is an int, the corresponding y offset.\n * @return {Point} the new point.\n */\n Point.prototype.offset = function(offsetX, offsetY) {\n if (offsetX instanceof Point) {\n offsetY = offsetX.y;\n offsetX = offsetX.x;\n }\n return new Point(this.x + offsetX, this.y + offsetY);\n };\n\n /**\n * Make a point from the string representation.\n *\n * @param {String} coordinates \"x,y\".\n * @return {Point} the point. Throws an exception if input is not valid.\n */\n Point.parse = function(coordinates) {\n var bits = coordinates.split(',');\n if (bits.length !== 2) {\n throw new Error(coordinates + ' is not a valid point');\n }\n return new Point(Math.round(bits[0]), Math.round(bits[1]));\n };\n\n /**\n * Line constructor. Class to represent the different types of drop zone shapes.\n *\n * @param {String} [labelstart] start label of a line.\n * @param {int} [x1] centre X1.\n * @param {int} [y1] centre Y1.\n * @param {int} [startRadius] startRadius.\n * @param {String} [labelend] end label of a line.\n * @param {int} [x2] centre X2.\n * @param {int} [y2] centre Y2.\n * @param {int} [endRadius] endRadius.\n * @param {String} [lineType] Line type.\n * @constructor\n */\n function Line(labelstart, x1, y1, startRadius, labelend, x2, y2, endRadius, lineType) {\n this.labelstart = labelstart;\n this.labelend = labelend;\n this.x1 = x1 || 15;\n this.y1 = y1 || 100;\n\n this.x2 = x2 || 200;\n this.y2 = y2 || 250;\n\n this.centre1 = new Point(x1 || 0, y1 || 0);\n this.centre2 = new Point(x2 || 0, y2 || 0);\n\n this.startRadius = startRadius;\n this.endRadius = endRadius;\n\n this.lineType = lineType;\n }\n Line.prototype = new Line();\n\n\n /**\n * Get the type of shape.\n *\n * @return {String} 'linesinglearrow', 'linedoublearrows', 'lineinfinite'.\n */\n Line.prototype.getType = function() {\n return this.lineType;\n };\n\n /**\n * Get the string representation of this shape.\n *\n * @return {String} coordinates as they need to be typed into the form.\n */\n Line.prototype.getCoordinates = function() {\n return [\n this.centre1.x + ',' + this.centre1.y + ';' + this.startRadius,\n this.centre2.x + ',' + this.centre2.y + ';' + this.endRadius\n ];\n };\n\n /**\n * Create the svg group with line.\n *\n * @param {SVGElement} svg the SVG graphic to add this shape to.\n * @return {SVGElement} SVG representation of this shape.\n */\n Line.prototype.makeSvg = function(svg) {\n addLineArrow(svg);\n var svgEl = createSvgShapeGroup(svg, 'polyline');\n this.updateSvg(svgEl);\n return svgEl;\n };\n\n /**\n * Update the SVG representation of this shape.\n *\n * @param {SVGElement} svgEl the SVG representation of this shape.\n */\n Line.prototype.updateSvg = function(svgEl) {\n\n // Set line attributes.\n this.drawLine(svgEl);\n\n // Set start and end circle attributes.\n svgEl.childNodes[1].setAttribute('cx', this.centre1.x);\n svgEl.childNodes[1].setAttribute('cy', this.centre1.y);\n svgEl.childNodes[1].setAttribute('r', Math.abs(this.startRadius));\n\n svgEl.childNodes[2].setAttribute('cx', this.centre2.x);\n svgEl.childNodes[2].setAttribute('cy', this.centre2.y);\n svgEl.childNodes[2].setAttribute('r', Math.abs(this.endRadius));\n\n // Set start and end label attributes.\n svgEl.childNodes[3].textContent = this.labelstart;\n svgEl.childNodes[3].setAttribute('x', this.centre1.x);\n svgEl.childNodes[3].setAttribute('y', this.centre1.y + 20);\n\n svgEl.childNodes[4].textContent = this.labelend;\n svgEl.childNodes[4].setAttribute('x', this.centre2.x);\n svgEl.childNodes[4].setAttribute('y', this.centre2.y + 20);\n };\n\n /**\n * Update svg line attributes.\n *\n * @param {SVGElement} svgEl the SVG representation of the shape.\n */\n Line.prototype.drawLine = function(svgEl) {\n // Set attributes for the polyline.\n svgEl.childNodes[0].style.stroke = \"#000973\";\n svgEl.childNodes[0].style['stroke-width'] = \"3\";\n svgEl.childNodes[0].style['stroke-dasharray'] = \"10,3\";\n\n var points = this.centre1.x + \",\" + this.centre1.y + \" \" + this.centre2.x + \",\" + this.centre2.y;\n svgEl.childNodes[0].setAttribute('points', points);\n\n // Set attributes to display line based on linetype.\n switch (this.lineType) {\n case 'linesinglearrow':\n svgEl.childNodes[0].style['marker-end'] = \"url(#arrow)\";\n break;\n\n case 'linedoublearrows':\n svgEl.childNodes[0].style['marker-start'] = \"url(#arrow)\";\n svgEl.childNodes[0].style['marker-end'] = \"url(#arrow)\";\n break;\n\n case 'lineinfinite':\n var newCoordinates = this.drawInfiniteLine(svgEl.parentNode);\n var infiniteLine = newCoordinates[0] + \",\" + newCoordinates[1] +\n \" \" + points + \" \" + newCoordinates[2] + \",\" + newCoordinates[3];\n svgEl.childNodes[0].setAttribute('points', infiniteLine);\n break;\n }\n };\n\n /**\n * Get the minimum and maximum endpoints of the line to draw an infinite line.\n *\n * @param {SVGElement} svg the SVG representation of the shape.\n */\n Line.prototype.drawInfiniteLine = function(svg) {\n\n const width = svg.width.baseVal.value;\n const height = svg.height.baseVal.value;\n\n // Calculate slope\n const dx = this.centre2.x - this.centre1.x;\n const dy = this.centre2.y - this.centre1.y;\n\n // Calculate points far outside the SVG canvas\n let xMin, yMin, xMax, yMax;\n if (dx === 0) { // Vertical line\n xMin = xMax = this.centre1.x;\n yMin = 0;\n yMax = height;\n } else if (dy === 0) { // Horizontal line\n xMin = 0;\n xMax = width;\n yMin = yMax = this.centre1.y;\n } else {\n const slope = dy / dx;\n const intercept = this.centre1.y - slope * this.centre1.x;\n\n // Find intersection points with SVG canvas borders\n xMin = -width; // Starting far left\n yMin = slope * xMin + intercept;\n\n xMax = 2 * width; // Extending far right\n yMax = slope * xMax + intercept;\n\n // Clamp to canvas height bounds\n if (yMin < 0) {\n yMin = 0;\n xMin = (yMin - intercept) / slope;\n } else if (yMin > height) {\n yMin = height;\n xMin = (yMin - intercept) / slope;\n }\n\n if (yMax < 0) {\n yMax = 0;\n xMax = (yMax - intercept) / slope;\n } else if (yMax > height) {\n yMax = height;\n xMax = (yMax - intercept) / slope;\n }\n }\n return [Math.round(xMin), Math.round(yMin), Math.round(xMax), Math.round(yMax)];\n\n };\n\n /**\n * Parse the coordinates from the string representation.\n *\n * @param {String} startcoordinates \"x1,y1\".\n * @param {String} endcoordinates \"x1,y1\".\n * @param {float} ratio .\n * @return {Point} the point. Throws an exception if input is not valid.\n */\n Line.prototype.parse = function(startcoordinates, endcoordinates, ratio) {\n var startcoordinatesbits = startcoordinates.split(';');\n var endcoordinatesbits = endcoordinates.split(';');\n this.centre1 = Point.parse(startcoordinatesbits[0]);\n this.centre2 = Point.parse(endcoordinatesbits[0]);\n this.centre1.x = this.centre1.x * parseFloat(ratio);\n this.centre1.y = this.centre1.y * parseFloat(ratio);\n this.x1 = this.centre1.x * parseFloat(ratio);\n this.y1 = this.centre1.y * parseFloat(ratio);\n this.x2 = this.centre2.x * parseFloat(ratio);\n this.y2 = this.centre2.y * parseFloat(ratio);\n this.centre2.x = this.centre2.x * parseFloat(ratio);\n this.centre2.y = this.centre2.y * parseFloat(ratio);\n this.startRadius = Math.round(startcoordinatesbits[1]) * parseFloat(ratio);\n this.endRadius = Math.round(endcoordinatesbits[1]) * parseFloat(ratio);\n\n return true;\n };\n\n /**\n * Move the entire shape by this offset.\n *\n * @param {String} handleIndex which handle was moved.\n * @param {int} dx x offset.\n * @param {int} dy y offset.\n * @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.\n * @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.\n */\n Line.prototype.move = function(handleIndex, dx, dy, maxX, maxY) {\n if (handleIndex === '0') {\n this.centre1.move(dx, dy);\n if (this.centre1.x < this.startRadius) {\n this.centre1.x = this.startRadius;\n this.x1 = this.startRadius;\n }\n if (this.centre1.x > maxX - this.startRadius) {\n this.centre1.x = maxX - this.startRadius;\n this.x1 = maxX - this.startRadius;\n }\n if (this.centre1.y < this.endRadius) {\n this.centre1.y = this.endRadius;\n this.y1 = this.endRadius;\n }\n if (this.centre1.y > maxY - this.endRadius) {\n this.centre1.y = maxY - this.endRadius;\n this.y1 = maxY - this.endRadius;\n }\n } else {\n this.centre2.move(dx, dy);\n if (this.centre2.x < this.startRadius) {\n this.centre2.x = this.startRadius;\n this.x2 = this.startRadius;\n }\n if (this.centre2.x > maxX - this.startRadius) {\n this.centre2.x = maxX - this.startRadius;\n this.x2 = maxX - this.startRadius;\n }\n if (this.centre2.y < this.endRadius) {\n this.centre2.y = this.endRadius;\n this.y2 = this.endRadius;\n }\n if (this.centre2.y > maxY - this.endRadius) {\n this.centre2.y = maxY - this.endRadius;\n this.y2 = maxY - this.endRadius;\n }\n }\n };\n\n /**\n * Move one of the edit handles by this offset.\n *\n * @param {String} handleIndex which handle was moved.\n * @param {int} dx x offset.\n * @param {int} dy y offset.\n * @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.\n * @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.\n */\n Line.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {\n var limit = 0;\n if (handleIndex === '0') {\n this.startRadius += dx;\n limit = Math.min(this.centre1.x, this.centre1.y, maxX - this.centre1.x, maxY - this.centre1.y);\n if (this.startRadius > limit) {\n this.startRadius = limit;\n }\n if (this.startRadius < -limit) {\n this.startRadius = -limit;\n }\n } else {\n this.endRadius += dx;\n limit = Math.min(this.centre2.x, this.centre2.y, maxX - this.centre2.x, maxY - this.centre2.y);\n if (this.endRadius > limit) {\n this.endRadius = limit;\n }\n if (this.endRadius < -limit) {\n this.endRadius = -limit;\n }\n }\n };\n\n /**\n * Get the handles that should be offered to edit this shape, or null if not appropriate.\n *\n * @return {Object[]} with properties moveHandleStart {Point}, moveHandleEnd {Point} and editHandles {Point[]}.\n */\n Line.prototype.getHandlePositions = function() {\n return {\n moveHandles: [new Point(this.centre1.x, this.centre1.y), new Point(this.centre2.x, this.centre2.y)],\n editHandles: [this.centre1.offset(this.startRadius, 0), this.centre2.offset(this.endRadius, 0)]\n };\n };\n\n /**\n * Update the properties of this shape after a sequence of edits.\n *\n * For example make sure the circle radius is positive, of the polygon centre is centred.\n */\n Line.prototype.normalizeShape = function() {\n this.startRadius = Math.abs(this.startRadius);\n this.endRadius = Math.abs(this.endRadius);\n };\n\n /**\n * Add a new arrow SVG DOM element as a child of svg.\n *\n * @param {SVGElement} svg the parent node.\n * @return {SVGElement} the newly created node.\n */\n function addLineArrow(svg) {\n if (svg.getElementsByTagName('defs')[0]) {\n return;\n }\n var svgdefsEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'defs');\n var svgmarkerEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'marker');\n svgmarkerEl.setAttribute('id', 'arrow');\n svgmarkerEl.setAttribute('viewBox', \"0 0 10 10\");\n svgmarkerEl.setAttribute('refX', '7');\n svgmarkerEl.setAttribute('refY', '5');\n svgmarkerEl.setAttribute('markerWidth', '4');\n svgmarkerEl.setAttribute('markerHeight', '4');\n svgmarkerEl.setAttribute('orient', 'auto-start-reverse');\n var svgPathEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'path');\n svgPathEl.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');\n svgmarkerEl.appendChild(svgPathEl);\n svgdefsEl.appendChild(svgmarkerEl);\n\n svg.appendChild(svgdefsEl);\n }\n\n /**\n * Make a new SVG DOM element as a child of svg.\n *\n * @param {SVGElement} svg the parent node.\n * @param {String} tagName the tag name.\n * @return {SVGElement} the newly created node.\n */\n function createSvgElement(svg, tagName) {\n var svgEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', tagName);\n svg.appendChild(svgEl);\n return svgEl;\n }\n\n /**\n * Make a group SVG DOM elements containing a polyline of the given linetype as first child,\n * two circles to mark the allowed radius for grading and text labels for the line.\n *\n * @param {SVGElement} svg the parent node.\n * @param {String} tagName the tag name.\n * @return {SVGElement} the newly created g element.\n */\n function createSvgShapeGroup(svg, tagName) {\n var svgEl = createSvgElement(svg, 'g');\n createSvgElement(svgEl, tagName).setAttribute('class', 'shape');\n createSvgElement(svgEl, 'circle').setAttribute('class', 'startcircle shape');\n createSvgElement(svgEl, 'circle').setAttribute('class', 'endcircle shape');\n createSvgElement(svgEl, 'text').setAttribute('class', 'labelstart shapeLabel');\n createSvgElement(svgEl, 'text').setAttribute('class', 'labelend shapeLabel');\n return svgEl;\n }\n\n /**\n * @alias module:qtype_drawlines/drawLine\n */\n return {\n /**\n * A point, with x and y coordinates.\n *\n * @param {int} x centre X.\n * @param {int} y centre Y.\n * @constructor\n */\n Point: Point,\n\n /**\n * Line constructor. Class to represent the different types of drop zone shapes.\n *\n * @param {String} [labelstart] start label of a line.\n * @param {int} [x1] centre X1.\n * @param {int} [y1] centre Y1.\n * @param {int} [startRadius] startRadius.\n * @param {String} [labelend] end label of a line.\n * @param {int} [x2] centre X2.\n * @param {int} [y2] centre Y2.\n * @param {int} [endRadius] endRadius.\n * @param {String} [lineType] Line type.\n * @constructor\n */\n Line: Line,\n\n /**\n * Make a new SVG DOM element as a child of svg.\n *\n * @param {SVGElement} svg the parent node.\n * @param {String} tagName the tag name.\n * @return {SVGElement} the newly created node.\n */\n createSvgElement: createSvgElement,\n\n /**\n * Make a line of the given type.\n *\n * @param {Array} linecoordinates in the format (x,y;radius).\n * @param {Array} labels Start and end labels of a line.\n * @param {String} lineType The linetype (e.g., linesinglearrow, linedoublearrows, ...).\n * @return {Line} the new line.\n */\n make: function(linecoordinates, labels, lineType) {\n // Line coordinates are in the format (x,y;radius).\n var startcoordinates = linecoordinates[0].split(';');\n var endcoordinates = linecoordinates[1].split(';');\n var linestartbits = startcoordinates[0].split(',');\n var lineendbits = endcoordinates[0].split(',');\n\n return new Line(labels[0], linestartbits[0], linestartbits[1], startcoordinates[1], labels[1],\n lineendbits[0], lineendbits[1], endcoordinates[1], lineType);\n },\n\n /**\n * Make a line of the given linetype having similar coordinates and labels as the original type.\n *\n * @param {String} lineType the new type of line to make.\n * @param {line} line the line to copy.\n * @return {line} the similar line of a different linetype.\n */\n getSimilar: function(lineType, line) {\n return new Line(line.labelstart, line.x1, line.y1, line.startRadius, line.labelend,\n line.x2, line.y2, line.endRadius, lineType);\n }\n };\n});\n"],"names":["define","Point","x","y","Line","labelstart","x1","y1","startRadius","labelend","x2","y2","endRadius","lineType","centre1","centre2","createSvgElement","svg","tagName","svgEl","ownerDocument","createElementNS","appendChild","prototype","toString","this","move","dx","dy","offset","offsetX","offsetY","parse","coordinates","bits","split","length","Error","Math","round","getType","getCoordinates","makeSvg","getElementsByTagName","svgdefsEl","svgmarkerEl","setAttribute","svgPathEl","addLineArrow","createSvgShapeGroup","updateSvg","drawLine","childNodes","abs","textContent","style","stroke","points","newCoordinates","drawInfiniteLine","parentNode","infiniteLine","width","baseVal","value","height","xMin","yMin","xMax","yMax","slope","intercept","startcoordinates","endcoordinates","ratio","startcoordinatesbits","endcoordinatesbits","parseFloat","handleIndex","maxX","maxY","edit","limit","min","getHandlePositions","moveHandles","editHandles","normalizeShape","make","linecoordinates","labels","linestartbits","lineendbits","getSimilar","line"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"Line.min.js","sources":["../src/Line.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/* eslint max-depth: [\"error\", 8] */\n\n/**\n * Library of classes for handling simple shapes.\n *\n * These classes can represent shapes, let you alter them, can go to and from a string\n * representation, and can give you an SVG representation.\n *\n * @module qtype_drawlines/drawLine\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(function() {\n\n \"use strict\";\n\n /**\n * A point, with x and y coordinates.\n *\n * @param {int} x centre X.\n * @param {int} y centre Y.\n * @constructor\n */\n function Point(x, y) {\n this.x = x;\n this.y = y;\n }\n\n /**\n * Standard toString method.\n * @returns {string} \"x;y\";\n */\n Point.prototype.toString = function() {\n return this.x + ',' + this.y;\n };\n\n /**\n * Move a point\n * @param {int} dx x offset\n * @param {int} dy y offset\n */\n Point.prototype.move = function(dx, dy) {\n this.x += dx;\n this.y += dy;\n };\n\n /**\n * Return a new point that is a certain position relative to this one.\n *\n * @param {(int|Point)} offsetX if a point, offset by this points coordinates, else and int x offset.\n * @param {int} [offsetY] used if offsetX is an int, the corresponding y offset.\n * @return {Point} the new point.\n */\n Point.prototype.offset = function(offsetX, offsetY) {\n if (offsetX instanceof Point) {\n offsetY = offsetX.y;\n offsetX = offsetX.x;\n }\n return new Point(this.x + offsetX, this.y + offsetY);\n };\n\n /**\n * Make a point from the string representation.\n *\n * @param {String} coordinates \"x,y\".\n * @return {Point} the point. Throws an exception if input is not valid.\n */\n Point.parse = function(coordinates) {\n var bits = coordinates.split(',');\n if (bits.length !== 2) {\n throw new Error(coordinates + ' is not a valid point');\n }\n return new Point(Math.round(bits[0]), Math.round(bits[1]));\n };\n\n /**\n * Line constructor. Class to represent the different types of drop zone shapes.\n *\n * @param {String} [labelstart] start label of a line.\n * @param {int} [x1] centre X1.\n * @param {int} [y1] centre Y1.\n * @param {int} [startRadius] startRadius.\n * @param {String} [labelend] end label of a line.\n * @param {int} [x2] centre X2.\n * @param {int} [y2] centre Y2.\n * @param {int} [endRadius] endRadius.\n * @param {String} [lineType] Line type.\n * @constructor\n */\n function Line(labelstart, x1, y1, startRadius, labelend, x2, y2, endRadius, lineType) {\n this.labelstart = labelstart;\n this.labelend = labelend;\n this.x1 = x1 || 15;\n this.y1 = y1 || 100;\n\n this.x2 = x2 || 200;\n this.y2 = y2 || 250;\n\n this.centre1 = new Point(x1 || 0, y1 || 0);\n this.centre2 = new Point(x2 || 0, y2 || 0);\n\n this.startRadius = startRadius;\n this.endRadius = endRadius;\n\n this.lineType = lineType;\n }\n Line.prototype = new Line();\n\n\n /**\n * Get the type of shape.\n *\n * @return {String} 'linesinglearrow', 'linedoublearrows', 'lineinfinite'.\n */\n Line.prototype.getType = function() {\n return this.lineType;\n };\n\n /**\n * Get the string representation of this shape.\n *\n * @return {String} coordinates as they need to be typed into the form.\n */\n Line.prototype.getCoordinates = function() {\n return [\n this.centre1.x + ',' + this.centre1.y + ';' + this.startRadius,\n this.centre2.x + ',' + this.centre2.y + ';' + this.endRadius\n ];\n };\n\n /**\n * Create the svg group with line.\n *\n * @param {SVGElement} svg the SVG graphic to add this shape to.\n * @return {SVGElement} SVG representation of this shape.\n */\n Line.prototype.makeSvg = function(svg) {\n addLineArrow(svg);\n var svgEl = createSvgShapeGroup(svg, 'polyline');\n this.updateSvg(svgEl);\n return svgEl;\n };\n\n /**\n * Update the SVG representation of this shape.\n *\n * @param {SVGElement} svgEl the SVG representation of this shape.\n */\n Line.prototype.updateSvg = function(svgEl) {\n\n // Set line attributes.\n this.drawLine(svgEl);\n\n // Set start and end circle attributes.\n svgEl.childNodes[1].setAttribute('cx', this.centre1.x);\n svgEl.childNodes[1].setAttribute('cy', this.centre1.y);\n svgEl.childNodes[1].setAttribute('r', Math.abs(this.startRadius));\n\n svgEl.childNodes[2].setAttribute('cx', this.centre2.x);\n svgEl.childNodes[2].setAttribute('cy', this.centre2.y);\n svgEl.childNodes[2].setAttribute('r', Math.abs(this.endRadius));\n\n // Set start and end label attributes.\n svgEl.childNodes[3].textContent = this.labelstart;\n svgEl.childNodes[3].setAttribute('x', this.centre1.x);\n svgEl.childNodes[3].setAttribute('y', parseInt(this.centre1.y) + 20);\n\n svgEl.childNodes[4].textContent = this.labelend;\n svgEl.childNodes[4].setAttribute('x', this.centre2.x);\n svgEl.childNodes[4].setAttribute('y', parseInt(this.centre2.y) + 20);\n };\n\n /**\n * Update svg line attributes.\n *\n * @param {SVGElement} svgEl the SVG representation of the shape.\n */\n Line.prototype.drawLine = function(svgEl) {\n // Set attributes for the polyline.\n svgEl.childNodes[0].style.stroke = \"#000973\";\n svgEl.childNodes[0].style['stroke-width'] = \"3\";\n svgEl.childNodes[0].style['stroke-dasharray'] = \"10,3\";\n\n var points = this.centre1.x + \",\" + this.centre1.y + \" \" + this.centre2.x + \",\" + this.centre2.y;\n svgEl.childNodes[0].setAttribute('points', points);\n\n // Set attributes to display line based on linetype.\n switch (this.lineType) {\n case 'linesinglearrow':\n svgEl.childNodes[0].style['marker-end'] = \"url(#arrow)\";\n break;\n\n case 'linedoublearrows':\n svgEl.childNodes[0].style['marker-start'] = \"url(#arrow)\";\n svgEl.childNodes[0].style['marker-end'] = \"url(#arrow)\";\n break;\n\n case 'lineinfinite':\n var newCoordinates = this.drawInfiniteLine(svgEl.parentNode);\n var infiniteLine = newCoordinates[0] + \",\" + newCoordinates[1] +\n \" \" + points + \" \" + newCoordinates[2] + \",\" + newCoordinates[3];\n svgEl.childNodes[0].setAttribute('points', infiniteLine);\n break;\n }\n };\n\n /**\n * Get the minimum and maximum endpoints of the line to draw an infinite line.\n *\n * @param {SVGElement} svg the SVG representation of the shape.\n */\n Line.prototype.drawInfiniteLine = function(svg) {\n\n const width = svg.width.baseVal.value;\n const height = svg.height.baseVal.value;\n\n // Calculate slope\n const dx = this.centre2.x - this.centre1.x;\n const dy = this.centre2.y - this.centre1.y;\n\n // Calculate points far outside the SVG canvas\n let xMin, yMin, xMax, yMax;\n if (dx === 0) { // Vertical line\n xMin = xMax = this.centre1.x;\n yMin = 0;\n yMax = height;\n } else if (dy === 0) { // Horizontal line\n xMin = 0;\n xMax = width;\n yMin = yMax = this.centre1.y;\n } else {\n const slope = dy / dx;\n const intercept = this.centre1.y - slope * this.centre1.x;\n\n // Find intersection points with SVG canvas borders\n xMin = -width; // Starting far left\n yMin = slope * xMin + intercept;\n\n xMax = 2 * width; // Extending far right\n yMax = slope * xMax + intercept;\n\n // Clamp to canvas height bounds\n if (yMin < 0) {\n yMin = 0;\n xMin = (yMin - intercept) / slope;\n } else if (yMin > height) {\n yMin = height;\n xMin = (yMin - intercept) / slope;\n }\n\n if (yMax < 0) {\n yMax = 0;\n xMax = (yMax - intercept) / slope;\n } else if (yMax > height) {\n yMax = height;\n xMax = (yMax - intercept) / slope;\n }\n }\n return [Math.round(xMin), Math.round(yMin), Math.round(xMax), Math.round(yMax)];\n\n };\n\n /**\n * Parse the coordinates from the string representation.\n *\n * @param {String} startcoordinates \"x1,y1\".\n * @param {String} endcoordinates \"x1,y1\".\n * @param {float} ratio .\n * @return {Point} the point. Throws an exception if input is not valid.\n */\n Line.prototype.parse = function(startcoordinates, endcoordinates, ratio) {\n var startcoordinatesbits = startcoordinates.split(';');\n var endcoordinatesbits = endcoordinates.split(';');\n this.centre1 = Point.parse(startcoordinatesbits[0]);\n this.centre2 = Point.parse(endcoordinatesbits[0]);\n this.centre1.x = this.centre1.x * parseFloat(ratio);\n this.centre1.y = this.centre1.y * parseFloat(ratio);\n this.x1 = this.centre1.x * parseFloat(ratio);\n this.y1 = this.centre1.y * parseFloat(ratio);\n this.x2 = this.centre2.x * parseFloat(ratio);\n this.y2 = this.centre2.y * parseFloat(ratio);\n this.centre2.x = this.centre2.x * parseFloat(ratio);\n this.centre2.y = this.centre2.y * parseFloat(ratio);\n this.startRadius = Math.round(startcoordinatesbits[1]) * parseFloat(ratio);\n this.endRadius = Math.round(endcoordinatesbits[1]) * parseFloat(ratio);\n\n return true;\n };\n\n /**\n * Move the entire shape by this offset.\n *\n * @param {String} handleIndex which handle was moved.\n * @param {int} dx x offset.\n * @param {int} dy y offset.\n * @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.\n * @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.\n */\n Line.prototype.move = function(handleIndex, dx, dy, maxX, maxY) {\n if (handleIndex === '0') {\n this.centre1.move(dx, dy);\n if (this.centre1.x < this.startRadius) {\n this.centre1.x = this.startRadius;\n this.x1 = this.startRadius;\n }\n if (this.centre1.x > maxX - this.startRadius) {\n this.centre1.x = maxX - this.startRadius;\n this.x1 = maxX - this.startRadius;\n }\n if (this.centre1.y < this.endRadius) {\n this.centre1.y = this.endRadius;\n this.y1 = this.endRadius;\n }\n if (this.centre1.y > maxY - this.endRadius) {\n this.centre1.y = maxY - this.endRadius;\n this.y1 = maxY - this.endRadius;\n }\n } else {\n this.centre2.move(dx, dy);\n if (this.centre2.x < this.startRadius) {\n this.centre2.x = this.startRadius;\n this.x2 = this.startRadius;\n }\n if (this.centre2.x > maxX - this.startRadius) {\n this.centre2.x = maxX - this.startRadius;\n this.x2 = maxX - this.startRadius;\n }\n if (this.centre2.y < this.endRadius) {\n this.centre2.y = this.endRadius;\n this.y2 = this.endRadius;\n }\n if (this.centre2.y > maxY - this.endRadius) {\n this.centre2.y = maxY - this.endRadius;\n this.y2 = maxY - this.endRadius;\n }\n }\n };\n\n /**\n * Move the entire line by this offset.\n *\n * @param {int} dx x offset.\n * @param {int} dy y offset.\n * @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.\n * @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.\n * @param {String} whichSVG The svg containing the drag.\n * @param {int} lineNo The line number\n */\n Line.prototype.moveDrags = function(dx, dy, maxX, maxY, whichSVG, lineNo) {\n this.centre1.move(dx, dy);\n this.centre2.move(dx, dy);\n if (whichSVG === 'svgDropZones') {\n this.centre1.x = 50;\n this.centre1.y = 25 + lineNo * 50;\n this.x1 = 50;\n this.y1 = 25 + lineNo * 50;\n this.centre2.x = 200;\n this.x2 = 200;\n this.centre2.y = 25 + lineNo * 50;\n this.y2 = 25 + lineNo * 50;\n } else {\n if (this.centre1.x < this.startRadius) {\n this.centre1.x = this.startRadius;\n this.x1 = this.startRadius;\n }\n if (this.centre1.x > maxX - this.startRadius) {\n this.centre1.x = maxX - this.startRadius;\n this.x1 = maxX - this.startRadius;\n }\n if (this.centre2.x < this.startRadius) {\n this.centre2.x = this.startRadius;\n this.x2 = this.startRadius;\n }\n if (this.centre2.x > maxX - this.startRadius) {\n this.centre2.x = maxX - this.startRadius;\n this.x2 = maxX - this.startRadius;\n }\n if (this.centre1.y < this.endRadius) {\n this.centre1.y = this.endRadius;\n this.y1 = this.endRadius;\n }\n if (this.centre1.y > maxY - this.endRadius) {\n this.centre1.y = maxY - this.endRadius;\n this.y1 = maxY - this.endRadius;\n }\n if (this.centre2.y < this.endRadius) {\n this.centre2.y = this.endRadius;\n this.y2 = this.endRadius;\n }\n if (this.centre2.y > maxY - this.endRadius) {\n this.centre2.y = maxY - this.endRadius;\n this.y2 = maxY - this.endRadius;\n }\n }\n };\n\n /**\n * Move the g element to the dropzone.\n * @param {SVGElement} svgDrags Svg element containing the drags.\n * @param {SVGElement} svgDropZones Svg element containing the dropZone.\n * @param {SVGElement} selectedElement The element selected for dragging.\n * @param {int} dropX\n * @param {int} dropY\n */\n Line.prototype.addToDropZone = function(svgDrags, svgDropZones, selectedElement, dropX, dropY) {\n var maxY = 0;\n var dropzoneNo = selectedElement.getAttribute('data-dropzone-no');\n\n if (this.isInsideSVG(svgDrags, dropX, dropY)) {\n // Append the element to the second SVG\n // Get the height of the dropZone SVG.\n maxY = svgDropZones.height.baseVal.value;\n svgDropZones.appendChild(selectedElement);\n selectedElement.getAttribute('data-dropzone-no');\n\n // Caluculate the position of line drop.\n this.centre1.y = maxY - (2 * this.startRadius) - (dropzoneNo * 50);\n this.y1 = maxY - (2 * this.startRadius) - (dropzoneNo * 50);\n this.centre2.y = maxY - (2 * this.endRadius) - (dropzoneNo * 50);\n this.y2 = maxY - (2 * this.endRadius) - (dropzoneNo * 50);\n } else if (this.isInsideSVG(svgDropZones, dropX, dropY)) {\n // Append the element to the first SVG (to ensure it stays in the same SVG if dropped there)\n svgDrags.appendChild(selectedElement);\n\n // We want to drop the lines from the top, depending on the line number.\n // Caluculate the position of line drop.\n this.centre1.x = 50;\n this.centre1.y = this.startRadius + (dropzoneNo * 50);\n this.y1 = this.startRadius + (dropzoneNo * 50);\n this.centre2.x = 200;\n this.centre2.y = this.endRadius + (dropzoneNo * 50);\n this.y2 = this.endRadius + (dropzoneNo * 50);\n }\n };\n\n /**\n * Check if the current selected element is in the svg .\n * @param {SVGElement} svg Svg element containing the drags.\n * @param {int} dropX\n * @param {int} dropY\n * @return {bool}\n */\n Line.prototype.isInsideSVG = function(svg, dropX, dropY){\n const rect = svg.getBoundingClientRect();\n return dropX >= rect.left && dropX <= rect.right && dropY >= rect.top && dropY <= rect.bottom;\n };\n\n /**\n * Move one of the edit handles by this offset.\n *\n * @param {String} handleIndex which handle was moved.\n * @param {int} dx x offset.\n * @param {int} dy y offset.\n * @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.\n * @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.\n */\n Line.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {\n var limit = 0;\n if (handleIndex === '0') {\n this.startRadius += dx;\n limit = Math.min(this.centre1.x, this.centre1.y, maxX - this.centre1.x, maxY - this.centre1.y);\n if (this.startRadius > limit) {\n this.startRadius = limit;\n }\n if (this.startRadius < -limit) {\n this.startRadius = -limit;\n }\n } else {\n this.endRadius += dx;\n limit = Math.min(this.centre2.x, this.centre2.y, maxX - this.centre2.x, maxY - this.centre2.y);\n if (this.endRadius > limit) {\n this.endRadius = limit;\n }\n if (this.endRadius < -limit) {\n this.endRadius = -limit;\n }\n }\n };\n\n /**\n * Get the handles that should be offered to edit this shape, or null if not appropriate.\n *\n * @return {Object[]} with properties moveHandleStart {Point}, moveHandleEnd {Point} and editHandles {Point[]}.\n */\n Line.prototype.getHandlePositions = function() {\n return {\n moveHandles: [new Point(this.centre1.x, this.centre1.y), new Point(this.centre2.x, this.centre2.y)],\n editHandles: [this.centre1.offset(this.startRadius, 0), this.centre2.offset(this.endRadius, 0)]\n };\n };\n\n /**\n * Update the properties of this shape after a sequence of edits.\n *\n * For example make sure the circle radius is positive, of the polygon centre is centred.\n */\n Line.prototype.normalizeShape = function() {\n this.startRadius = Math.abs(this.startRadius);\n this.endRadius = Math.abs(this.endRadius);\n };\n\n /**\n * Add a new arrow SVG DOM element as a child of svg.\n *\n * @param {SVGElement} svg the parent node.\n * @return {SVGElement} the newly created node.\n */\n function addLineArrow(svg) {\n if (svg.getElementsByTagName('defs')[0]) {\n return;\n }\n var svgdefsEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'defs');\n var svgmarkerEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'marker');\n svgmarkerEl.setAttribute('id', 'arrow');\n svgmarkerEl.setAttribute('viewBox', \"0 0 10 10\");\n svgmarkerEl.setAttribute('refX', '7');\n svgmarkerEl.setAttribute('refY', '5');\n svgmarkerEl.setAttribute('markerWidth', '4');\n svgmarkerEl.setAttribute('markerHeight', '4');\n svgmarkerEl.setAttribute('orient', 'auto-start-reverse');\n var svgPathEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'path');\n svgPathEl.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');\n svgmarkerEl.appendChild(svgPathEl);\n svgdefsEl.appendChild(svgmarkerEl);\n\n svg.appendChild(svgdefsEl);\n }\n\n /**\n * Make a new SVG DOM element as a child of svg.\n *\n * @param {SVGElement} svg the parent node.\n * @param {String} tagName the tag name.\n * @return {SVGElement} the newly created node.\n */\n function createSvgElement(svg, tagName) {\n var svgEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', tagName);\n svg.appendChild(svgEl);\n return svgEl;\n }\n\n /**\n * Make a group SVG DOM elements containing a polyline of the given linetype as first child,\n * two circles to mark the allowed radius for grading and text labels for the line.\n *\n * @param {SVGElement} svg the parent node.\n * @param {String} tagName the tag name.\n * @return {SVGElement} the newly created g element.\n */\n function createSvgShapeGroup(svg, tagName) {\n var svgEl = createSvgElement(svg, 'g');\n createSvgElement(svgEl, tagName).setAttribute('class', 'shape');\n createSvgElement(svgEl, 'circle').setAttribute('class', 'startcircle shape');\n createSvgElement(svgEl, 'circle').setAttribute('class', 'endcircle shape');\n createSvgElement(svgEl, 'text').setAttribute('class', 'labelstart shapeLabel');\n createSvgElement(svgEl, 'text').setAttribute('class', 'labelend shapeLabel');\n return svgEl;\n }\n\n /**\n * @alias module:qtype_drawlines/drawLine\n */\n return {\n /**\n * A point, with x and y coordinates.\n *\n * @param {int} x centre X.\n * @param {int} y centre Y.\n * @constructor\n */\n Point: Point,\n\n /**\n * Line constructor. Class to represent the different types of drop zone shapes.\n *\n * @param {String} [labelstart] start label of a line.\n * @param {int} [x1] centre X1.\n * @param {int} [y1] centre Y1.\n * @param {int} [startRadius] startRadius.\n * @param {String} [labelend] end label of a line.\n * @param {int} [x2] centre X2.\n * @param {int} [y2] centre Y2.\n * @param {int} [endRadius] endRadius.\n * @param {String} [lineType] Line type.\n * @constructor\n */\n Line: Line,\n\n /**\n * Make a new SVG DOM element as a child of svg.\n *\n * @param {SVGElement} svg the parent node.\n * @param {String} tagName the tag name.\n * @return {SVGElement} the newly created node.\n */\n createSvgElement: createSvgElement,\n\n /**\n * Make a line of the given type.\n *\n * @param {Array} linecoordinates in the format (x,y;radius).\n * @param {Array} labels Start and end labels of a line.\n * @param {String} lineType The linetype (e.g., linesinglearrow, linedoublearrows, ...).\n * @return {Line} the new line.\n */\n make: function(linecoordinates, labels, lineType) {\n // Line coordinates are in the format (x,y;radius).\n var startcoordinates = linecoordinates[0].split(';');\n var endcoordinates = linecoordinates[1].split(';');\n var linestartbits = startcoordinates[0].split(',');\n var lineendbits = endcoordinates[0].split(',');\n\n return new Line(labels[0], linestartbits[0], linestartbits[1], startcoordinates[1], labels[1],\n lineendbits[0], lineendbits[1], endcoordinates[1], lineType);\n },\n\n /**\n * Make a line of the given linetype having similar coordinates and labels as the original type.\n *\n * @param {String} lineType the new type of line to make.\n * @param {line} line the line to copy.\n * @return {line} the similar line of a different linetype.\n */\n getSimilar: function(lineType, line) {\n return new Line(line.labelstart, parseInt(line.x1), parseInt(line.y1), parseInt(line.startRadius),\n parseInt(line.labelend), parseInt(line.x2), parseInt(line.y2), parseInt(line.endRadius), lineType);\n }\n };\n});\n"],"names":["define","Point","x","y","Line","labelstart","x1","y1","startRadius","labelend","x2","y2","endRadius","lineType","centre1","centre2","createSvgElement","svg","tagName","svgEl","ownerDocument","createElementNS","appendChild","prototype","toString","this","move","dx","dy","offset","offsetX","offsetY","parse","coordinates","bits","split","length","Error","Math","round","getType","getCoordinates","makeSvg","getElementsByTagName","svgdefsEl","svgmarkerEl","setAttribute","svgPathEl","addLineArrow","createSvgShapeGroup","updateSvg","drawLine","childNodes","abs","textContent","parseInt","style","stroke","points","newCoordinates","drawInfiniteLine","parentNode","infiniteLine","width","baseVal","value","height","xMin","yMin","xMax","yMax","slope","intercept","startcoordinates","endcoordinates","ratio","startcoordinatesbits","endcoordinatesbits","parseFloat","handleIndex","maxX","maxY","moveDrags","whichSVG","lineNo","addToDropZone","svgDrags","svgDropZones","selectedElement","dropX","dropY","dropzoneNo","getAttribute","isInsideSVG","rect","getBoundingClientRect","left","right","top","bottom","edit","limit","min","getHandlePositions","moveHandles","editHandles","normalizeShape","make","linecoordinates","labels","linestartbits","lineendbits","getSimilar","line"],"mappings":";;;;;;;;;;AA4BAA,+BAAO,oBAWMC,MAAMC,EAAGC,QACTD,EAAIA,OACJC,EAAIA,WAgEJC,KAAKC,WAAYC,GAAIC,GAAIC,YAAaC,SAAUC,GAAIC,GAAIC,UAAWC,eACnER,WAAaA,gBACbI,SAAWA,cACXH,GAAKA,IAAM,QACXC,GAAKA,IAAM,SAEXG,GAAKA,IAAM,SACXC,GAAKA,IAAM,SAEXG,QAAU,IAAIb,MAAMK,IAAM,EAAGC,IAAM,QACnCQ,QAAU,IAAId,MAAMS,IAAM,EAAGC,IAAM,QAEnCH,YAAcA,iBACdI,UAAYA,eAEZC,SAAWA,kBA+aXG,iBAAiBC,IAAKC,aACvBC,MAAQF,IAAIG,cAAcC,gBAAgB,6BAA8BH,gBAC5ED,IAAIK,YAAYH,OACTA,aA1fXlB,MAAMsB,UAAUC,SAAW,kBAChBC,KAAKvB,EAAI,IAAMuB,KAAKtB,GAQ/BF,MAAMsB,UAAUG,KAAO,SAASC,GAAIC,SAC3B1B,GAAKyB,QACLxB,GAAKyB,IAUd3B,MAAMsB,UAAUM,OAAS,SAASC,QAASC,gBACnCD,mBAAmB7B,QACnB8B,QAAUD,QAAQ3B,EAClB2B,QAAUA,QAAQ5B,GAEf,IAAID,MAAMwB,KAAKvB,EAAI4B,QAASL,KAAKtB,EAAI4B,UAShD9B,MAAM+B,MAAQ,SAASC,iBACfC,KAAOD,YAAYE,MAAM,QACT,IAAhBD,KAAKE,aACC,IAAIC,MAAMJ,YAAc,gCAE3B,IAAIhC,MAAMqC,KAAKC,MAAML,KAAK,IAAKI,KAAKC,MAAML,KAAK,MAkC1D9B,KAAKmB,UAAY,IAAInB,KAQrBA,KAAKmB,UAAUiB,QAAU,kBACdf,KAAKZ,UAQhBT,KAAKmB,UAAUkB,eAAiB,iBACrB,CACHhB,KAAKX,QAAQZ,EAAI,IAAMuB,KAAKX,QAAQX,EAAI,IAAMsB,KAAKjB,YACnDiB,KAAKV,QAAQb,EAAI,IAAMuB,KAAKV,QAAQZ,EAAI,IAAMsB,KAAKb,YAU3DR,KAAKmB,UAAUmB,QAAU,SAASzB,eAmXXA,QACfA,IAAI0B,qBAAqB,QAAQ,cAGjCC,UAAY3B,IAAIG,cAAcC,gBAAgB,6BAA8B,QAC5EwB,YAAc5B,IAAIG,cAAcC,gBAAgB,6BAA8B,UAClFwB,YAAYC,aAAa,KAAM,SAC/BD,YAAYC,aAAa,UAAW,aACpCD,YAAYC,aAAa,OAAQ,KACjCD,YAAYC,aAAa,OAAQ,KACjCD,YAAYC,aAAa,cAAe,KACxCD,YAAYC,aAAa,eAAgB,KACzCD,YAAYC,aAAa,SAAU,0BAC/BC,UAAY9B,IAAIG,cAAcC,gBAAgB,6BAA8B,QAChF0B,UAAUD,aAAa,IAAK,yBAC5BD,YAAYvB,YAAYyB,WACxBH,UAAUtB,YAAYuB,aAEtB5B,IAAIK,YAAYsB,WApYhBI,CAAa/B,SACTE,eA2ZqBF,IAAKC,aAC1BC,MAAQH,iBAAiBC,IAAK,YAClCD,iBAAiBG,MAAOD,SAAS4B,aAAa,QAAS,SACvD9B,iBAAiBG,MAAO,UAAU2B,aAAa,QAAS,qBACxD9B,iBAAiBG,MAAO,UAAU2B,aAAa,QAAS,mBACxD9B,iBAAiBG,MAAO,QAAQ2B,aAAa,QAAS,yBACtD9B,iBAAiBG,MAAO,QAAQ2B,aAAa,QAAS,uBAC/C3B,MAlaK8B,CAAoBhC,IAAK,wBAChCiC,UAAU/B,OACRA,OAQXf,KAAKmB,UAAU2B,UAAY,SAAS/B,YAG3BgC,SAAShC,OAGdA,MAAMiC,WAAW,GAAGN,aAAa,KAAMrB,KAAKX,QAAQZ,GACpDiB,MAAMiC,WAAW,GAAGN,aAAa,KAAMrB,KAAKX,QAAQX,GACpDgB,MAAMiC,WAAW,GAAGN,aAAa,IAAKR,KAAKe,IAAI5B,KAAKjB,cAEpDW,MAAMiC,WAAW,GAAGN,aAAa,KAAMrB,KAAKV,QAAQb,GACpDiB,MAAMiC,WAAW,GAAGN,aAAa,KAAMrB,KAAKV,QAAQZ,GACpDgB,MAAMiC,WAAW,GAAGN,aAAa,IAAKR,KAAKe,IAAI5B,KAAKb,YAGpDO,MAAMiC,WAAW,GAAGE,YAAc7B,KAAKpB,WACvCc,MAAMiC,WAAW,GAAGN,aAAa,IAAKrB,KAAKX,QAAQZ,GACnDiB,MAAMiC,WAAW,GAAGN,aAAa,IAAKS,SAAS9B,KAAKX,QAAQX,GAAK,IAEjEgB,MAAMiC,WAAW,GAAGE,YAAc7B,KAAKhB,SACvCU,MAAMiC,WAAW,GAAGN,aAAa,IAAKrB,KAAKV,QAAQb,GACnDiB,MAAMiC,WAAW,GAAGN,aAAa,IAAKS,SAAS9B,KAAKV,QAAQZ,GAAK,KAQrEC,KAAKmB,UAAU4B,SAAW,SAAShC,OAE/BA,MAAMiC,WAAW,GAAGI,MAAMC,OAAS,UACnCtC,MAAMiC,WAAW,GAAGI,MAAM,gBAAkB,IAC5CrC,MAAMiC,WAAW,GAAGI,MAAM,oBAAsB,WAE5CE,OAASjC,KAAKX,QAAQZ,EAAI,IAAMuB,KAAKX,QAAQX,EAAI,IAAMsB,KAAKV,QAAQb,EAAI,IAAMuB,KAAKV,QAAQZ,SAC/FgB,MAAMiC,WAAW,GAAGN,aAAa,SAAUY,QAGnCjC,KAAKZ,cACJ,kBACDM,MAAMiC,WAAW,GAAGI,MAAM,cAAgB,wBAGzC,mBACDrC,MAAMiC,WAAW,GAAGI,MAAM,gBAAkB,cAC5CrC,MAAMiC,WAAW,GAAGI,MAAM,cAAgB,wBAGzC,mBACGG,eAAiBlC,KAAKmC,iBAAiBzC,MAAM0C,YAC7CC,aAAeH,eAAe,GAAK,IAAMA,eAAe,GACxD,IAAMD,OAAS,IAAMC,eAAe,GAAK,IAAMA,eAAe,GAClExC,MAAMiC,WAAW,GAAGN,aAAa,SAAUgB,gBAUvD1D,KAAKmB,UAAUqC,iBAAmB,SAAS3C,WAEjC8C,MAAQ9C,IAAI8C,MAAMC,QAAQC,MAC1BC,OAASjD,IAAIiD,OAAOF,QAAQC,MAG5BtC,GAAKF,KAAKV,QAAQb,EAAIuB,KAAKX,QAAQZ,EACnC0B,GAAKH,KAAKV,QAAQZ,EAAIsB,KAAKX,QAAQX,MAGrCgE,KAAMC,KAAMC,KAAMC,QACX,IAAP3C,GACAwC,KAAOE,KAAO5C,KAAKX,QAAQZ,EAC3BkE,KAAO,EACPE,KAAOJ,YACJ,GAAW,IAAPtC,GACPuC,KAAO,EACPE,KAAON,MACPK,KAAOE,KAAO7C,KAAKX,QAAQX,MACxB,OACGoE,MAAQ3C,GAAKD,GACb6C,UAAY/C,KAAKX,QAAQX,EAAIoE,MAAQ9C,KAAKX,QAAQZ,EAGxDiE,MAAQJ,MACRK,KAAOG,MAAQJ,KAAOK,UAEtBH,KAAO,EAAIN,MACXO,KAAOC,MAAQF,KAAOG,UAGlBJ,KAAO,GACPA,KAAO,EACPD,MAAQC,KAAOI,WAAaD,OACrBH,KAAOF,SACdE,KAAOF,OACPC,MAAQC,KAAOI,WAAaD,OAG5BD,KAAO,GACPA,KAAO,EACPD,MAAQC,KAAOE,WAAaD,OACrBD,KAAOJ,SACdI,KAAOJ,OACPG,MAAQC,KAAOE,WAAaD,aAG7B,CAACjC,KAAKC,MAAM4B,MAAO7B,KAAKC,MAAM6B,MAAO9B,KAAKC,MAAM8B,MAAO/B,KAAKC,MAAM+B,QAY7ElE,KAAKmB,UAAUS,MAAQ,SAASyC,iBAAkBC,eAAgBC,WAC1DC,qBAAuBH,iBAAiBtC,MAAM,KAC9C0C,mBAAqBH,eAAevC,MAAM,iBACzCrB,QAAUb,MAAM+B,MAAM4C,qBAAqB,SAC3C7D,QAAUd,MAAM+B,MAAM6C,mBAAmB,SACzC/D,QAAQZ,EAAIuB,KAAKX,QAAQZ,EAAI4E,WAAWH,YACxC7D,QAAQX,EAAIsB,KAAKX,QAAQX,EAAI2E,WAAWH,YACxCrE,GAAKmB,KAAKX,QAAQZ,EAAI4E,WAAWH,YACjCpE,GAAKkB,KAAKX,QAAQX,EAAI2E,WAAWH,YACjCjE,GAAKe,KAAKV,QAAQb,EAAI4E,WAAWH,YACjChE,GAAKc,KAAKV,QAAQZ,EAAI2E,WAAWH,YACjC5D,QAAQb,EAAIuB,KAAKV,QAAQb,EAAI4E,WAAWH,YACxC5D,QAAQZ,EAAIsB,KAAKV,QAAQZ,EAAI2E,WAAWH,YACxCnE,YAAc8B,KAAKC,MAAMqC,qBAAqB,IAAME,WAAWH,YAC/D/D,UAAY0B,KAAKC,MAAMsC,mBAAmB,IAAMC,WAAWH,QAEzD,GAYXvE,KAAKmB,UAAUG,KAAO,SAASqD,YAAapD,GAAIC,GAAIoD,KAAMC,MAClC,MAAhBF,kBACKjE,QAAQY,KAAKC,GAAIC,IAClBH,KAAKX,QAAQZ,EAAIuB,KAAKjB,mBACjBM,QAAQZ,EAAIuB,KAAKjB,iBACjBF,GAAKmB,KAAKjB,aAEfiB,KAAKX,QAAQZ,EAAI8E,KAAOvD,KAAKjB,mBACxBM,QAAQZ,EAAI8E,KAAOvD,KAAKjB,iBACxBF,GAAK0E,KAAOvD,KAAKjB,aAEtBiB,KAAKX,QAAQX,EAAIsB,KAAKb,iBACjBE,QAAQX,EAAIsB,KAAKb,eACjBL,GAAKkB,KAAKb,WAEfa,KAAKX,QAAQX,EAAI8E,KAAOxD,KAAKb,iBACxBE,QAAQX,EAAI8E,KAAOxD,KAAKb,eACxBL,GAAK0E,KAAOxD,KAAKb,kBAGrBG,QAAQW,KAAKC,GAAIC,IAClBH,KAAKV,QAAQb,EAAIuB,KAAKjB,mBACjBO,QAAQb,EAAIuB,KAAKjB,iBACjBE,GAAKe,KAAKjB,aAEfiB,KAAKV,QAAQb,EAAI8E,KAAOvD,KAAKjB,mBACxBO,QAAQb,EAAI8E,KAAOvD,KAAKjB,iBACxBE,GAAKsE,KAAOvD,KAAKjB,aAEtBiB,KAAKV,QAAQZ,EAAIsB,KAAKb,iBACjBG,QAAQZ,EAAIsB,KAAKb,eACjBD,GAAKc,KAAKb,WAEfa,KAAKV,QAAQZ,EAAI8E,KAAOxD,KAAKb,iBACxBG,QAAQZ,EAAI8E,KAAOxD,KAAKb,eACxBD,GAAKsE,KAAOxD,KAAKb,aAelCR,KAAKmB,UAAU2D,UAAY,SAASvD,GAAIC,GAAIoD,KAAMC,KAAME,SAAUC,aACzDtE,QAAQY,KAAKC,GAAIC,SACjBb,QAAQW,KAAKC,GAAIC,IACL,iBAAbuD,eACKrE,QAAQZ,EAAI,QACZY,QAAQX,EAAI,GAAc,GAATiF,YACjB9E,GAAK,QACLC,GAAK,GAAc,GAAT6E,YACVrE,QAAQb,EAAI,SACZQ,GAAK,SACLK,QAAQZ,EAAI,GAAc,GAATiF,YACjBzE,GAAK,GAAc,GAATyE,SAEX3D,KAAKX,QAAQZ,EAAIuB,KAAKjB,mBACjBM,QAAQZ,EAAIuB,KAAKjB,iBACjBF,GAAKmB,KAAKjB,aAEfiB,KAAKX,QAAQZ,EAAI8E,KAAOvD,KAAKjB,mBACxBM,QAAQZ,EAAI8E,KAAOvD,KAAKjB,iBACxBF,GAAK0E,KAAOvD,KAAKjB,aAEtBiB,KAAKV,QAAQb,EAAIuB,KAAKjB,mBACjBO,QAAQb,EAAIuB,KAAKjB,iBACjBE,GAAKe,KAAKjB,aAEfiB,KAAKV,QAAQb,EAAI8E,KAAOvD,KAAKjB,mBACxBO,QAAQb,EAAI8E,KAAOvD,KAAKjB,iBACxBE,GAAKsE,KAAOvD,KAAKjB,aAEtBiB,KAAKX,QAAQX,EAAIsB,KAAKb,iBACjBE,QAAQX,EAAIsB,KAAKb,eACjBL,GAAKkB,KAAKb,WAEfa,KAAKX,QAAQX,EAAI8E,KAAOxD,KAAKb,iBACxBE,QAAQX,EAAI8E,KAAOxD,KAAKb,eACxBL,GAAK0E,KAAOxD,KAAKb,WAEtBa,KAAKV,QAAQZ,EAAIsB,KAAKb,iBACjBG,QAAQZ,EAAIsB,KAAKb,eACjBD,GAAKc,KAAKb,WAEfa,KAAKV,QAAQZ,EAAI8E,KAAOxD,KAAKb,iBACxBG,QAAQZ,EAAI8E,KAAOxD,KAAKb,eACxBD,GAAKsE,KAAOxD,KAAKb,aAalCR,KAAKmB,UAAU8D,cAAgB,SAASC,SAAUC,aAAcC,gBAAiBC,MAAOC,WAChFT,KAAO,EACPU,WAAaH,gBAAgBI,aAAa,oBAE1CnE,KAAKoE,YAAYP,SAAUG,MAAOC,QAGlCT,KAAOM,aAAarB,OAAOF,QAAQC,MACnCsB,aAAajE,YAAYkE,iBACzBA,gBAAgBI,aAAa,yBAGxB9E,QAAQX,EAAI8E,KAAQ,EAAIxD,KAAKjB,YAA6B,GAAbmF,gBAC7CpF,GAAK0E,KAAQ,EAAIxD,KAAKjB,YAA6B,GAAbmF,gBACtC5E,QAAQZ,EAAI8E,KAAQ,EAAIxD,KAAKb,UAA2B,GAAb+E,gBAC3ChF,GAAKsE,KAAQ,EAAIxD,KAAKb,UAA2B,GAAb+E,YAClClE,KAAKoE,YAAYN,aAAcE,MAAOC,SAE7CJ,SAAShE,YAAYkE,sBAIhB1E,QAAQZ,EAAI,QACZY,QAAQX,EAAIsB,KAAKjB,YAA4B,GAAbmF,gBAChCpF,GAAKkB,KAAKjB,YAA4B,GAAbmF,gBACzB5E,QAAQb,EAAI,SACZa,QAAQZ,EAAIsB,KAAKb,UAA0B,GAAb+E,gBAC9BhF,GAAKc,KAAKb,UAA0B,GAAb+E,aAWpCvF,KAAKmB,UAAUsE,YAAc,SAAS5E,IAAKwE,MAAOC,aACxCI,KAAO7E,IAAI8E,+BACVN,OAASK,KAAKE,MAAQP,OAASK,KAAKG,OAASP,OAASI,KAAKI,KAAOR,OAASI,KAAKK,QAY3F/F,KAAKmB,UAAU6E,KAAO,SAASrB,YAAapD,GAAIC,GAAIoD,KAAMC,UAClDoB,MAAQ,EACQ,MAAhBtB,kBACKvE,aAAemB,GACpB0E,MAAQ/D,KAAKgE,IAAI7E,KAAKX,QAAQZ,EAAGuB,KAAKX,QAAQX,EAAG6E,KAAOvD,KAAKX,QAAQZ,EAAG+E,KAAOxD,KAAKX,QAAQX,GACxFsB,KAAKjB,YAAc6F,aACd7F,YAAc6F,OAEnB5E,KAAKjB,aAAe6F,aACf7F,aAAe6F,cAGnBzF,WAAae,GAClB0E,MAAQ/D,KAAKgE,IAAI7E,KAAKV,QAAQb,EAAGuB,KAAKV,QAAQZ,EAAG6E,KAAOvD,KAAKV,QAAQb,EAAG+E,KAAOxD,KAAKV,QAAQZ,GACxFsB,KAAKb,UAAYyF,aACZzF,UAAYyF,OAEjB5E,KAAKb,WAAayF,aACbzF,WAAayF,SAU9BjG,KAAKmB,UAAUgF,mBAAqB,iBACzB,CACHC,YAAa,CAAC,IAAIvG,MAAMwB,KAAKX,QAAQZ,EAAGuB,KAAKX,QAAQX,GAAI,IAAIF,MAAMwB,KAAKV,QAAQb,EAAGuB,KAAKV,QAAQZ,IAChGsG,YAAa,CAAChF,KAAKX,QAAQe,OAAOJ,KAAKjB,YAAa,GAAIiB,KAAKV,QAAQc,OAAOJ,KAAKb,UAAW,MASpGR,KAAKmB,UAAUmF,eAAiB,gBACvBlG,YAAc8B,KAAKe,IAAI5B,KAAKjB,kBAC5BI,UAAY0B,KAAKe,IAAI5B,KAAKb,YAgE5B,CAQHX,MAAOA,MAgBPG,KAAMA,KASNY,iBAAkBA,iBAUlB2F,KAAM,SAASC,gBAAiBC,OAAQhG,cAEhC4D,iBAAmBmC,gBAAgB,GAAGzE,MAAM,KAC5CuC,eAAiBkC,gBAAgB,GAAGzE,MAAM,KAC1C2E,cAAgBrC,iBAAiB,GAAGtC,MAAM,KAC1C4E,YAAcrC,eAAe,GAAGvC,MAAM,YAEnC,IAAI/B,KAAKyG,OAAO,GAAIC,cAAc,GAAIA,cAAc,GAAIrC,iBAAiB,GAAIoC,OAAO,GACvFE,YAAY,GAAIA,YAAY,GAAIrC,eAAe,GAAI7D,WAU3DmG,WAAY,SAASnG,SAAUoG,aACpB,IAAI7G,KAAK6G,KAAK5G,WAAYkD,SAAS0D,KAAK3G,IAAKiD,SAAS0D,KAAK1G,IAAKgD,SAAS0D,KAAKzG,aACjF+C,SAAS0D,KAAKxG,UAAW8C,SAAS0D,KAAKvG,IAAK6C,SAAS0D,KAAKtG,IAAK4C,SAAS0D,KAAKrG,WAAYC"} \ No newline at end of file diff --git a/amd/build/question.min.js b/amd/build/question.min.js index c87651c..1cfc2f1 100644 --- a/amd/build/question.min.js +++ b/amd/build/question.min.js @@ -5,6 +5,6 @@ * @copyright 2024 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_drawlines/question",["jquery","core/dragdrop","qtype_drawlines/Line","core/key_codes","core_form/changechecker"],(function($,dragDrop,Lines,keys,FormChangeChecker){function DrawlinesQuestion(containerId,readOnly,visibleDropZones){window.console.log("containerId ---------------------------------------------"),window.console.log("containerId = "+containerId);var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.lineSVGs=[],this.lines=[],this.isPrinting=!1,this.questionAnswer={},readOnly&&this.getRoot().addClass("qtype_drawlines-readonly"),thisQ.allImagesLoaded=!1,thisQ.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),thisQ.waitForAllImagesToBeLoaded()}DrawlinesQuestion.prototype.drawDropzones=function(){if(this.visibleDropZones.length>0){var bgImage=this.bgImage();this.getRoot().find("div.dropzones").html('');for(var svg=this.getRoot().find("svg.dropzones"),nextColourIndex=0,dropZoneNo=0;dropZoneNo'+dropZone.markertext+"");var markerspan=this.getRoot().find("div.ddarea div.markertexts span.markertext"+dropZoneNo);if(markerspan.length){var handles=line.getHandlePositions(),positionLeft=handles.moveHandle.x-markerspan.outerWidth()/2-4,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 lineSVG=line.makeSvg(svg[0]);lineSVG.setAttribute("class","dropzone "+colourClass),this.lines[this.Lines.length]=line,this.lineSVGs[this.lineSVGs.length]=lineSVG}},DrawlinesQuestion.prototype.repositionDrags=function(){var root=this.getRoot(),thisQ=this;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),imageCoords=thisQ.getImageCoords(input);if(imageCoords.length){var drag=thisQ.getRoot().find(".draghomes span.marker.choice"+choiceNo).not(".dragplaceholder");drag.remove();for(var i=0;i{result[inputNode.id]=inputNode.value})),result},DrawlinesQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DrawlinesQuestion.prototype.getImageCoords=function(inputNode){var imageCoords=[],val=$(inputNode).val();if(""!==val)for(var coordsStrings=val.split(";"),i=0;i=bgPosition.left&&point.x=bgPosition.top&&point.y2&&void 0!==arguments[2]&&arguments[2];var dropArea=this.dropArea(),bgRatio=this.bgRatio();drag.removeClass("beingdragged").removeClass("unneeded");var dragXY=this.convertToBgImgXY(new Lines.Point(drag.data("pagex"),drag.data("pagey")));isScaling?(drag.data("originX",dragXY.x/bgRatio).data("originY",dragXY.y/bgRatio),drag.css("left",dragXY.x).css("top",dragXY.y)):(drag.data("originX",dragXY.x).data("originY",dragXY.y),drag.css("left",dragXY.x*bgRatio).css("top",dragXY.y*bgRatio)),initialLoad||drag.data("scaleRatio",bgRatio),dropArea.append(drag),this.handleElementScale(drag,"left top")},DrawlinesQuestion.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)+this.getDragNoClass(drag,!0)).length,displayedDragsInDragHomes=this.getRoot().find("div.draghomes .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).not(".dragplaceholder").length;if((this.isInfiniteDrag(drag)||!this.isInfiniteDrag(drag)&&displayedDragsInDropArea1;)dragsInHome.first().remove(),displayedDrags--},DrawlinesQuestion.prototype.getInput=function(drag){var choiceNo=this.getChoiceNoFromElement(drag);return this.getRoot().find("input.choices.choice"+choiceNo)},DrawlinesQuestion.prototype.bgRatio=function(){var bgImg=this.bgImage(),bgImgNaturalWidth=bgImg.get(0).naturalWidth;return bgImg.width()/bgImgNaturalWidth},DrawlinesQuestion.prototype.handleElementScale=function(element,type){var bgRatio=parseFloat(this.bgRatio());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})},DrawlinesQuestion.prototype.isInfiniteDrag=function(drag){return drag.hasClass("infinite")},DrawlinesQuestion.prototype.waitForAllImagesToBeLoaded=function(){this.allImagesLoaded||(null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId),this.getNotYetLoadedImages().length>0?this.imageLoadingTimeoutId=setTimeout((function(){this.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,this.repositionDrags(),this.drawDropzones()))},DrawlinesQuestion.prototype.getNotYetLoadedImages=function(){return this.getRoot().find(".drawlines img.dropbackground").not((function(i,imgNode){return this.imageIsLoaded(imgNode)}))},DrawlinesQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,markerEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,visibleDropZones){if(questionManager.questions[containerId]=new DrawlinesQuestion(containerId,readOnly,visibleDropZones),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.markerEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("drawlines")&&!questionContainer.classList.contains("qtype_drawlines-readonly")&&(questionManager.addEventHandlersToMarker($(questionContainer).find("div.draghomes .marker")),questionManager.addEventHandlersToMarker($(questionContainer).find("div.droparea .marker")))}},setupEventHandlers:function(){$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToMarker:function(element){element.on("mousedown touchstart",questionManager.handleDragStart).on("keydown keypress",questionManager.handleKeyPress).focusin((function(e){questionManager.handleKeyboardFocus(e,!0)})).focusout((function(e){questionManager.handleKeyboardFocus(e,!1)}))},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},handleKeyboardFocus:function(e,isNavigating){questionManager.isKeyboardNavigation=isNavigating},fixLayoutIfThingsMoved:function(){questionManager.isKeyboardNavigation||this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.drawlines").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); +define("qtype_drawlines/question",["jquery","core/dragdrop","qtype_drawlines/Line","core/key_codes","core_form/changechecker"],(function($,dragDrop,Lines,keys,FormChangeChecker){function DrawlinesQuestion(containerId,readOnly,visibleDropZones,questionLines){var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.questionLines=questionLines,this.lineSVGs=[],this.lines=[],this.isPrinting=!1,this.questionAnswer={},this.svgEl=null,readOnly&&this.getRoot().addClass("qtype_drawlines-readonly"),thisQ.allImagesLoaded=!1,thisQ.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),thisQ.waitForAllImagesToBeLoaded()}DrawlinesQuestion.prototype.updateCoordinates=function(){for(var line=0;line';var dragsSvg=document.getElementById("que-dlines-svg-drags");for(let line=0;line');this.drawSVGLines(this.questionLines)},DrawlinesQuestion.prototype.repositionDrags=function(){var root=this.getRoot(),thisQ=this;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),imageCoords=thisQ.getImageCoords(input);if(imageCoords.length){var drag=thisQ.getRoot().find(".draghomes span.marker.choice"+choiceNo).not(".dragplaceholder");drag.remove();for(var i=0;i{result[inputNode.id]=inputNode.value})),result},DrawlinesQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DrawlinesQuestion.prototype.getImageCoords=function(inputNode){var imageCoords=[],val=$(inputNode).val();if(""!==val)for(var coordsStrings=val.split(";"),i=0;i=bgPosition.left&&point.x=bgPosition.top&&point.y2&&void 0!==arguments[2]&&arguments[2];var dropArea=this.dropArea(),bgRatio=this.bgRatio();drag.removeClass("beingdragged").removeClass("unneeded");var dragXY=this.convertToBgImgXY(new Lines.Point(drag.data("pagex"),drag.data("pagey")));isScaling?(drag.data("originX",dragXY.x/bgRatio).data("originY",dragXY.y/bgRatio),drag.css("left",dragXY.x).css("top",dragXY.y)):(drag.data("originX",dragXY.x).data("originY",dragXY.y),drag.css("left",dragXY.x*bgRatio).css("top",dragXY.y*bgRatio)),initialLoad||drag.data("scaleRatio",bgRatio),dropArea.append(drag),this.handleElementScale(drag,"left top")},DrawlinesQuestion.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)+this.getDragNoClass(drag,!0)).length,displayedDragsInDragHomes=this.getRoot().find("div.draghomes .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).not(".dragplaceholder").length;if((this.isInfiniteDrag(drag)||!this.isInfiniteDrag(drag)&&displayedDragsInDropArea1;)dragsInHome.first().remove(),displayedDrags--},DrawlinesQuestion.prototype.getInput=function(drag){var choiceNo=this.getChoiceNoFromElement(drag);return this.getRoot().find("input.choices.choice"+choiceNo)},DrawlinesQuestion.prototype.handleElementScale=function(element,type){var bgRatio=parseFloat(this.bgRatio());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})},DrawlinesQuestion.prototype.isInfiniteDrag=function(drag){return drag.hasClass("infinite")},DrawlinesQuestion.prototype.waitForAllImagesToBeLoaded=function(){this.allImagesLoaded||(null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId),this.getNotYetLoadedImages().length>0?this.imageLoadingTimeoutId=setTimeout((function(){this.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,this.cloneDrags(),this.repositionDrags(),this.drawDropzone()))},DrawlinesQuestion.prototype.getNotYetLoadedImages=function(){return this.getRoot().find(".drawlines img.dropbackground").not((function(i,imgNode){return this.imageIsLoaded(imgNode)}))},DrawlinesQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,markerEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},noOfLines:null,dropZones:[],questionLines:[],init:function(containerId,readOnly,visibleDropZones,questionLines){if(questionManager.questions[containerId]=new DrawlinesQuestion(containerId,readOnly,visibleDropZones,questionLines),questionManager.questions[containerId].updateCoordinates(),!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.markerEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);if(questionContainer.classList.contains("drawlines")&&!questionContainer.classList.contains("qtype_drawlines-readonly")){var dropArea=document.querySelector(".droparea");dropArea.addEventListener("mousedown",questionManager.handleEventMove),dropArea.addEventListener("touchstart",questionManager.handleEventMove);var drags=document.querySelector(".draghomes");drags.addEventListener("mousedown",questionManager.handleEventDragMove),drags.addEventListener("touchstart",questionManager.handleEventDragMove)}}},handleEventMove:function(event){var dropzoneNo,handleIndex,question=questionManager.getQuestionForEvent(event);event.target.closest(".dropzone .startcircle.shape")?(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,handleIndex="0",question.handleMove(event,handleIndex,dropzoneNo)):event.target.closest(".dropzone .endcircle.shape")?(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,handleIndex="1",question.handleMove(event,handleIndex,dropzoneNo)):event.target.closest("polyline.shape")&&(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleDragMove(event,dropzoneNo))},handleEventDragMove:function(event){var dropzoneNo,question=questionManager.getQuestionForEvent(event);event.target.closest(".dropzone polyline.shape")&&(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleDragMove(event,dropzoneNo))},getSvg:function(){var svg=document.querySelector(".droparea svg");return null===svg?null:svg},getFormValue:function(name,indexes){return this.getEl(name,indexes).value},addEventHandlersToMarker:function(element){element.on("mousedown touchstart",questionManager.handleDragStart).on("keydown keypress",questionManager.handleKeyPress).focusin((function(e){questionManager.handleKeyboardFocus(e,!0)})).focusout((function(e){questionManager.handleKeyboardFocus(e,!1)}))},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},handleKeyboardFocus:function(e,isNavigating){questionManager.isKeyboardNavigation=isNavigating},fixLayoutIfThingsMoved:function(){questionManager.isKeyboardNavigation||this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.drawlines").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); //# sourceMappingURL=question.min.js.map \ No newline at end of file diff --git a/amd/build/question.min.js.map b/amd/build/question.min.js.map index 1d0eae8..e3526f0 100644 --- a/amd/build/question.min.js.map +++ b/amd/build/question.min.js.map @@ -1 +1 @@ -{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_drawlines/question\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'qtype_drawlines/Line',\n 'core/key_codes',\n 'core_form/changechecker'\n], function(\n $,\n dragDrop,\n Lines,\n keys,\n FormChangeChecker\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Object[]} visibleDropZones the geometry of any drop-zones to show.\n * Objects have fields line, coords and markertext.\n * @constructor\n */\n function DrawlinesQuestion(containerId, readOnly, visibleDropZones) {\n window.console.log('containerId ---------------------------------------------');\n window.console.log('containerId = ' + containerId);\n var thisQ = this;\n this.containerId = containerId;\n this.visibleDropZones = visibleDropZones;\n this.lineSVGs = [];\n this.lines = [];\n this.isPrinting = false;\n this.questionAnswer = {};\n if (readOnly) {\n this.getRoot().addClass('qtype_drawlines-readonly');\n }\n thisQ.allImagesLoaded = false;\n thisQ.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n thisQ.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Draws the svg lines of any drop zones that should be visible for feedback purposes.\n */\n DrawlinesQuestion.prototype.drawDropzones = function() {\n if (this.visibleDropZones.length > 0) {\n var bgImage = this.bgImage();\n\n this.getRoot().find('div.dropzones').html('');\n var svg = this.getRoot().find('svg.dropzones');\n\n var nextColourIndex = 0;\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var colourClass = 'color' + nextColourIndex;\n nextColourIndex = (nextColourIndex + 1) % 8;\n this.addDropzone(svg, dropZoneNo, colourClass);\n }\n }\n };\n\n /**\n * Adds a dropzone line with colour, coords and link provided to the array of Lines.\n *\n * @param {jQuery} svg the SVG image to which to add this drop zone.\n * @param {int} dropZoneNo which drop-zone to add.\n * @param {string} colourClass class name\n */\n DrawlinesQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {\n var dropZone = this.visibleDropZones[dropZoneNo],\n line = Lines.make(dropZone.line, ''),\n existingmarkertext,\n bgRatio = this.bgRatio();\n if (!line.parse(dropZone.coords, bgRatio)) {\n return;\n }\n\n existingmarkertext = this.getRoot().find('div.markertexts span.markertext' + dropZoneNo);\n if (existingmarkertext.length) {\n if (dropZone.markertext !== '') {\n existingmarkertext.html(dropZone.markertext);\n } else {\n existingmarkertext.remove();\n }\n } else if (dropZone.markertext !== '') {\n var classnames = 'markertext markertext' + dropZoneNo;\n this.getRoot().find('div.markertexts').append('' +\n dropZone.markertext + '');\n var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n if (markerspan.length) {\n var handles = line.getHandlePositions();\n var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;\n var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);\n markerspan\n .css('left', positionLeft)\n .css('top', positionTop);\n markerspan\n .data('originX', markerspan.position().left / bgRatio)\n .data('originY', markerspan.position().top / bgRatio);\n this.handleElementScale(markerspan, 'center');\n }\n }\n\n var lineSVG = line.makeSvg(svg[0]);\n lineSVG.setAttribute('class', 'dropzone ' + colourClass);\n\n this.lines[this.Lines.length] = line;\n this.lineSVGs[this.lineSVGs.length] = lineSVG;\n };\n\n /**\n * Draws the drag items on the page (and drop zones if required).\n * The idea is to re-draw all the drags and drops whenever there is a change\n * like a widow resize or an item dropped in place.\n */\n DrawlinesQuestion.prototype.repositionDrags = function() {\n var root = this.getRoot(),\n thisQ = this;\n\n root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {\n $(item).addClass('unneeded');\n });\n\n root.find('input.choices').each(function(key, input) {\n var choiceNo = thisQ.getChoiceNoFromElement(input),\n imageCoords = thisQ.getImageCoords(input);\n if (imageCoords.length) {\n var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');\n drag.remove();\n for (var i = 0; i < imageCoords.length; i++) {\n var dragInDrop = drag.clone();\n // Convert image coords to screen coords.\n const screenCoords = thisQ.convertToWindowXY(imageCoords[i]);\n dragInDrop.data('pagex', screenCoords.x).data('pagey', screenCoords.y);\n // Save image coords to the drag item so we can use it later.\n dragInDrop.data('imageCoords', imageCoords[i]);\n // We always save the coordinates in the 1:1 ratio.\n // So we need to set the scale ratio to 1 for the initial load.\n dragInDrop.data('scaleRatio', 1);\n thisQ.sendDragToDrop(dragInDrop, false, true);\n }\n thisQ.getDragClone(drag).addClass('active');\n thisQ.cloneDragIfNeeded(drag);\n }\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DrawlinesQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.choices').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DrawlinesQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Determine what drag items need to be shown and\n * return coords of all drag items except any that are currently being dragged\n * based on contents of hidden inputs and whether drags are 'infinite' or how many\n * drags should be shown.\n *\n * @param {jQuery} inputNode\n * @returns {Point[]} image coordinates of however many copies of the drag item should be shown.\n */\n DrawlinesQuestion.prototype.getImageCoords = function(inputNode) {\n var imageCoords = [],\n val = $(inputNode).val();\n if (val !== '') {\n var coordsStrings = val.split(';');\n for (var i = 0; i < coordsStrings.length; i++) {\n imageCoords[i] = Lines.Point.parse(coordsStrings[i]);\n }\n }\n return imageCoords;\n };\n\n /**\n * Converts the relative x and y position coordinates into\n * absolute x and y position coordinates.\n *\n * @param {Point} point relative to the background image.\n * @returns {Point} point relative to the page.\n */\n DrawlinesQuestion.prototype.convertToWindowXY = function(point) {\n var bgImage = this.bgImage();\n // The +1 seems rather odd, but seems to give the best results in\n // the three main browsers at a range of zoom levels.\n // (Its due to the 1px border around the image, that shifts the\n // image pixels by 1 down and to the left.)\n return point.offset(bgImage.offset().left + 1, bgImage.offset().top + 1);\n };\n\n /**\n * Utility function converting window coordinates to relative to the\n * background image coordinates.\n *\n * @param {Point} point relative to the page.\n * @returns {Point} point relative to the background image.\n */\n DrawlinesQuestion.prototype.convertToBgImgXY = function(point) {\n var bgImage = this.bgImage();\n return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);\n };\n\n /**\n * Is the point within the background image?\n *\n * @param {Point} point relative to the BG image.\n * @return {boolean} true it they are.\n */\n DrawlinesQuestion.prototype.coordsInBgImg = function(point) {\n var bgImage = this.bgImage();\n var bgPosition = bgImage.offset();\n\n return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()\n && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();\n };\n\n /**\n * Get the outer div for this question.\n * @returns {jQuery} containing that div.\n */\n DrawlinesQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DrawlinesQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n DrawlinesQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n dragged = $(e.target).closest('.marker');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragged.addClass('beingdragged').css('transform', '');\n\n var placed = !dragged.hasClass('unneeded');\n if (!placed) {\n var hiddenDrag = thisQ.getDragClone(dragged);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n dragged.offset(hiddenDrag.offset());\n }\n }\n\n dragDrop.start(e, dragged, function() {\n void (1);\n }, function(x, y, dragged) {\n thisQ.dragEnd(dragged);\n });\n };\n\n /**\n * Functionality at the end of a drag drop.\n * @param {jQuery} dragged the marker that was dragged.\n */\n DrawlinesQuestion.prototype.dragEnd = function(dragged) {\n var placed = false,\n choiceNo = this.getChoiceNoFromElement(dragged),\n bgRatio = this.bgRatio(),\n dragXY;\n\n dragged.data('pagex', dragged.offset().left).data('pagey', dragged.offset().top);\n dragXY = new Lines.Point(dragged.data('pagex'), dragged.data('pagey'));\n if (this.coordsInBgImg(dragXY)) {\n this.sendDragToDrop(dragged, true);\n placed = true;\n // Since we already move the drag item to new position.\n // Remove the image coords if this drag item have it.\n // We will get the new image coords for this drag item in saveCoordsForChoice.\n if (dragged.data('imageCoords')) {\n dragged.data('imageCoords', null);\n }\n // It seems that the dragdrop sometimes leaves the drag\n // one pixel out of position. Put it in exactly the right place.\n var bgImgXY = this.convertToBgImgXY(dragXY);\n bgImgXY = new Lines.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);\n }\n\n if (!placed) {\n this.sendDragHome(dragged);\n this.removeDragIfNeeded(dragged);\n } else {\n this.cloneDragIfNeeded(dragged);\n }\n\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Save the coordinates for a dropped item in the form field.\n * @param {Number} choiceNo which copy of the choice this was.\n */\n DrawlinesQuestion.prototype.saveCoordsForChoice = function(choiceNo) {\n let imageCoords = [];\n var items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),\n thiQ = this,\n bgRatio = this.bgRatio();\n\n if (items.length) {\n items.each(function() {\n var drag = $(this);\n if (!drag.hasClass('beingdragged') && !drag.data('imageCoords')) {\n if (drag.data('scaleRatio') !== bgRatio) {\n // The scale ratio for the draggable item was changed. We need to update that.\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n }\n var dragXY = new Lines.Point(drag.data('pagex'), drag.data('pagey'));\n if (thiQ.coordsInBgImg(dragXY)) {\n var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n bgImgXY = new Lines.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n imageCoords[imageCoords.length] = bgImgXY;\n }\n } else if (drag.data('imageCoords')) {\n imageCoords[imageCoords.length] = drag.data('imageCoords');\n }\n });\n }\n\n this.getRoot().find('input.choice' + choiceNo).val(imageCoords.join(';'));\n if (this.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n this.questionAnswer = this.getQuestionAnsweredValues();\n }\n };\n\n /**\n * Handle key down / press events on markers.\n * @param {KeyboardEvent} e\n */\n DrawlinesQuestion.prototype.handleKeyPress = function(e) {\n var drag = $(e.target).closest('.marker'),\n point = new Lines.Point(drag.offset().left, drag.offset().top),\n choiceNo = this.getChoiceNoFromElement(drag);\n\n switch (e.keyCode) {\n case keys.arrowLeft:\n case 65: // A.\n point.x -= 1;\n break;\n case keys.arrowRight:\n case 68: // D.\n point.x += 1;\n break;\n case keys.arrowDown:\n case 83: // S.\n point.y += 1;\n break;\n case keys.arrowUp:\n case 87: // W.\n point.y -= 1;\n break;\n case keys.space:\n case keys.escape:\n point = null;\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n if (point !== null) {\n point = this.constrainToBgImg(point);\n drag.offset({'left': point.x, 'top': point.y});\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n var dragXY = this.convertToBgImgXY(new Lines.Point(drag.data('pagex'), drag.data('pagey')));\n drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());\n if (this.coordsInBgImg(new Lines.Point(drag.offset().left, drag.offset().top))) {\n if (drag.hasClass('unneeded')) {\n this.sendDragToDrop(drag, true);\n var hiddenDrag = this.getDragClone(drag);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n }\n this.cloneDragIfNeeded(drag);\n }\n }\n } else {\n drag.css('left', '').css('top', '');\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n this.sendDragHome(drag);\n this.removeDragIfNeeded(drag);\n }\n drag.focus();\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Makes sure the dragged item always exists within the background image area.\n *\n * @param {Point} windowxy\n * @returns {Point} coordinates\n */\n DrawlinesQuestion.prototype.constrainToBgImg = function(windowxy) {\n var bgImg = this.bgImage(),\n bgImgXY = this.convertToBgImgXY(windowxy);\n bgImgXY.x = Math.max(0, bgImgXY.x);\n bgImgXY.y = Math.max(0, bgImgXY.y);\n bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);\n bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);\n return this.convertToWindowXY(bgImgXY);\n };\n\n /**\n * Returns the choice number for a node.\n *\n * @param {Element|jQuery} node\n * @returns {Number}\n */\n DrawlinesQuestion.prototype.getChoiceNoFromElement = function(node) {\n return Number(this.getClassnameNumericSuffix(node, 'choice'));\n };\n\n /**\n * Returns the numeric part of a class with the given prefix.\n *\n * @param {Element|jQuery} node\n * @param {String} prefix\n * @returns {Number|null}\n */\n DrawlinesQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = $(node).attr('class');\n if (classes !== undefined && classes !== '') {\n var classesarr = classes.split(' ');\n for (var index = 0; index < classesarr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesarr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesarr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Handle when the window is resized.\n */\n DrawlinesQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('div.droparea .marker').not('.beingdragged').each(function(key, drag) {\n $(drag)\n .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio))\n .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(drag, 'left top');\n });\n\n this.getRoot().find('div.droparea svg.dropzones')\n .width(this.bgImage().width())\n .height(this.bgImage().height());\n\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var dropZone = thisQ.visibleDropZones[dropZoneNo];\n var originCoords = dropZone.coords;\n var line = thisQ.lines[dropZoneNo];\n var lineSVG = thisQ.lineSVGs[dropZoneNo];\n line.parse(originCoords, bgRatio);\n line.updateSvg(lineSVG);\n\n var handles = line.getHandlePositions();\n var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n markerSpan\n .css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)\n .css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));\n thisQ.handleElementScale(markerSpan, 'center');\n }\n };\n\n // /**\n // * Clone the drag.\n // */\n // DrawlinesQuestion.prototype.cloneDrags = function() {\n // var thisQ = this;\n // this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {\n // var drag = $(draghome);\n // var placeHolder = drag.clone();\n // placeHolder.removeClass();\n // placeHolder.addClass('marker');\n // placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));\n // placeHolder.addClass(thisQ.getDragNoClass(drag, false));\n // placeHolder.addClass('dragplaceholder');\n // drag.before(placeHolder);\n // });\n // };\n\n /**\n * Get the drag number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the drag number.\n */\n DrawlinesQuestion.prototype.getDragNo = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'dragno');\n };\n\n /**\n * Get the drag number prefix of a drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} includeSelector include the CSS selector prefix or not.\n * @return {String} Class name\n */\n DrawlinesQuestion.prototype.getDragNoClass = function(drag, includeSelector) {\n var className = 'dragno' + this.getDragNo(drag);\n if (this.isInfiniteDrag(drag)) {\n className = 'infinite';\n }\n\n if (includeSelector) {\n return '.' + className;\n }\n\n return className;\n };\n\n // /**\n // * Get drag clone for a given drag.\n // *\n // * @param {jQuery} drag the drag.\n // * @returns {jQuery} the drag's clone.\n // */\n // DrawlinesQuestion.prototype.getDragClone = function(drag) {\n // return this.getRoot().find('.draghomes' + ' span.marker' +\n // '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');\n // };\n\n /**\n * Get the drop area element.\n * @returns {jQuery} droparea element.\n */\n DrawlinesQuestion.prototype.dropArea = function() {\n return this.getRoot().find('div.droparea');\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DrawlinesQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('beingdragged')\n .addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n var placeHolder = this.getDragClone(drag);\n placeHolder.after(drag);\n placeHolder.removeClass('active');\n };\n\n /**\n * Animate a drag item into a given place.\n *\n * @param {jQuery} drag the item to place.\n * @param {boolean} isScaling Scaling or not.\n * @param {boolean} initialLoad Whether it is the initial load or not.\n */\n DrawlinesQuestion.prototype.sendDragToDrop = function(drag, isScaling, initialLoad = false) {\n var dropArea = this.dropArea(),\n bgRatio = this.bgRatio();\n drag.removeClass('beingdragged').removeClass('unneeded');\n var dragXY = this.convertToBgImgXY(new Lines.Point(drag.data('pagex'), drag.data('pagey')));\n if (isScaling) {\n drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);\n drag.css('left', dragXY.x).css('top', dragXY.y);\n } else {\n drag.data('originX', dragXY.x).data('originY', dragXY.y);\n drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);\n }\n // We need to save the original scale ratio for each draggable item.\n if (!initialLoad) {\n // Only set the scale ratio for a current being-dragged item, not for the initial loading.\n drag.data('scaleRatio', bgRatio);\n }\n dropArea.append(drag);\n this.handleElementScale(drag, 'left top');\n };\n\n /**\n * Clone the drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DrawlinesQuestion.prototype.cloneDragIfNeeded = function(drag) {\n var inputNode = this.getInput(drag),\n noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),\n displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,\n displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;\n\n if ((this.isInfiniteDrag(drag) ||\n !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {\n var dragClone = drag.clone();\n dragClone.addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n this.getDragClone(drag)\n .removeClass('active')\n .after(dragClone);\n questionManager.addEventHandlersToMarker(dragClone);\n }\n };\n\n /**\n * Remove the clone drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DrawlinesQuestion.prototype.removeDragIfNeeded = function(drag) {\n var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');\n var displayedDrags = dragsInHome.length;\n while (displayedDrags > 1) {\n dragsInHome.first().remove();\n displayedDrags--;\n }\n };\n\n /**\n * Get the input belong to drag.\n *\n * @param {jQuery} drag the item to place.\n * @returns {jQuery} input element.\n */\n DrawlinesQuestion.prototype.getInput = function(drag) {\n var choiceNo = this.getChoiceNoFromElement(drag);\n return this.getRoot().find('input.choices.choice' + choiceNo);\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DrawlinesQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DrawlinesQuestion.prototype.handleElementScale = function(element, type) {\n var bgRatio = parseFloat(this.bgRatio());\n if (this.isPrinting) {\n bgRatio = 1;\n }\n $(element).css({\n '-webkit-transform': 'scale(' + bgRatio + ')',\n '-moz-transform': 'scale(' + bgRatio + ')',\n '-ms-transform': 'scale(' + bgRatio + ')',\n '-o-transform': 'scale(' + bgRatio + ')',\n 'transform': 'scale(' + bgRatio + ')',\n 'transform-origin': type\n });\n };\n\n /**\n * Check if the given drag is in infinite mode or not.\n *\n * @param {jQuery} drag The drag item need to check.\n */\n DrawlinesQuestion.prototype.isInfiniteDrag = function(drag) {\n return drag.hasClass('infinite');\n };\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DrawlinesQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n this.waitForAllImagesToBeLoaded();\n }, 100);\n return;\n }\n\n // We now have all images. Carry on, but only after giving the layout a chance to settle down.\n this.allImagesLoaded = true;\n //this.cloneDrags();\n this.repositionDrags();\n this.drawDropzones();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DrawlinesQuestion.prototype.getNotYetLoadedImages = function() {\n return this.getRoot().find('.drawlines img.dropbackground').not(function(i, imgNode) {\n return this.imageIsLoaded(imgNode);\n });\n };\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n DrawlinesQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\n * @type {Object}\n */\n var questionManager = {\n\n /**\n * {boolean} ensures that the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} ensures that the marker event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n markerEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @param {String} containerId the id of the div.que that contains this question.\n * @param {boolean} readOnly whether the question is read-only.\n * @param {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.\n */\n init: function(containerId, readOnly, visibleDropZones) {\n questionManager.questions[containerId] =\n new DrawlinesQuestion(containerId, readOnly, visibleDropZones);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.markerEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('drawlines') &&\n !questionContainer.classList.contains('qtype_drawlines-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker'));\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $(window).on('resize', function() {\n questionManager.handleWindowResize(false);\n });\n window.addEventListener('beforeprint', function() {\n questionManager.isPrinting = true;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n window.addEventListener('afterprint', function() {\n questionManager.isPrinting = false;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved();\n }, 100);\n },\n\n /**\n * Binding the event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToMarker: function(element) {\n element\n .on('mousedown touchstart', questionManager.handleDragStart)\n .on('keydown keypress', questionManager.handleKeyPress)\n .focusin(function(e) {\n questionManager.handleKeyboardFocus(e, true);\n })\n .focusout(function(e) {\n questionManager.handleKeyboardFocus(e, false);\n });\n },\n\n /**\n * Handle mouse down / touch start events on markers.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on markers.\n * @param {Event} e\n */\n handleKeyPress: function(e) {\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * Handle focus lost events on markers.\n * @param {Event} e\n * @param {boolean} isNavigating\n */\n handleKeyboardFocus: function(e, isNavigating) {\n questionManager.isKeyboardNavigation = isNavigating;\n },\n\n /**\n * Sometimes, despite our best efforts, things change in a way that cannot\n * be specifically caught (e.g. dock expanding or collapsing in Boost).\n * Therefore, we need to periodically check everything is in the right position.\n */\n fixLayoutIfThingsMoved: function() {\n if (!questionManager.isKeyboardNavigation) {\n this.handleWindowResize(questionManager.isPrinting);\n }\n // We use setTimeout after finishing work, rather than setInterval,\n // in case positioning things is slow. We want 100 ms gap\n // between executions, not what setInterval does.\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);\n }, 100);\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DrawlinesQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.drawlines').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_drawlines/question\n */\n return {\n /**\n * Initialise one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {String} bgImgUrl the URL of the background image.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {String[]} visibleDropZones the geometry of any drop-zones to show.\n */\n init: questionManager.init\n };\n});"],"names":["define","$","dragDrop","Lines","keys","FormChangeChecker","DrawlinesQuestion","containerId","readOnly","visibleDropZones","window","console","log","thisQ","this","lineSVGs","lines","isPrinting","questionAnswer","getRoot","addClass","allImagesLoaded","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","drawDropzones","length","bgImage","find","html","outerWidth","outerHeight","svg","nextColourIndex","dropZoneNo","colourClass","addDropzone","existingmarkertext","dropZone","line","make","bgRatio","parse","coords","markertext","remove","classnames","append","markerspan","handles","getHandlePositions","positionLeft","moveHandle","x","positionTop","y","css","data","position","left","top","handleElementScale","lineSVG","makeSvg","setAttribute","repositionDrags","root","not","each","key","item","input","choiceNo","getChoiceNoFromElement","imageCoords","getImageCoords","drag","i","dragInDrop","clone","screenCoords","convertToWindowXY","sendDragToDrop","getDragClone","cloneDragIfNeeded","getQuestionAnsweredValues","result","inputNode","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","val","coordsStrings","split","Point","point","offset","convertToBgImgXY","coordsInBgImg","bgPosition","width","height","document","getElementById","handleDragStart","e","dragged","target","closest","prepare","start","hasClass","hiddenDrag","dragEnd","dragXY","placed","bgImgXY","sendDragHome","removeDragIfNeeded","saveCoordsForChoice","items","thiQ","join","questionManager","handleFormDirty","handleKeyPress","keyCode","arrowLeft","arrowRight","arrowDown","arrowUp","space","escape","preventDefault","constrainToBgImg","focus","windowxy","bgImg","Math","max","min","node","Number","getClassnameNumericSuffix","prefix","classes","attr","undefined","classesarr","index","RegExp","test","match","exec","handleResize","parseFloat","originCoords","updateSvg","markerSpan","getDragNo","getDragNoClass","includeSelector","className","isInfiniteDrag","dropArea","removeClass","placeHolder","after","isScaling","initialLoad","getInput","noOfDrags","displayedDragsInDropArea","displayedDragsInDragHomes","dragClone","addEventHandlersToMarker","dragsInHome","displayedDrags","first","bgImgNaturalWidth","get","naturalWidth","element","type","imageLoadingTimeoutId","clearTimeout","setTimeout","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","eventHandlersInitialised","markerEventHandlersInitialised","isKeyboardNavigation","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","classList","contains","on","handleWindowResize","addEventListener","fixLayoutIfThingsMoved","focusin","handleKeyboardFocus","focusout","question","getQuestionForEvent","isNavigating","currentTarget","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_drawlines/question\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'qtype_drawlines/Line',\n 'core/key_codes',\n 'core_form/changechecker',\n], function(\n $,\n dragDrop,\n Lines,\n keys,\n FormChangeChecker,\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Object[]} visibleDropZones the geometry of any drop-zones to show.\n * Objects have fields line, coords and markertext.\n * @param {line[]} questionLines\n * @constructor\n */\n function DrawlinesQuestion(containerId, readOnly, visibleDropZones, questionLines) {\n var thisQ = this;\n this.containerId = containerId;\n this.visibleDropZones = visibleDropZones;\n this.questionLines = questionLines;\n this.lineSVGs = [];\n this.lines = [];\n this.isPrinting = false;\n this.questionAnswer = {};\n this.svgEl = null;\n if (readOnly) {\n this.getRoot().addClass('qtype_drawlines-readonly');\n }\n thisQ.allImagesLoaded = false;\n thisQ.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n thisQ.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Update the coordinates from a particular string.\n */\n DrawlinesQuestion.prototype.updateCoordinates = function() {\n // We don't need to scale the shape for editing form.\n for (var line = 0; line < this.lineSVGs.length; line++) {\n var coordinates = this.getCoordinates(this.lineSVGs[line]);\n if (!this.lines[line].parse(coordinates[0], coordinates[1], 1)) {\n // Invalid coordinates. Don't update the preview.\n return;\n }\n this.updateSvgEl(line);\n }\n };\n\n /**\n * Draws the svg lines of any drop zones.\n * @param {Object[]} questionLines\n */\n DrawlinesQuestion.prototype.drawSVGLines = function(questionLines) {\n var bgImage = document.querySelector('img.dropbackground');\n\n var drags = document.querySelector('.draghomes');\n drags.innerHTML =\n '';\n\n var dragsSvg = document.getElementById('que-dlines-svg-drags');\n var initialHeight = 25;\n for (let line = 0; line < questionLines.length; line++) {\n var height = initialHeight + line * 50;\n var startcoordinates = '50,' + height + ';10';\n var endcoordinates = '200,' + height + ';10';\n this.lines[line] = Lines.make([startcoordinates, endcoordinates],\n [questionLines[line].labelstart, questionLines[line].labelend], questionLines[line].type);\n this.addToSvg(line, dragsSvg);\n }\n };\n\n /**\n * Draws the svg lines of any drop zones that should be visible for feedback purposes.\n */\n DrawlinesQuestion.prototype.drawDropzone = function() {\n var bgImage = document.querySelector('img.dropbackground');\n var svg = document.querySelector('svg.dropzones');\n document.getElementById('que-dlines-dropzone').style.position = 'relative';\n document.getElementById('que-dlines-dropzone').style.top = (bgImage.height + 1) * -1 + \"px\";\n document.getElementById('que-dlines-dropzone').style.height = bgImage.height + \"px\";\n document.getElementById('que-dlines-droparea').style.height = bgImage.height + \"px\";\n if (!svg) {\n var dropZone = document.querySelector('#que-dlines-dropzone');\n dropZone.innerHTML =\n '';\n }\n this.drawSVGLines(this.questionLines);\n };\n\n //\n // /**\n // * Adds a dropzone line with colour, coords and link provided to the array of Lines.\n // *\n // * @param {jQuery} svg the SVG image to which to add this drop zone.\n // * @param {int} dropZoneNo which drop-zone to add.\n // * @param {string} colourClass class name\n // */\n // DrawlinesQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {\n // var dropZone = this.visibleDropZones[dropZoneNo],\n // line = Lines.make(dropZone.line, ''),\n // existingmarkertext,\n // bgRatio = this.bgRatio();\n // if (!line.parse(dropZone.coords, bgRatio)) {\n // return;\n // }\n //\n // existingmarkertext = this.getRoot().find('div.markertexts span.markerlabelstart' + dropZoneNo);\n // if (existingmarkertext.length) {\n // if (dropZone.markertext !== '') {\n // existingmarkertext.html(dropZone.markertext);\n // } else {\n // existingmarkertext.remove();\n // }\n // } else if (dropZone.markertext !== '') {\n // var classnames = 'markertext markertext' + dropZoneNo;\n // this.getRoot().find('div.markertexts').append('' +\n // dropZone.markertext + '');\n // var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n // if (markerspan.length) {\n // var handles = line.getHandlePositions();\n // var positionLeft = handles.moveHandles.x - (markerspan.outerWidth() / 2) - 4;\n // var positionTop = handles.moveHandles.y - (markerspan.outerHeight() / 2);\n // markerspan\n // .css('left', positionLeft)\n // .css('top', positionTop);\n // markerspan\n // .data('originX', markerspan.position().left / bgRatio)\n // .data('originY', markerspan.position().top / bgRatio);\n // this.handleElementScale(markerspan, 'center');\n // }\n // }\n //\n // var lineSVG = line.makeSvg(svg[0]);\n // lineSVG.setAttribute('class', 'dropzone ' + colourClass);\n //\n // this.lines[this.Lines.length] = line;\n // this.lineSVGs[this.lineSVGs.length] = lineSVG;\n // };\n\n /**\n * Draws the drag items on the page (and drop zones if required).\n * The idea is to re-draw all the drags and drops whenever there is a change\n * like a widow resize or an item dropped in place.\n */\n DrawlinesQuestion.prototype.repositionDrags = function() {\n var root = this.getRoot(),\n thisQ = this;\n\n root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {\n $(item).addClass('unneeded');\n });\n\n root.find('input.choices').each(function(key, input) {\n var choiceNo = thisQ.getChoiceNoFromElement(input),\n imageCoords = thisQ.getImageCoords(input);\n\n if (imageCoords.length) {\n var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');\n drag.remove();\n for (var i = 0; i < imageCoords.length; i++) {\n var dragInDrop = drag.clone();\n // Convert image coords to screen coords.\n const screenCoords = thisQ.convertToWindowXY(imageCoords[i]);\n dragInDrop.data('pagex', screenCoords.x).data('pagey', screenCoords.y);\n // Save image coords to the drag item so we can use it later.\n dragInDrop.data('imageCoords', imageCoords[i]);\n // We always save the coordinates in the 1:1 ratio.\n // So we need to set the scale ratio to 1 for the initial load.\n dragInDrop.data('scaleRatio', 1);\n thisQ.sendDragToDrop(dragInDrop, false, true);\n }\n thisQ.getDragClone(drag).addClass('active');\n thisQ.cloneDragIfNeeded(drag);\n }\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DrawlinesQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.choices').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DrawlinesQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Determine what drag items need to be shown and\n * return coords of all drag items except any that are currently being dragged\n * based on contents of hidden inputs and whether drags are 'infinite' or how many\n * drags should be shown.\n *\n * @param {jQuery} inputNode\n * @returns {Point[]} image coordinates of however many copies of the drag item should be shown.\n */\n DrawlinesQuestion.prototype.getImageCoords = function(inputNode) {\n var imageCoords = [],\n val = $(inputNode).val();\n if (val !== '') {\n var coordsStrings = val.split(';');\n for (var i = 0; i < coordsStrings.length; i++) {\n imageCoords[i] = Lines.Point.parse(coordsStrings[i]);\n }\n }\n return imageCoords;\n };\n\n /**\n * Converts the relative x and y position coordinates into\n * absolute x and y position coordinates.\n *\n * @param {Point} point relative to the background image.\n * @returns {Point} point relative to the page.\n */\n DrawlinesQuestion.prototype.convertToWindowXY = function(point) {\n var bgImage = this.bgImage();\n // The +1 seems rather odd, but seems to give the best results in\n // the three main browsers at a range of zoom levels.\n // (Its due to the 1px border around the image, that shifts the\n // image pixels by 1 down and to the left.)\n return point.offset(bgImage.offset().left + 1, bgImage.offset().top + 1);\n };\n\n /**\n * Utility function converting window coordinates to relative to the\n * background image coordinates.\n *\n * @param {Point} point relative to the page.\n * @returns {Point} point relative to the background image.\n */\n DrawlinesQuestion.prototype.convertToBgImgXY = function(point) {\n var bgImage = this.bgImage();\n return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);\n };\n\n /**\n * Is the point within the background image?\n *\n * @param {Point} point relative to the BG image.\n * @return {boolean} true it they are.\n */\n DrawlinesQuestion.prototype.coordsInBgImg = function(point) {\n var bgImage = this.bgImage();\n var bgPosition = bgImage.offset();\n\n return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()\n && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();\n };\n\n /**\n * Get the outer div for this question.\n * @returns {jQuery} containing that div.\n */\n DrawlinesQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DrawlinesQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n DrawlinesQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n dragged = $(e.target).closest('.marker');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n dragged.addClass('beingdragged').css('transform', '');\n\n var placed = !dragged.hasClass('unneeded');\n if (!placed) {\n var hiddenDrag = thisQ.getDragClone(dragged);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n dragged.offset(hiddenDrag.offset());\n }\n }\n\n dragDrop.start(e, dragged, function() {\n void (1);\n }, function(x, y, dragged) {\n thisQ.dragEnd(dragged);\n });\n };\n\n /**\n * Functionality at the end of a drag drop.\n * @param {jQuery} dragged the marker that was dragged.\n */\n DrawlinesQuestion.prototype.dragEnd = function(dragged) {\n var placed = false,\n choiceNo = this.getChoiceNoFromElement(dragged),\n bgRatio = this.bgRatio(),\n dragXY;\n\n dragged.data('pagex', dragged.offset().left).data('pagey', dragged.offset().top);\n dragXY = new Lines.Point(dragged.data('pagex'), dragged.data('pagey'));\n if (this.coordsInBgImg(dragXY)) {\n this.sendDragToDrop(dragged, true);\n placed = true;\n // Since we already move the drag item to new position.\n // Remove the image coords if this drag item have it.\n // We will get the new image coords for this drag item in saveCoordsForChoice.\n if (dragged.data('imageCoords')) {\n dragged.data('imageCoords', null);\n }\n // It seems that the dragdrop sometimes leaves the drag\n // one pixel out of position. Put it in exactly the right place.\n var bgImgXY = this.convertToBgImgXY(dragXY);\n bgImgXY = new Lines.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);\n }\n\n if (!placed) {\n this.sendDragHome(dragged);\n this.removeDragIfNeeded(dragged);\n } else {\n this.cloneDragIfNeeded(dragged);\n }\n\n this.saveCoordsForChoice(choiceNo);\n };\n\n\n /**\n * Returns the coordinates for the line from the text input in the form.\n * @param {SVGElement} svgEl\n * @returns {Array} the coordinates.\n */\n DrawlinesQuestion.prototype.getCoordinates = function(svgEl) {\n\n var circleStartXCoords = svgEl.childNodes[1].getAttribute('cx');\n var circleStartYCoords = svgEl.childNodes[1].getAttribute('cy');\n var circleStartRCoords = svgEl.childNodes[1].getAttribute('r');\n var circleEndXCoords = svgEl.childNodes[2].getAttribute('cx');\n var circleEndYCoords = svgEl.childNodes[2].getAttribute('cy');\n var circleEndRCoords = svgEl.childNodes[2].getAttribute('r');\n return [circleStartXCoords + ',' + circleStartYCoords + ';' + circleStartRCoords,\n circleEndXCoords + ',' + circleEndYCoords + ';' + circleEndRCoords];\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DrawlinesQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n\n /**\n * Add this line to an SVG graphic.\n *\n * @param {int} lineNumber Line Number\n * @param {SVGElement} svg the SVG image to which to add this drop zone.\n */\n DrawlinesQuestion.prototype.addToSvg = function(lineNumber, svg) {\n this.lineSVGs[lineNumber] = this.lines[lineNumber].makeSvg(svg);\n if (!this.lineSVGs[lineNumber]) {\n return;\n }\n this.lineSVGs[lineNumber].setAttribute('class', 'dropzone');\n this.lineSVGs[lineNumber].setAttribute('data-dropzone-no', lineNumber);\n };\n\n /**\n * Update the shape of this drop zone (but not type) in an SVG image.\n * @param {int} dropzoneNo\n */\n DrawlinesQuestion.prototype.updateSvgEl = function(dropzoneNo) {\n this.lines[dropzoneNo].updateSvg(this.lineSVGs[dropzoneNo]);\n };\n\n /**\n * Start responding to dragging the move handle.\n * @param {Event} e Event object\n * @param {String} handleIndex\n * @param {int} dropzoneNo\n */\n DrawlinesQuestion.prototype.handleMove = function(e, handleIndex, dropzoneNo) {\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n var movingDropZone = this,\n lastX = info.x,\n lastY = info.y,\n dragProxy = this.makeDragProxy(info.x, info.y),\n svg = document.querySelector('svg.dropzones'),\n maxX = svg.width.baseVal.value,\n maxY = svg.height.baseVal.value;\n\n dragDrop.start(e, $(dragProxy), function(pageX, pageY) {\n movingDropZone.lines[dropzoneNo].move(handleIndex,\n parseInt(pageX) - parseInt(lastX), parseInt(pageY) - parseInt(lastY), parseInt(maxX), parseInt(maxY));\n lastX = pageX;\n lastY = pageY;\n movingDropZone.updateSvgEl(dropzoneNo);\n }, function() {\n document.body.removeChild(dragProxy);\n });\n };\n\n\n /**\n * Start responding to dragging the move handle.\n * @param {Event} e Event object\n * @param {int} dropzoneNo\n */\n DrawlinesQuestion.prototype.handleDragMove = function(e, dropzoneNo) {\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n var movingDrag = this,\n lastX = info.x,\n lastY = info.y,\n dragProxy = this.makeDragProxy(info.x, info.y),\n svgDrags = document.querySelector('svg.drags'),\n svgDropZones = document.querySelector('svg.dropzones'),\n maxX = svgDrags.width.baseVal.value,\n maxY = svgDrags.height.baseVal.value,\n whichSVG = \"\";\n\n var selectedElement = this.lineSVGs[dropzoneNo];\n const dropX = e.clientX;\n const dropY = e.clientY;\n\n dragDrop.start(e, $(dragProxy), function(pageX, pageY) {\n var svgDragsbBox = svgDrags.getBBox();\n var svgDropZonesbBox = svgDropZones.getBBox();\n\n // Check if the drags need to be moved from one svg to another.\n // If true, the drag is moved from draghomes SVG to dropZone SVG.\n if (movingDrag.lines[dropzoneNo].centre1.y === 10 && svgDragsbBox.y === 0) {\n movingDrag.lines[dropzoneNo].addToDropZone(svgDrags, svgDropZones, selectedElement,\n dropX, dropY);\n maxX = svgDropZones.width.baseVal.value;\n maxY = svgDropZones.height.baseVal.value;\n whichSVG = \"svgDropZones\";\n } else if (movingDrag.lines[dropzoneNo].centre1.y === svgDropZonesbBox.y + 10) {\n // Move drags from dropZone SVG to draghomes SVG.\n movingDrag.lines[dropzoneNo].addToDropZone(svgDrags, svgDropZones, selectedElement,\n dropX, dropY);\n whichSVG = \"svgDrags\";\n }\n\n // Drag the lines within the SVG\n movingDrag.lines[dropzoneNo].moveDrags(\n parseInt(pageX) - parseInt(lastX), parseInt(pageY) - parseInt(lastY),\n parseInt(maxX), parseInt(maxY), whichSVG, dropzoneNo);\n lastX = pageX;\n lastY = pageY;\n\n movingDrag.updateSvgEl(dropzoneNo);\n }, function() {\n document.body.removeChild(dragProxy);\n });\n };\n\n\n /**\n * Make an invisible drag proxy.\n *\n * @param {int} x x position .\n * @param {int} y y position.\n * @returns {HTMLElement} the drag proxy.\n */\n DrawlinesQuestion.prototype.makeDragProxy = function(x, y) {\n var dragProxy = document.createElement('div');\n dragProxy.style.position = 'absolute';\n dragProxy.style.top = y + 'px';\n dragProxy.style.left = x + 'px';\n dragProxy.style.width = '1px';\n dragProxy.style.height = '1px';\n document.body.appendChild(dragProxy);\n return dragProxy;\n };\n\n /**\n * Save the coordinates for a dropped item in the form field.\n * @param {Number} choiceNo which copy of the choice this was.\n */\n DrawlinesQuestion.prototype.saveCoordsForChoice = function(choiceNo) {\n let imageCoords = [];\n var items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),\n thiQ = this,\n bgRatio = this.bgRatio();\n\n if (items.length) {\n items.each(function() {\n var drag = $(this);\n if (!drag.hasClass('beingdragged') && !drag.data('imageCoords')) {\n if (drag.data('scaleRatio') !== bgRatio) {\n // The scale ratio for the draggable item was changed. We need to update that.\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n }\n var dragXY = new Lines.Point(drag.data('pagex'), drag.data('pagey'));\n if (thiQ.coordsInBgImg(dragXY)) {\n var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n bgImgXY = new Lines.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n imageCoords[imageCoords.length] = bgImgXY;\n }\n } else if (drag.data('imageCoords')) {\n imageCoords[imageCoords.length] = drag.data('imageCoords');\n }\n });\n }\n\n this.getRoot().find('input.choice' + choiceNo).val(imageCoords.join(';'));\n if (this.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n this.questionAnswer = this.getQuestionAnsweredValues();\n }\n };\n\n /**\n * Handle key down / press events on markers.\n * @param {KeyboardEvent} e\n */\n DrawlinesQuestion.prototype.handleKeyPress = function(e) {\n var drag = $(e.target).closest('.marker'),\n point = new Lines.Point(drag.offset().left, drag.offset().top),\n choiceNo = this.getChoiceNoFromElement(drag);\n\n switch (e.keyCode) {\n case keys.arrowLeft:\n case 65: // A.\n point.x -= 1;\n break;\n case keys.arrowRight:\n case 68: // D.\n point.x += 1;\n break;\n case keys.arrowDown:\n case 83: // S.\n point.y += 1;\n break;\n case keys.arrowUp:\n case 87: // W.\n point.y -= 1;\n break;\n case keys.space:\n case keys.escape:\n point = null;\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n if (point !== null) {\n point = this.constrainToBgImg(point);\n drag.offset({'left': point.x, 'top': point.y});\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n var dragXY = this.convertToBgImgXY(new Lines.Point(drag.data('pagex'), drag.data('pagey')));\n drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());\n if (this.coordsInBgImg(new Lines.Point(drag.offset().left, drag.offset().top))) {\n if (drag.hasClass('unneeded')) {\n this.sendDragToDrop(drag, true);\n var hiddenDrag = this.getDragClone(drag);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n }\n this.cloneDragIfNeeded(drag);\n }\n }\n } else {\n drag.css('left', '').css('top', '');\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n this.sendDragHome(drag);\n this.removeDragIfNeeded(drag);\n }\n drag.focus();\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Makes sure the dragged item always exists within the background image area.\n *\n * @param {Point} windowxy\n * @returns {Point} coordinates\n */\n DrawlinesQuestion.prototype.constrainToBgImg = function(windowxy) {\n var bgImg = this.bgImage(),\n bgImgXY = this.convertToBgImgXY(windowxy);\n bgImgXY.x = Math.max(0, bgImgXY.x);\n bgImgXY.y = Math.max(0, bgImgXY.y);\n bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);\n bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);\n return this.convertToWindowXY(bgImgXY);\n };\n\n /**\n * Returns the choice number for a node.\n *\n * @param {Element|jQuery} node\n * @returns {Number}\n */\n DrawlinesQuestion.prototype.getChoiceNoFromElement = function(node) {\n return Number(this.getClassnameNumericSuffix(node, 'choice'));\n };\n\n /**\n * Returns the numeric part of a class with the given prefix.\n *\n * @param {Element|jQuery} node\n * @param {String} prefix\n * @returns {Number|null}\n */\n DrawlinesQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = $(node).attr('class');\n if (classes !== undefined && classes !== '') {\n var classesarr = classes.split(' ');\n for (var index = 0; index < classesarr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesarr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesarr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Handle when the window is resized.\n */\n DrawlinesQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('div.droparea .marker').not('.beingdragged').each(function(key, drag) {\n $(drag)\n .css('left', parseFloat($(drag).data('originX')) * parseFloat(bgRatio))\n .css('top', parseFloat($(drag).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(drag, 'left top');\n });\n\n this.getRoot().find('div.droparea svg.dropzones')\n .width(this.bgImage().width())\n .height(this.bgImage().height());\n\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var dropZone = thisQ.visibleDropZones[dropZoneNo];\n var originCoords = dropZone.coords;\n var line = thisQ.lines[dropZoneNo];\n var lineSVG = thisQ.lineSVGs[dropZoneNo];\n line.parse(originCoords, bgRatio);\n line.updateSvg(lineSVG);\n\n var handles = line.getHandlePositions();\n var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n markerSpan\n .css('left', handles.moveHandles.x - (markerSpan.outerWidth() / 2) - 4)\n .css('top', handles.moveHandles.y - (markerSpan.outerHeight() / 2));\n thisQ.handleElementScale(markerSpan, 'center');\n }\n };\n\n /**\n * Clone the drag.\n */\n DrawlinesQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('marker');\n placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));\n placeHolder.addClass(thisQ.getDragNoClass(drag, false));\n placeHolder.addClass('dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Get the drag number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the drag number.\n */\n DrawlinesQuestion.prototype.getDragNo = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'dragno');\n };\n\n /**\n * Get the drag number prefix of a drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} includeSelector include the CSS selector prefix or not.\n * @return {String} Class name\n */\n DrawlinesQuestion.prototype.getDragNoClass = function(drag, includeSelector) {\n var className = 'dragno' + this.getDragNo(drag);\n if (this.isInfiniteDrag(drag)) {\n className = 'infinite';\n }\n\n if (includeSelector) {\n return '.' + className;\n }\n\n return className;\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DrawlinesQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draghomes' + ' span.marker' +\n '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');\n };\n\n /**\n * Get the drop area element.\n * @returns {jQuery} droparea element.\n */\n DrawlinesQuestion.prototype.dropArea = function() {\n return this.getRoot().find('div.droparea');\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DrawlinesQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('beingdragged')\n .addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n var placeHolder = this.getDragClone(drag);\n placeHolder.after(drag);\n placeHolder.removeClass('active');\n };\n\n /**\n * Animate a drag item into a given place.\n *\n * @param {jQuery} drag the item to place.\n * @param {boolean} isScaling Scaling or not.\n * @param {boolean} initialLoad Whether it is the initial load or not.\n */\n DrawlinesQuestion.prototype.sendDragToDrop = function(drag, isScaling, initialLoad = false) {\n var dropArea = this.dropArea(),\n bgRatio = this.bgRatio();\n drag.removeClass('beingdragged').removeClass('unneeded');\n var dragXY = this.convertToBgImgXY(new Lines.Point(drag.data('pagex'), drag.data('pagey')));\n if (isScaling) {\n drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);\n drag.css('left', dragXY.x).css('top', dragXY.y);\n } else {\n drag.data('originX', dragXY.x).data('originY', dragXY.y);\n drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);\n }\n // We need to save the original scale ratio for each draggable item.\n if (!initialLoad) {\n // Only set the scale ratio for a current being-dragged item, not for the initial loading.\n drag.data('scaleRatio', bgRatio);\n }\n dropArea.append(drag);\n this.handleElementScale(drag, 'left top');\n };\n\n /**\n * Clone the drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DrawlinesQuestion.prototype.cloneDragIfNeeded = function(drag) {\n var inputNode = this.getInput(drag),\n noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),\n displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,\n displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;\n\n if ((this.isInfiniteDrag(drag) ||\n !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {\n var dragClone = drag.clone();\n dragClone.addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n this.getDragClone(drag)\n .removeClass('active')\n .after(dragClone);\n questionManager.addEventHandlersToMarker(dragClone);\n }\n };\n\n /**\n * Remove the clone drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DrawlinesQuestion.prototype.removeDragIfNeeded = function(drag) {\n var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');\n var displayedDrags = dragsInHome.length;\n while (displayedDrags > 1) {\n dragsInHome.first().remove();\n displayedDrags--;\n }\n };\n\n /**\n * Get the input belong to drag.\n *\n * @param {jQuery} drag the item to place.\n * @returns {jQuery} input element.\n */\n DrawlinesQuestion.prototype.getInput = function(drag) {\n var choiceNo = this.getChoiceNoFromElement(drag);\n return this.getRoot().find('input.choices.choice' + choiceNo);\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DrawlinesQuestion.prototype.handleElementScale = function(element, type) {\n var bgRatio = parseFloat(this.bgRatio());\n if (this.isPrinting) {\n bgRatio = 1;\n }\n $(element).css({\n '-webkit-transform': 'scale(' + bgRatio + ')',\n '-moz-transform': 'scale(' + bgRatio + ')',\n '-ms-transform': 'scale(' + bgRatio + ')',\n '-o-transform': 'scale(' + bgRatio + ')',\n 'transform': 'scale(' + bgRatio + ')',\n 'transform-origin': type\n });\n };\n\n /**\n * Check if the given drag is in infinite mode or not.\n *\n * @param {jQuery} drag The drag item need to check.\n */\n DrawlinesQuestion.prototype.isInfiniteDrag = function(drag) {\n return drag.hasClass('infinite');\n };\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DrawlinesQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n this.waitForAllImagesToBeLoaded();\n }, 100);\n return;\n }\n\n // We now have all images. Carry on, but only after giving the layout a chance to settle down.\n this.allImagesLoaded = true;\n this.cloneDrags();\n this.repositionDrags();\n this.drawDropzone();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DrawlinesQuestion.prototype.getNotYetLoadedImages = function() {\n return this.getRoot().find('.drawlines img.dropbackground').not(function(i, imgNode) {\n return this.imageIsLoaded(imgNode);\n });\n };\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n DrawlinesQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\n * @type {Object}\n */\n var questionManager = {\n\n /**\n * {boolean} ensures that the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} ensures that the marker event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n markerEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * @var {int} the number of lines on the form.\n */\n noOfLines: null,\n\n /**\n * @var {DrawlinesQuestion[]} the lines in the preview, indexed by line number.\n */\n dropZones: [],\n\n /**\n * @var {line[]} the question lines in the preview, indexed by line number.\n */\n questionLines: [],\n\n /**\n * Initialise one question.\n *\n * @param {String} containerId the id of the div.que that contains this question.\n * @param {boolean} readOnly whether the question is read-only.\n * @param {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.\n * @param {Object[]} questionLines\n */\n init: function(containerId, readOnly, visibleDropZones, questionLines) {\n questionManager.questions[containerId] =\n new DrawlinesQuestion(containerId, readOnly, visibleDropZones, questionLines);\n\n questionManager.questions[containerId].updateCoordinates();\n\n if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.markerEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('drawlines') &&\n !questionContainer.classList.contains('qtype_drawlines-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n // questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker'));\n // questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker'));\n // Add event listeners to the 'previewArea'.\n\n var dropArea = document.querySelector('.droparea');\n dropArea.addEventListener('mousedown', questionManager.handleEventMove);\n dropArea.addEventListener('touchstart', questionManager.handleEventMove);\n\n var drags = document.querySelector('.draghomes');\n drags.addEventListener('mousedown', questionManager.handleEventDragMove);\n drags.addEventListener('touchstart', questionManager.handleEventDragMove);\n }\n }\n },\n\n // TODO: commented as currently we are not using this function. To be removed later if not needed.\n // /**\n // * Set up the event handlers that make this question type work. (Done once per page.)\n // */\n // setupEventHandlers: function() {\n // $(window).on('resize', function() {\n // questionManager.handleWindowResize(false);\n // });\n // window.addEventListener('beforeprint', function() {\n // questionManager.isPrinting = true;\n // questionManager.handleWindowResize(questionManager.isPrinting);\n // });\n // window.addEventListener('afterprint', function() {\n // questionManager.isPrinting = false;\n // questionManager.handleWindowResize(questionManager.isPrinting);\n // });\n // setTimeout(function() {\n // questionManager.fixLayoutIfThingsMoved();\n // }, 100);\n // },\n\n handleEventMove: function(event) {\n var dropzoneElement, dropzoneNo, handleIndex;\n var question = questionManager.getQuestionForEvent(event);\n if (event.target.closest('.dropzone .startcircle.shape')) {\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n handleIndex = \"0\";\n question.handleMove(event, handleIndex, dropzoneNo);\n } else if (event.target.closest('.dropzone .endcircle.shape')) {\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n handleIndex = \"1\";\n question.handleMove(event, handleIndex, dropzoneNo);\n } else if (event.target.closest('polyline.shape')) {\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleDragMove(event, dropzoneNo);\n }\n },\n\n handleEventDragMove: function(event) {\n var dropzoneElement, dropzoneNo;\n var question = questionManager.getQuestionForEvent(event);\n if (event.target.closest('.dropzone polyline.shape')) {\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleDragMove(event, dropzoneNo);\n }\n },\n\n /**\n * Get the SVG element, if there is one, otherwise return null.\n *\n * @returns {SVGElement|null} the SVG element or null.\n */\n getSvg: function() {\n var svg = document.querySelector('.droparea svg');\n if (svg === null) {\n return null;\n } else {\n return svg;\n }\n },\n\n /**\n * Helper to get the value of a form elements with name like \"zonestart[0]\".\n *\n * @param {String} name the base name, e.g. 'zonestart'.\n * @param {String[]} indexes the indexes, e.g. ['0'].\n * @return {String} the value of that field.\n */\n getFormValue: function(name, indexes) {\n var el = this.getEl(name, indexes);\n return el.value;\n },\n\n /**\n * Binding the event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToMarker: function(element) {\n element\n .on('mousedown touchstart', questionManager.handleDragStart)\n .on('keydown keypress', questionManager.handleKeyPress)\n .focusin(function(e) {\n questionManager.handleKeyboardFocus(e, true);\n })\n .focusout(function(e) {\n questionManager.handleKeyboardFocus(e, false);\n });\n },\n\n /**\n * Handle mouse down / touch start events on markers.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on markers.\n * @param {Event} e\n */\n handleKeyPress: function(e) {\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * Handle focus lost events on markers.\n * @param {Event} e\n * @param {boolean} isNavigating\n */\n handleKeyboardFocus: function(e, isNavigating) {\n questionManager.isKeyboardNavigation = isNavigating;\n },\n\n /**\n * Sometimes, despite our best efforts, things change in a way that cannot\n * be specifically caught (e.g. dock expanding or collapsing in Boost).\n * Therefore, we need to periodically check everything is in the right position.\n */\n fixLayoutIfThingsMoved: function() {\n if (!questionManager.isKeyboardNavigation) {\n this.handleWindowResize(questionManager.isPrinting);\n }\n // We use setTimeout after finishing work, rather than setInterval,\n // in case positioning things is slow. We want 100 ms gap\n // between executions, not what setInterval does.\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);\n }, 100);\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DrawlinesQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.drawlines').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_drawlines/question\n */\n return {\n /**\n * Initialise one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {String[]} visibleDropZones the geometry of any drop-zones to show.\n * @param {Object[]} questionLines\n */\n init: questionManager.init,\n };\n});"],"names":["define","$","dragDrop","Lines","keys","FormChangeChecker","DrawlinesQuestion","containerId","readOnly","visibleDropZones","questionLines","thisQ","this","lineSVGs","lines","isPrinting","questionAnswer","svgEl","getRoot","addClass","allImagesLoaded","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","updateCoordinates","line","length","coordinates","getCoordinates","parse","updateSvgEl","drawSVGLines","bgImage","document","querySelector","innerHTML","width","dragsSvg","getElementById","height","startcoordinates","endcoordinates","make","labelstart","labelend","type","addToSvg","drawDropzone","svg","style","position","top","repositionDrags","root","find","not","each","key","item","input","choiceNo","getChoiceNoFromElement","imageCoords","getImageCoords","drag","remove","i","dragInDrop","clone","screenCoords","convertToWindowXY","data","x","y","sendDragToDrop","getDragClone","cloneDragIfNeeded","getQuestionAnsweredValues","result","inputNode","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","val","coordsStrings","split","Point","point","offset","left","convertToBgImgXY","coordsInBgImg","bgPosition","handleDragStart","e","dragged","target","closest","prepare","start","css","hasClass","hiddenDrag","dragEnd","dragXY","placed","bgRatio","bgImgXY","sendDragHome","removeDragIfNeeded","saveCoordsForChoice","childNodes","getAttribute","bgImg","bgImgNaturalWidth","get","naturalWidth","lineNumber","makeSvg","setAttribute","dropzoneNo","updateSvg","handleMove","handleIndex","info","movingDropZone","lastX","lastY","dragProxy","makeDragProxy","maxX","baseVal","maxY","pageX","pageY","move","parseInt","body","removeChild","handleDragMove","movingDrag","svgDrags","svgDropZones","whichSVG","selectedElement","dropX","clientX","dropY","clientY","svgDragsbBox","getBBox","svgDropZonesbBox","centre1","addToDropZone","moveDrags","createElement","appendChild","items","thiQ","join","questionManager","handleFormDirty","handleKeyPress","keyCode","arrowLeft","arrowRight","arrowDown","arrowUp","space","escape","preventDefault","constrainToBgImg","focus","windowxy","Math","max","min","node","Number","getClassnameNumericSuffix","prefix","classes","attr","undefined","classesarr","index","RegExp","test","match","exec","handleResize","parseFloat","handleElementScale","dropZoneNo","originCoords","coords","lineSVG","handles","getHandlePositions","markerSpan","moveHandles","outerWidth","outerHeight","cloneDrags","draghome","placeHolder","removeClass","getDragNoClass","before","getDragNo","includeSelector","className","isInfiniteDrag","dropArea","after","isScaling","initialLoad","append","getInput","noOfDrags","displayedDragsInDropArea","displayedDragsInDragHomes","dragClone","addEventHandlersToMarker","dragsInHome","displayedDrags","first","element","imageLoadingTimeoutId","clearTimeout","setTimeout","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","eventHandlersInitialised","markerEventHandlersInitialised","isKeyboardNavigation","questions","noOfLines","dropZones","init","hasOwnProperty","questionContainer","classList","contains","addEventListener","handleEventMove","drags","handleEventDragMove","event","question","getQuestionForEvent","dataset","getSvg","getFormValue","name","indexes","getEl","on","focusin","handleKeyboardFocus","focusout","handleWindowResize","isNavigating","fixLayoutIfThingsMoved","currentTarget","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/Line.js b/amd/src/Line.js index 8af27fe..5d7a334 100644 --- a/amd/src/Line.js +++ b/amd/src/Line.js @@ -179,11 +179,11 @@ define(function() { // Set start and end label attributes. svgEl.childNodes[3].textContent = this.labelstart; svgEl.childNodes[3].setAttribute('x', this.centre1.x); - svgEl.childNodes[3].setAttribute('y', this.centre1.y + 20); + svgEl.childNodes[3].setAttribute('y', parseInt(this.centre1.y) + 20); svgEl.childNodes[4].textContent = this.labelend; svgEl.childNodes[4].setAttribute('x', this.centre2.x); - svgEl.childNodes[4].setAttribute('y', this.centre2.y + 20); + svgEl.childNodes[4].setAttribute('y', parseInt(this.centre2.y) + 20); }; /** @@ -352,6 +352,115 @@ define(function() { } }; + /** + * Move the entire line 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. + * @param {String} whichSVG The svg containing the drag. + * @param {int} lineNo The line number + */ + Line.prototype.moveDrags = function(dx, dy, maxX, maxY, whichSVG, lineNo) { + this.centre1.move(dx, dy); + this.centre2.move(dx, dy); + if (whichSVG === 'svgDropZones') { + this.centre1.x = 50; + this.centre1.y = 25 + lineNo * 50; + this.x1 = 50; + this.y1 = 25 + lineNo * 50; + this.centre2.x = 200; + this.x2 = 200; + this.centre2.y = 25 + lineNo * 50; + this.y2 = 25 + lineNo * 50; + } else { + if (this.centre1.x < this.startRadius) { + this.centre1.x = this.startRadius; + this.x1 = this.startRadius; + } + if (this.centre1.x > maxX - this.startRadius) { + this.centre1.x = maxX - this.startRadius; + this.x1 = maxX - this.startRadius; + } + if (this.centre2.x < this.startRadius) { + this.centre2.x = this.startRadius; + this.x2 = this.startRadius; + } + if (this.centre2.x > maxX - this.startRadius) { + this.centre2.x = maxX - this.startRadius; + this.x2 = maxX - this.startRadius; + } + if (this.centre1.y < this.endRadius) { + this.centre1.y = this.endRadius; + this.y1 = this.endRadius; + } + if (this.centre1.y > maxY - this.endRadius) { + this.centre1.y = maxY - this.endRadius; + this.y1 = maxY - this.endRadius; + } + if (this.centre2.y < this.endRadius) { + this.centre2.y = this.endRadius; + this.y2 = this.endRadius; + } + if (this.centre2.y > maxY - this.endRadius) { + this.centre2.y = maxY - this.endRadius; + this.y2 = maxY - this.endRadius; + } + } + }; + + /** + * Move the g element to the dropzone. + * @param {SVGElement} svgDrags Svg element containing the drags. + * @param {SVGElement} svgDropZones Svg element containing the dropZone. + * @param {SVGElement} selectedElement The element selected for dragging. + * @param {int} dropX + * @param {int} dropY + */ + Line.prototype.addToDropZone = function(svgDrags, svgDropZones, selectedElement, dropX, dropY) { + var maxY = 0; + var dropzoneNo = selectedElement.getAttribute('data-dropzone-no'); + + if (this.isInsideSVG(svgDrags, dropX, dropY)) { + // Append the element to the second SVG + // Get the height of the dropZone SVG. + maxY = svgDropZones.height.baseVal.value; + svgDropZones.appendChild(selectedElement); + selectedElement.getAttribute('data-dropzone-no'); + + // Caluculate the position of line drop. + this.centre1.y = maxY - (2 * this.startRadius) - (dropzoneNo * 50); + this.y1 = maxY - (2 * this.startRadius) - (dropzoneNo * 50); + this.centre2.y = maxY - (2 * this.endRadius) - (dropzoneNo * 50); + this.y2 = maxY - (2 * this.endRadius) - (dropzoneNo * 50); + } else if (this.isInsideSVG(svgDropZones, dropX, dropY)) { + // Append the element to the first SVG (to ensure it stays in the same SVG if dropped there) + svgDrags.appendChild(selectedElement); + + // We want to drop the lines from the top, depending on the line number. + // Caluculate the position of line drop. + this.centre1.x = 50; + this.centre1.y = this.startRadius + (dropzoneNo * 50); + this.y1 = this.startRadius + (dropzoneNo * 50); + this.centre2.x = 200; + this.centre2.y = this.endRadius + (dropzoneNo * 50); + this.y2 = this.endRadius + (dropzoneNo * 50); + } + }; + + /** + * Check if the current selected element is in the svg . + * @param {SVGElement} svg Svg element containing the drags. + * @param {int} dropX + * @param {int} dropY + * @return {bool} + */ + Line.prototype.isInsideSVG = function(svg, dropX, dropY){ + const rect = svg.getBoundingClientRect(); + return dropX >= rect.left && dropX <= rect.right && dropY >= rect.top && dropY <= rect.bottom; + }; + /** * Move one of the edit handles by this offset. * @@ -529,8 +638,8 @@ define(function() { * @return {line} the similar line of a different linetype. */ getSimilar: function(lineType, line) { - return new Line(line.labelstart, line.x1, line.y1, line.startRadius, line.labelend, - line.x2, line.y2, line.endRadius, lineType); + return new Line(line.labelstart, parseInt(line.x1), parseInt(line.y1), parseInt(line.startRadius), + parseInt(line.labelend), parseInt(line.x2), parseInt(line.y2), parseInt(line.endRadius), lineType); } }; }); diff --git a/amd/src/question.js b/amd/src/question.js index 7e0be7d..9ad5504 100644 --- a/amd/src/question.js +++ b/amd/src/question.js @@ -26,13 +26,13 @@ define([ 'core/dragdrop', 'qtype_drawlines/Line', 'core/key_codes', - 'core_form/changechecker' + 'core_form/changechecker', ], function( $, dragDrop, Lines, keys, - FormChangeChecker + FormChangeChecker, ) { "use strict"; @@ -44,18 +44,19 @@ define([ * @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 line, coords and markertext. + * @param {line[]} questionLines * @constructor */ - function DrawlinesQuestion(containerId, readOnly, visibleDropZones) { - window.console.log('containerId ---------------------------------------------'); - window.console.log('containerId = ' + containerId); + function DrawlinesQuestion(containerId, readOnly, visibleDropZones, questionLines) { var thisQ = this; this.containerId = containerId; this.visibleDropZones = visibleDropZones; + this.questionLines = questionLines; this.lineSVGs = []; this.lines = []; this.isPrinting = false; this.questionAnswer = {}; + this.svgEl = null; if (readOnly) { this.getRoot().addClass('qtype_drawlines-readonly'); } @@ -67,75 +68,120 @@ define([ } /** - * Draws the svg lines of any drop zones that should be visible for feedback purposes. + * Update the coordinates from a particular string. */ - DrawlinesQuestion.prototype.drawDropzones = function() { - if (this.visibleDropZones.length > 0) { - var bgImage = this.bgImage(); - - this.getRoot().find('div.dropzones').html(''); - 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); + DrawlinesQuestion.prototype.updateCoordinates = function() { + // We don't need to scale the shape for editing form. + for (var line = 0; line < this.lineSVGs.length; line++) { + var coordinates = this.getCoordinates(this.lineSVGs[line]); + if (!this.lines[line].parse(coordinates[0], coordinates[1], 1)) { + // Invalid coordinates. Don't update the preview. + return; } + this.updateSvgEl(line); } }; /** - * Adds a dropzone line with colour, coords and link provided to the array of Lines. - * - * @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 - */ - DrawlinesQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) { - var dropZone = this.visibleDropZones[dropZoneNo], - line = Lines.make(dropZone.line, ''), - existingmarkertext, - bgRatio = this.bgRatio(); - if (!line.parse(dropZone.coords, bgRatio)) { - return; + * Draws the svg lines of any drop zones. + * @param {Object[]} questionLines + */ + DrawlinesQuestion.prototype.drawSVGLines = function(questionLines) { + var bgImage = document.querySelector('img.dropbackground'); + + var drags = document.querySelector('.draghomes'); + drags.innerHTML = + ''; + + var dragsSvg = document.getElementById('que-dlines-svg-drags'); + var initialHeight = 25; + for (let line = 0; line < questionLines.length; line++) { + var height = initialHeight + line * 50; + var startcoordinates = '50,' + height + ';10'; + var endcoordinates = '200,' + height + ';10'; + this.lines[line] = Lines.make([startcoordinates, endcoordinates], + [questionLines[line].labelstart, questionLines[line].labelend], questionLines[line].type); + this.addToSvg(line, dragsSvg); } + }; - 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('' + - dropZone.markertext + ''); - var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo); - if (markerspan.length) { - var handles = line.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'); - } + /** + * Draws the svg lines of any drop zones that should be visible for feedback purposes. + */ + DrawlinesQuestion.prototype.drawDropzone = function() { + var bgImage = document.querySelector('img.dropbackground'); + var svg = document.querySelector('svg.dropzones'); + document.getElementById('que-dlines-dropzone').style.position = 'relative'; + document.getElementById('que-dlines-dropzone').style.top = (bgImage.height + 1) * -1 + "px"; + document.getElementById('que-dlines-dropzone').style.height = bgImage.height + "px"; + document.getElementById('que-dlines-droparea').style.height = bgImage.height + "px"; + if (!svg) { + var dropZone = document.querySelector('#que-dlines-dropzone'); + dropZone.innerHTML = + ''; } - - var lineSVG = line.makeSvg(svg[0]); - lineSVG.setAttribute('class', 'dropzone ' + colourClass); - - this.lines[this.Lines.length] = line; - this.lineSVGs[this.lineSVGs.length] = lineSVG; + this.drawSVGLines(this.questionLines); }; + // + // /** + // * Adds a dropzone line with colour, coords and link provided to the array of Lines. + // * + // * @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 + // */ + // DrawlinesQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) { + // var dropZone = this.visibleDropZones[dropZoneNo], + // line = Lines.make(dropZone.line, ''), + // existingmarkertext, + // bgRatio = this.bgRatio(); + // if (!line.parse(dropZone.coords, bgRatio)) { + // return; + // } + // + // existingmarkertext = this.getRoot().find('div.markertexts span.markerlabelstart' + 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('' + + // dropZone.markertext + ''); + // var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo); + // if (markerspan.length) { + // var handles = line.getHandlePositions(); + // var positionLeft = handles.moveHandles.x - (markerspan.outerWidth() / 2) - 4; + // var positionTop = handles.moveHandles.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 lineSVG = line.makeSvg(svg[0]); + // lineSVG.setAttribute('class', 'dropzone ' + colourClass); + // + // this.lines[this.Lines.length] = line; + // this.lineSVGs[this.lineSVGs.length] = lineSVG; + // }; + /** * 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 @@ -152,6 +198,7 @@ define([ root.find('input.choices').each(function(key, input) { var choiceNo = thisQ.getChoiceNoFromElement(input), imageCoords = thisQ.getImageCoords(input); + if (imageCoords.length) { var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder'); drag.remove(); @@ -302,7 +349,6 @@ define([ if (!info.start) { return; } - dragged.addClass('beingdragged').css('transform', ''); var placed = !dragged.hasClass('unneeded'); @@ -359,6 +405,167 @@ define([ this.saveCoordsForChoice(choiceNo); }; + + /** + * Returns the coordinates for the line from the text input in the form. + * @param {SVGElement} svgEl + * @returns {Array} the coordinates. + */ + DrawlinesQuestion.prototype.getCoordinates = function(svgEl) { + + var circleStartXCoords = svgEl.childNodes[1].getAttribute('cx'); + var circleStartYCoords = svgEl.childNodes[1].getAttribute('cy'); + var circleStartRCoords = svgEl.childNodes[1].getAttribute('r'); + var circleEndXCoords = svgEl.childNodes[2].getAttribute('cx'); + var circleEndYCoords = svgEl.childNodes[2].getAttribute('cy'); + var circleEndRCoords = svgEl.childNodes[2].getAttribute('r'); + return [circleStartXCoords + ',' + circleStartYCoords + ';' + circleStartRCoords, + circleEndXCoords + ',' + circleEndYCoords + ';' + circleEndRCoords]; + }; + + /** + * Return the background ratio. + * + * @returns {number} Background ratio. + */ + DrawlinesQuestion.prototype.bgRatio = function() { + var bgImg = this.bgImage(); + var bgImgNaturalWidth = bgImg.get(0).naturalWidth; + var bgImgClientWidth = bgImg.width(); + + return bgImgClientWidth / bgImgNaturalWidth; + }; + + + /** + * Add this line to an SVG graphic. + * + * @param {int} lineNumber Line Number + * @param {SVGElement} svg the SVG image to which to add this drop zone. + */ + DrawlinesQuestion.prototype.addToSvg = function(lineNumber, svg) { + this.lineSVGs[lineNumber] = this.lines[lineNumber].makeSvg(svg); + if (!this.lineSVGs[lineNumber]) { + return; + } + this.lineSVGs[lineNumber].setAttribute('class', 'dropzone'); + this.lineSVGs[lineNumber].setAttribute('data-dropzone-no', lineNumber); + }; + + /** + * Update the shape of this drop zone (but not type) in an SVG image. + * @param {int} dropzoneNo + */ + DrawlinesQuestion.prototype.updateSvgEl = function(dropzoneNo) { + this.lines[dropzoneNo].updateSvg(this.lineSVGs[dropzoneNo]); + }; + + /** + * Start responding to dragging the move handle. + * @param {Event} e Event object + * @param {String} handleIndex + * @param {int} dropzoneNo + */ + DrawlinesQuestion.prototype.handleMove = function(e, handleIndex, dropzoneNo) { + 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), + svg = document.querySelector('svg.dropzones'), + maxX = svg.width.baseVal.value, + maxY = svg.height.baseVal.value; + + dragDrop.start(e, $(dragProxy), function(pageX, pageY) { + movingDropZone.lines[dropzoneNo].move(handleIndex, + parseInt(pageX) - parseInt(lastX), parseInt(pageY) - parseInt(lastY), parseInt(maxX), parseInt(maxY)); + lastX = pageX; + lastY = pageY; + movingDropZone.updateSvgEl(dropzoneNo); + }, function() { + document.body.removeChild(dragProxy); + }); + }; + + + /** + * Start responding to dragging the move handle. + * @param {Event} e Event object + * @param {int} dropzoneNo + */ + DrawlinesQuestion.prototype.handleDragMove = function(e, dropzoneNo) { + var info = dragDrop.prepare(e); + if (!info.start) { + return; + } + var movingDrag = this, + lastX = info.x, + lastY = info.y, + dragProxy = this.makeDragProxy(info.x, info.y), + svgDrags = document.querySelector('svg.drags'), + svgDropZones = document.querySelector('svg.dropzones'), + maxX = svgDrags.width.baseVal.value, + maxY = svgDrags.height.baseVal.value, + whichSVG = ""; + + var selectedElement = this.lineSVGs[dropzoneNo]; + const dropX = e.clientX; + const dropY = e.clientY; + + dragDrop.start(e, $(dragProxy), function(pageX, pageY) { + var svgDragsbBox = svgDrags.getBBox(); + var svgDropZonesbBox = svgDropZones.getBBox(); + + // Check if the drags need to be moved from one svg to another. + // If true, the drag is moved from draghomes SVG to dropZone SVG. + if (movingDrag.lines[dropzoneNo].centre1.y === 10 && svgDragsbBox.y === 0) { + movingDrag.lines[dropzoneNo].addToDropZone(svgDrags, svgDropZones, selectedElement, + dropX, dropY); + maxX = svgDropZones.width.baseVal.value; + maxY = svgDropZones.height.baseVal.value; + whichSVG = "svgDropZones"; + } else if (movingDrag.lines[dropzoneNo].centre1.y === svgDropZonesbBox.y + 10) { + // Move drags from dropZone SVG to draghomes SVG. + movingDrag.lines[dropzoneNo].addToDropZone(svgDrags, svgDropZones, selectedElement, + dropX, dropY); + whichSVG = "svgDrags"; + } + + // Drag the lines within the SVG + movingDrag.lines[dropzoneNo].moveDrags( + parseInt(pageX) - parseInt(lastX), parseInt(pageY) - parseInt(lastY), + parseInt(maxX), parseInt(maxY), whichSVG, dropzoneNo); + lastX = pageX; + lastY = pageY; + + movingDrag.updateSvgEl(dropzoneNo); + }, function() { + document.body.removeChild(dragProxy); + }); + }; + + + /** + * Make an invisible drag proxy. + * + * @param {int} x x position . + * @param {int} y y position. + * @returns {HTMLElement} the drag proxy. + */ + DrawlinesQuestion.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; + }; + /** * Save the coordinates for a dropped item in the form field. * @param {Number} choiceNo which copy of the choice this was. @@ -540,28 +747,28 @@ define([ var handles = line.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)); + .css('left', handles.moveHandles.x - (markerSpan.outerWidth() / 2) - 4) + .css('top', handles.moveHandles.y - (markerSpan.outerHeight() / 2)); thisQ.handleElementScale(markerSpan, 'center'); } }; - // /** - // * Clone the drag. - // */ - // DrawlinesQuestion.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'); - // placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag)); - // placeHolder.addClass(thisQ.getDragNoClass(drag, false)); - // placeHolder.addClass('dragplaceholder'); - // drag.before(placeHolder); - // }); - // }; + /** + * Clone the drag. + */ + DrawlinesQuestion.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'); + placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag)); + placeHolder.addClass(thisQ.getDragNoClass(drag, false)); + placeHolder.addClass('dragplaceholder'); + drag.before(placeHolder); + }); + }; /** * Get the drag number of a drag. @@ -593,16 +800,16 @@ define([ return className; }; - // /** - // * Get drag clone for a given drag. - // * - // * @param {jQuery} drag the drag. - // * @returns {jQuery} the drag's clone. - // */ - // DrawlinesQuestion.prototype.getDragClone = function(drag) { - // return this.getRoot().find('.draghomes' + ' span.marker' + - // '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder'); - // }; + /** + * Get drag clone for a given drag. + * + * @param {jQuery} drag the drag. + * @returns {jQuery} the drag's clone. + */ + DrawlinesQuestion.prototype.getDragClone = function(drag) { + return this.getRoot().find('.draghomes' + ' span.marker' + + '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder'); + }; /** * Get the drop area element. @@ -709,19 +916,6 @@ define([ return this.getRoot().find('input.choices.choice' + choiceNo); }; - /** - * Return the background ratio. - * - * @returns {number} Background ratio. - */ - DrawlinesQuestion.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. * @@ -783,9 +977,9 @@ define([ // We now have all images. Carry on, but only after giving the layout a chance to settle down. this.allImagesLoaded = true; - //this.cloneDrags(); + this.cloneDrags(); this.repositionDrags(); - this.drawDropzones(); + this.drawDropzone(); }; /** @@ -843,20 +1037,35 @@ define([ */ questions: {}, // An object containing all the information about each question on the page. + /** + * @var {int} the number of lines on the form. + */ + noOfLines: null, + + /** + * @var {DrawlinesQuestion[]} the lines in the preview, indexed by line number. + */ + dropZones: [], + + /** + * @var {line[]} the question lines in the preview, indexed by line number. + */ + questionLines: [], + /** * Initialise one question. * * @param {String} containerId the id of the div.que that contains this question. * @param {boolean} readOnly whether the question is read-only. * @param {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback. + * @param {Object[]} questionLines */ - init: function(containerId, readOnly, visibleDropZones) { + init: function(containerId, readOnly, visibleDropZones, questionLines) { questionManager.questions[containerId] = - new DrawlinesQuestion(containerId, readOnly, visibleDropZones); - if (!questionManager.eventHandlersInitialised) { - questionManager.setupEventHandlers(); - questionManager.eventHandlersInitialised = true; - } + new DrawlinesQuestion(containerId, readOnly, visibleDropZones, questionLines); + + questionManager.questions[containerId].updateCoordinates(); + if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) { questionManager.markerEventHandlersInitialised[containerId] = true; // We do not use the body event here to prevent the other event on Mobile device, such as scroll event. @@ -864,30 +1073,96 @@ define([ if (questionContainer.classList.contains('drawlines') && !questionContainer.classList.contains('qtype_drawlines-readonly')) { // TODO: Convert all the jQuery selectors and events to native Javascript. - questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker')); - questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker')); + // questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker')); + // questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker')); + // Add event listeners to the 'previewArea'. + + var dropArea = document.querySelector('.droparea'); + dropArea.addEventListener('mousedown', questionManager.handleEventMove); + dropArea.addEventListener('touchstart', questionManager.handleEventMove); + + var drags = document.querySelector('.draghomes'); + drags.addEventListener('mousedown', questionManager.handleEventDragMove); + drags.addEventListener('touchstart', questionManager.handleEventDragMove); } } }, + // TODO: commented as currently we are not using this function. To be removed later if not needed. + // /** + // * Set up the event handlers that make this question type work. (Done once per page.) + // */ + // setupEventHandlers: function() { + // $(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); + // }, + + handleEventMove: function(event) { + var dropzoneElement, dropzoneNo, handleIndex; + var question = questionManager.getQuestionForEvent(event); + if (event.target.closest('.dropzone .startcircle.shape')) { + dropzoneElement = event.target.closest('g'); + dropzoneNo = dropzoneElement.dataset.dropzoneNo; + handleIndex = "0"; + question.handleMove(event, handleIndex, dropzoneNo); + } else if (event.target.closest('.dropzone .endcircle.shape')) { + dropzoneElement = event.target.closest('g'); + dropzoneNo = dropzoneElement.dataset.dropzoneNo; + handleIndex = "1"; + question.handleMove(event, handleIndex, dropzoneNo); + } else if (event.target.closest('polyline.shape')) { + dropzoneElement = event.target.closest('g'); + dropzoneNo = dropzoneElement.dataset.dropzoneNo; + question.handleDragMove(event, dropzoneNo); + } + }, + + handleEventDragMove: function(event) { + var dropzoneElement, dropzoneNo; + var question = questionManager.getQuestionForEvent(event); + if (event.target.closest('.dropzone polyline.shape')) { + dropzoneElement = event.target.closest('g'); + dropzoneNo = dropzoneElement.dataset.dropzoneNo; + question.handleDragMove(event, dropzoneNo); + } + }, + /** - * Set up the event handlers that make this question type work. (Done once per page.) + * Get the SVG element, if there is one, otherwise return null. + * + * @returns {SVGElement|null} the SVG element or null. */ - setupEventHandlers: function() { - $(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); + getSvg: function() { + var svg = document.querySelector('.droparea svg'); + if (svg === null) { + return null; + } else { + return svg; + } + }, + + /** + * Helper to get the value of a form elements with name like "zonestart[0]". + * + * @param {String} name the base name, e.g. 'zonestart'. + * @param {String[]} indexes the indexes, e.g. ['0']. + * @return {String} the value of that field. + */ + getFormValue: function(name, indexes) { + var el = this.getEl(name, indexes); + return el.value; }, /** @@ -996,10 +1271,10 @@ define([ * 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. + * @param {Object[]} questionLines */ - init: questionManager.init + init: questionManager.init, }; }); \ No newline at end of file diff --git a/renderer.php b/renderer.php index 784237c..4d37501 100644 --- a/renderer.php +++ b/renderer.php @@ -118,41 +118,21 @@ public function formulation_and_controls(question_attempt $qa, question_display_ $output = html_writer::div($questiontext, 'qtext'); $output .= html_writer::start_div('ddarea'); - $output .= html_writer::start_div($dropareaclass); + $output .= html_writer::start_div($dropareaclass, ['id' => 'que-dlines-droparea']); $output .= html_writer::img(self::get_url_for_image($qa, 'bgimage'), get_string('dropbackground', 'qtype_drawlines'), - ['class' => 'dropbackground img-fluid w-100']); + ['class' => 'dropbackground img-fluid']); + $output .= html_writer::start_div('', ['id' => 'que-dlines-dropzone']); + $output .= html_writer::end_div(); + $output .= html_writer::end_div(); - $output .= html_writer::div('', 'dropzones'); $output .= html_writer::div('', 'markertexts'); - $output .= html_writer::end_div(); $output .= html_writer::start_div($draghomesclass); if (!$options->readonly) { $attr['tabindex'] = 0; } - // Display the lines. - $hiddenfields = ''; - foreach ($question->lines as $key => $line) { - // TODO:Aadd data attribute to be used by question.js. - $classes = ['line' . $line->number, 'user-select-none', 'choice' . $line->number]; - $attr = []; - $classes[] = 'dragno' . (count($question->lines) * 2); - $dragoutput = html_writer::start_span(join(' ', $classes), $attr); - $targeticonhtml = $this->output->image_icon('crosshairs', '', $componentname, ['class' => 'target']); - $labelstart = html_writer::span($line->labelstart, 'labelstart'); - $labelend = html_writer::span($line->labelend, 'labelend'); - $dragoutput .= $targeticonhtml . $labelstart; - $dragoutput .= $targeticonhtml . $labelend; - $dragoutput .= html_writer::end_span(); - $output .= $dragoutput; - $hiddenfields .= $this->hidden_field_choice($qa, $line->number, $line->labelstart, null); - - $output .= html_writer::div(implode(' ', (array)$line)); - //$output .= html_writer::div('Line ' . $line->number . ' will be displayed here'); - } - if ($question->showmisplaced && $qa->get_state()->is_finished()) { $visibledropzones = $question->get_drop_zones_without_hit($response); } else { @@ -165,7 +145,7 @@ public function formulation_and_controls(question_attempt $qa, question_display_ // Call to js $this->page->requires->js_call_amd('qtype_drawlines/question', 'init', - [$qa->get_outer_question_div_unique_id(), $options->readonly, $visibledropzones]); + [$qa->get_outer_question_div_unique_id(), $options->readonly, $visibledropzones, $question->lines]); $output .= html_writer::end_div(); $output .= html_writer::end_div(); diff --git a/styles.css b/styles.css index 69c5992..2bff5da 100644 --- a/styles.css +++ b/styles.css @@ -1,44 +1,45 @@ -/* Styles for . */ -.que. .qtext { +/* Styles for DrawLines. */ +.que.drawlines .qtext { display: block; margin-bottom: 0.5em; } -.que. .dropbackground, +.que.drawlines .dropbackground, form.mform fieldset#id_previewareaheader .dropbackground { margin: 0 auto; border: 1px solid black; } -.que. line.color1 { +.que.drawlines line.color1 { fill: #1d1e1d; } -.que. line.color2 { +.que.drawlines line.color2 { fill: #1c5fa1; } -.que. line.color3 { +.que.drawlines line.color3 { fill: #39da20; } -.que. line.color4 { +.que.drawlines line.color4 { fill: rgba(255, 165, 0, 0.98); } -.que. line.color5 { +.que.drawlines line.color5 { fill: #a11c1c; } -.que. line.color6 { +.que.drawlines line.color6 { fill: #e50de5; } -.que. .dropzone.color7 .shape { +.que.drawlines .dropzone.color7 .shape { fill: #f0e68c; } -.que. .dropzone .shapeLabel { +.que.drawlines .dropzone .shapeLabel { text-anchor: middle; + font-size: large; } -.que. .dropzone .handlestart, -.que. .dropzone .handleend { +.que.drawlines .dropzone .handlestart, +.que.drawlines .dropzone .handleend { fill: #fff; fill-opacity: 0.1; /* Need a small amount of opacity of the handle can't be grabbed. */ stroke-width: 1; @@ -46,64 +47,81 @@ form.mform fieldset#id_previewareaheader .dropbackground { cursor: move; } -.que. .dropzone .startcircle, -.que. .dropzone .endcircle { +.que.drawlines .dropzone .startcircle, +.que.drawlines .dropzone .endcircle { fill: #fff; fill-opacity: 0.5; stroke: black; stroke-width: 1; } -.que. .dropzone .handlestart.move, -.que. .dropzone .handleend.move { +.que.drawlines .dropzone .handlestart.move, +.que.drawlines .dropzone .handleend.move { stroke: #800; } -.que. .dropzone .handlestart.edit, -.que. .dropzone .handleend.edit { +.que.drawlines .dropzone .handlestart.edit, +.que.drawlines .dropzone .handleend.edit { stroke: #008; } -.que. .dropzone.active .handlestart, -.que. .dropzone.active .handleend { +.que.drawlines .dropzone.active .handlestart, +.que.drawlines .dropzone.active .handleend { display: inherit; } -.que.ddmarker .draghomes .marker, -.que.ddmarker .droparea .marker { +.que.drawlines .draghomes .marker, +.que.drawlines .droparea .marker { vertical-align: top; cursor: move; } -.que.ddmarker .draghomes.readonly .marker, -.que.ddmarker .droparea.readonly .marker { +.que.drawlines .draghomes.readonly .marker, +.que.drawlines .droparea.readonly .marker { cursor: auto; } -.que.ddmarker .droparea .marker { +.que.drawlines .droparea .marker { position: absolute; } -.que.ddmarker .draghomes .marker { +.que.drawlines .draghomes .marker { position: relative; display: inline-block; margin: 10px; } -.que.ddmarker .draghomes .marker.dragplaceholder { +.que.drawlines .draghomes .marker.dragplaceholder { display: none; } -.que.ddmarker .draghomes .marker.dragplaceholder.active { +.que.drawlines .draghomes .marker.dragplaceholder.active { visibility: hidden; display: inline-block; } -.que.ddmarker .droparea .marker img.target, -.que.ddmarker .draghomes .marker img.target { - position: absolute; +.que.drawlines .droparea .marker img.target, +.que.drawlines .draghomes .marker img.target { 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. */ } -.que.ddmarker div.dragitems div.draghome img.target { +.que.drawlines .draghomes .marker .icon { + margin-right: 0; +} + +.que.drawlines div.dragitems div.draghome img.target { display: none; } + +.que.drawlines .droparea { + position: relative; + background-color: #e7f3f5; +} + +.que.drawlines div.ddarea, +form.mform fieldset#id_previewareaheader div.ddarea { + text-align: center; +} + +.que.drawlines polyline.shape { + cursor: move; +}