diff --git a/amd/build/line.min.js b/amd/build/line.min.js index ad867f9..9a00888 100644 --- a/amd/build/line.min.js +++ b/amd/build/line.min.js @@ -8,6 +8,6 @@ * @copyright 2024 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,this.y1=y1,this.x2=x2,this.y2=y2,this.centre1=new Point(x1,y1),this.centre2=new Point(x2,y2),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"),lineEl=createSvgElement(svgEl,tagName);return lineEl.setAttribute("class","shape"),lineEl.setAttribute("tabindex","0"),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);var svgClass=svgEl.getAttribute("class");svgClass&&svgClass.includes("placed")&&(svgEl.childNodes[1].setAttribute("tabindex","0"),svgEl.childNodes[2].setAttribute("tabindex","0"))},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)",svgEl.childNodes[0].setAttribute("class","shape singlearrow");break;case"linedoublearrows":svgEl.childNodes[0].style["marker-start"]="url(#arrow)",svgEl.childNodes[0].style["marker-end"]="url(#arrow)",svgEl.childNodes[0].setAttribute("class","shape doublearrows");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),svgEl.childNodes[0].setAttribute("class","shape infinite")}},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(whichHandle,dx,dy,maxX,maxY){"startcircle"===whichHandle?(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){"DragsSVG"===whichSVG?(this.centre1.move(0,dy),this.centre2.move(0,dy),this.centre1.x=50,this.x1=50,this.centre2.x=200,this.x2=200):(this.centre1.move(dx,dy),this.centre2.move(dx,dy),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(eventType,selectedElement,svgDropZones,svgDragsHome,dropX,dropY,whichSVG){var maxY=0,dropzoneNo=selectedElement.getAttribute("data-dropzone-no"),classattributes="";return("mouse"===eventType?this.isInsideSVG(svgDragsHome,dropX,dropY):"DragsSVG"===whichSVG)?(maxY=svgDropZones.height.baseVal.value,svgDropZones.appendChild(selectedElement),selectedElement.getAttribute("data-dropzone-no"),selectedElement.childNodes[1].setAttribute("tabindex","0"),selectedElement.childNodes[2].setAttribute("tabindex","0"),this.centre1.y=maxY-2*this.startRadius,this.y1=maxY-2*this.startRadius,this.centre2.y=maxY-2*this.endRadius,this.y2=maxY-2*this.endRadius,classattributes=(classattributes=selectedElement.getAttribute("class")).replace("inactive","placed"),selectedElement.setAttribute("class",classattributes)):(svgDragsHome.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,classattributes=(classattributes=selectedElement.getAttribute("class")).replace("placed","inactive"),selectedElement.setAttribute("class",classattributes),selectedElement.childNodes[1].setAttribute("tabindex","-1"),selectedElement.childNodes[2].setAttribute("tabindex","-1")),""},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),line.labelend,parseInt(line.x2),parseInt(line.y2),parseInt(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,this.y1=y1,this.x2=x2,this.y2=y2,this.centre1=new Point(x1,y1),this.centre2=new Point(x2,y2),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 svgEl.setAttribute("tabindex","0"),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);var svgClass=svgEl.getAttribute("class");svgClass&&svgClass.includes("placed")&&(svgEl.childNodes[1].setAttribute("tabindex","0"),svgEl.childNodes[2].setAttribute("tabindex","0"))},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)",svgEl.childNodes[0].setAttribute("class","shape singlearrow");break;case"linedoublearrows":svgEl.childNodes[0].style["marker-start"]="url(#arrow)",svgEl.childNodes[0].style["marker-end"]="url(#arrow)",svgEl.childNodes[0].setAttribute("class","shape doublearrows");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),svgEl.childNodes[0].setAttribute("class","shape infinite")}},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(whichHandle,dx,dy,maxX,maxY){"startcircle"===whichHandle?(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){"DragsSVG"===whichSVG?(this.centre1.move(0,dy),this.centre2.move(0,dy),this.centre1.x=50,this.x1=50,this.centre2.x=200,this.x2=200):(this.centre1.move(dx,dy),this.centre2.move(dx,dy),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(eventType,selectedElement,svgDropZones,svgDragsHome,dropX,dropY,whichSVG){var maxY=0,dropzoneNo=selectedElement.getAttribute("data-dropzone-no"),classattributes="";("mouse"===eventType?this.isInsideSVG(svgDragsHome,dropX,dropY):"DragsSVG"===whichSVG)?(maxY=svgDropZones.height.baseVal.value,svgDropZones.appendChild(selectedElement),selectedElement.getAttribute("data-dropzone-no"),selectedElement.childNodes[1].setAttribute("tabindex","0"),selectedElement.childNodes[2].setAttribute("tabindex","0"),this.centre1.y=maxY-2*this.startRadius,this.y1=maxY-2*this.startRadius,this.centre2.y=maxY-2*this.endRadius,this.y2=maxY-2*this.endRadius,classattributes=(classattributes=selectedElement.getAttribute("class")).replace("inactive","placed"),selectedElement.setAttribute("class",classattributes)):(svgDragsHome.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,classattributes=(classattributes=selectedElement.getAttribute("class")).replace("placed","inactive"),selectedElement.setAttribute("class",classattributes),selectedElement.childNodes[1].setAttribute("tabindex","-1"),selectedElement.childNodes[2].setAttribute("tabindex","-1"))},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],parseInt(linestartbits[0]),parseInt(linestartbits[1]),parseInt(startcoordinates[1]),labels[1],parseInt(lineendbits[0]),parseInt(lineendbits[1]),parseInt(endcoordinates[1]),lineType)},getSimilar:function(lineType,line){return new Line(line.labelstart,parseInt(line.x1),parseInt(line.y1),parseInt(line.startRadius),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 c14e6d4..13d5a36 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 lines and points.\n *\n * These classes can represent Points and line, let you alter them\n * and can give you an SVG representation.\n *\n * @module qtype_drawlines/line\n * @copyright 2024 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 *\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 *\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 these 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;\n this.y1 = y1;\n\n this.x2 = x2;\n this.y2 = y2;\n\n this.centre1 = new Point(x1, y1);\n this.centre2 = new Point(x2, y2);\n\n this.startRadius = startRadius;\n this.endRadius = endRadius;\n\n this.lineType = lineType;\n }\n Line.prototype = new Line();\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 // 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 // If the svg g element is already placed in dropzone, then add the keyboard support.\n var svgClass = svgEl.getAttribute('class');\n if (svgClass && svgClass.includes('placed')) {\n svgEl.childNodes[1].setAttribute('tabindex', '0');\n svgEl.childNodes[2].setAttribute('tabindex', '0');\n }\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 svgEl.childNodes[0].setAttribute('class', 'shape singlearrow');\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 svgEl.childNodes[0].setAttribute('class', 'shape doublearrows');\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 svgEl.childNodes[0].setAttribute('class', 'shape infinite');\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 * Parse the coordinates from the string representation.\n *\n * @param {String} startcoordinates \"x1,y1;radius\".\n * @param {String} endcoordinates \"x1,y1;radius\".\n * @param {float} ratio .\n * @return {boolean} True if the coordinates are valid and parsed. Throws an exception if input point 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} whichHandle which circle handle was moved, i.e., startcircle or endcircle.\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(whichHandle, dx, dy, maxX, maxY) {\n if (whichHandle === 'startcircle') {\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 line end points 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 */\n Line.prototype.moveDrags = function(dx, dy, maxX, maxY, whichSVG) {\n // If the drags are in the dragHomes then we want to keep the x coordinates fixed.\n if (whichSVG === 'DragsSVG') {\n // We don't want to move drags horizontally in this SVG.\n this.centre1.move(0, dy);\n this.centre2.move(0, dy);\n this.centre1.x = 50;\n this.x1 = 50;\n this.centre2.x = 200;\n this.x2 = 200;\n } else {\n this.centre1.move(dx, dy);\n this.centre2.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.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 }\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 * Move the g element between the dropzones and dragHomes.\n *\n * @param {String} eventType Whether it's a mouse event or a keyboard event.\n * @param {SVGElement} selectedElement The element selected for dragging.\n * @param {SVG} svgDropZones\n * @param {SVG} svgDragsHome\n * @param {int|null} dropX Used by mouse events to calculate the svg to which it belongs.\n * @param {int|null} dropY\n * @param {String|null} whichSVG\n */\n Line.prototype.addToDropZone = function(eventType, selectedElement, svgDropZones, svgDragsHome, dropX, dropY, whichSVG) {\n var maxY = 0,\n dropzoneNo = selectedElement.getAttribute('data-dropzone-no'),\n classattributes = '',\n dropZone = false;\n if (eventType === 'mouse') {\n dropZone = this.isInsideSVG(svgDragsHome, dropX, dropY);\n } else {\n dropZone = (whichSVG === 'DragsSVG');\n }\n if (dropZone) {\n // Append the element to the dropzone SVG.\n // Get the height of the dropZone SVG, to decide the position to where to drop the line.\n maxY = svgDropZones.height.baseVal.value;\n svgDropZones.appendChild(selectedElement);\n selectedElement.getAttribute('data-dropzone-no');\n\n // Set tabindex to add keyevents to the circle movehandles.\n selectedElement.childNodes[1].setAttribute('tabindex', '0');\n selectedElement.childNodes[2].setAttribute('tabindex', '0');\n\n // Caluculate the position of line drop.\n this.centre1.y = maxY - (2 * this.startRadius);\n this.y1 = maxY - (2 * this.startRadius);\n this.centre2.y = maxY - (2 * this.endRadius);\n this.y2 = maxY - (2 * this.endRadius);\n\n // Update the class attributes to 'placed' if the line is in the svgDropZone.\n classattributes = selectedElement.getAttribute('class');\n classattributes = classattributes.replace('inactive', 'placed');\n selectedElement.setAttribute('class', classattributes);\n } else {\n // Append the element to the draghomes SVG.\n svgDragsHome.appendChild(selectedElement);\n\n // We want to drop the lines from the top, depending on the line number.\n // Calculate 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 // Update the class attributes to 'inactive' if the line is in the svg draghome.\n classattributes = selectedElement.getAttribute('class');\n classattributes = classattributes.replace('placed', 'inactive');\n selectedElement.setAttribute('class', classattributes);\n // Set tabindex = -1, so the circle movehandles aren't focusable when in draghomes svg.\n selectedElement.childNodes[1].setAttribute('tabindex', '-1');\n selectedElement.childNodes[2].setAttribute('tabindex', '-1');\n }\n return '';\n };\n\n /**\n * Check if the current selected element is in the svg .\n *\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 */\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 var lineEl = createSvgElement(svgEl, tagName);\n lineEl.setAttribute('class', 'shape');\n lineEl.setAttribute('tabindex', '0');\n var startcircleEl = createSvgElement(svgEl, 'circle');\n startcircleEl.setAttribute('class', 'startcircle shape');\n var endcirleEl = createSvgElement(svgEl, 'circle');\n endcirleEl.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 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","lineEl","createSvgShapeGroup","updateSvg","drawLine","childNodes","abs","textContent","parseInt","svgClass","getAttribute","includes","style","stroke","points","newCoordinates","drawInfiniteLine","parentNode","infiniteLine","width","baseVal","value","height","xMin","yMin","xMax","yMax","slope","intercept","startcoordinates","endcoordinates","ratio","startcoordinatesbits","endcoordinatesbits","parseFloat","whichHandle","maxX","maxY","moveDrags","whichSVG","addToDropZone","eventType","selectedElement","svgDropZones","svgDragsHome","dropX","dropY","dropzoneNo","classattributes","isInsideSVG","replace","rect","getBoundingClientRect","left","right","top","bottom","edit","handleIndex","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 lines and points.\n *\n * These classes can represent Points and line, let you alter them\n * and can give you an SVG representation.\n *\n * @module qtype_drawlines/line\n * @copyright 2024 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 *\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 *\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 these 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;\n this.y1 = y1;\n\n this.x2 = x2;\n this.y2 = y2;\n\n this.centre1 = new Point(x1, y1);\n this.centre2 = new Point(x2, y2);\n\n this.startRadius = startRadius;\n this.endRadius = endRadius;\n\n this.lineType = lineType;\n }\n Line.prototype = new Line();\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 // 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 // If the svg g element is already placed in dropzone, then add the keyboard support.\n var svgClass = svgEl.getAttribute('class');\n if (svgClass && svgClass.includes('placed')) {\n svgEl.childNodes[1].setAttribute('tabindex', '0');\n svgEl.childNodes[2].setAttribute('tabindex', '0');\n }\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 svgEl.childNodes[0].setAttribute('class', 'shape singlearrow');\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 svgEl.childNodes[0].setAttribute('class', 'shape doublearrows');\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 svgEl.childNodes[0].setAttribute('class', 'shape infinite');\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 * Parse the coordinates from the string representation.\n *\n * @param {String} startcoordinates \"x1,y1;radius\".\n * @param {String} endcoordinates \"x1,y1;radius\".\n * @param {float} ratio .\n * @return {boolean} True if the coordinates are valid and parsed. Throws an exception if input point 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} whichHandle which circle handle was moved, i.e., startcircle or endcircle.\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(whichHandle, dx, dy, maxX, maxY) {\n if (whichHandle === 'startcircle') {\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 line end points 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 */\n Line.prototype.moveDrags = function(dx, dy, maxX, maxY, whichSVG) {\n // If the drags are in the dragHomes then we want to keep the x coordinates fixed.\n if (whichSVG === 'DragsSVG') {\n // We don't want to move drags horizontally in this SVG.\n this.centre1.move(0, dy);\n this.centre2.move(0, dy);\n this.centre1.x = 50;\n this.x1 = 50;\n this.centre2.x = 200;\n this.x2 = 200;\n } else {\n this.centre1.move(dx, dy);\n this.centre2.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.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 }\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 * Move the g element between the dropzones and dragHomes.\n *\n * @param {String} eventType Whether it's a mouse event or a keyboard event.\n * @param {SVGElement} selectedElement The element selected for dragging.\n * @param {SVG} svgDropZones\n * @param {SVG} svgDragsHome\n * @param {int|null} dropX Used by mouse events to calculate the svg to which it belongs.\n * @param {int|null} dropY\n * @param {String|null} whichSVG\n */\n Line.prototype.addToDropZone = function(eventType, selectedElement, svgDropZones, svgDragsHome, dropX, dropY, whichSVG) {\n var maxY = 0,\n dropzoneNo = selectedElement.getAttribute('data-dropzone-no'),\n classattributes = '',\n dropZone = false;\n if (eventType === 'mouse') {\n dropZone = this.isInsideSVG(svgDragsHome, dropX, dropY);\n } else {\n dropZone = (whichSVG === 'DragsSVG');\n }\n if (dropZone) {\n // Append the element to the dropzone SVG.\n // Get the height of the dropZone SVG, to decide the position to where to drop the line.\n maxY = svgDropZones.height.baseVal.value;\n svgDropZones.appendChild(selectedElement);\n selectedElement.getAttribute('data-dropzone-no');\n\n // Set tabindex to add keyevents to the circle movehandles.\n selectedElement.childNodes[1].setAttribute('tabindex', '0');\n selectedElement.childNodes[2].setAttribute('tabindex', '0');\n\n // Caluculate the position of line drop.\n this.centre1.y = maxY - (2 * this.startRadius);\n this.y1 = maxY - (2 * this.startRadius);\n this.centre2.y = maxY - (2 * this.endRadius);\n this.y2 = maxY - (2 * this.endRadius);\n\n // Update the class attributes to 'placed' if the line is in the svgDropZone.\n classattributes = selectedElement.getAttribute('class');\n classattributes = classattributes.replace('inactive', 'placed');\n selectedElement.setAttribute('class', classattributes);\n } else {\n // Append the element to the draghomes SVG.\n svgDragsHome.appendChild(selectedElement);\n\n // We want to drop the lines from the top, depending on the line number.\n // Calculate 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 // Update the class attributes to 'inactive' if the line is in the svg draghome.\n classattributes = selectedElement.getAttribute('class');\n classattributes = classattributes.replace('placed', 'inactive');\n selectedElement.setAttribute('class', classattributes);\n // Set tabindex = -1, so the circle movehandles aren't focusable when in draghomes svg.\n selectedElement.childNodes[1].setAttribute('tabindex', '-1');\n selectedElement.childNodes[2].setAttribute('tabindex', '-1');\n }\n };\n\n /**\n * Check if the current selected element is in the svg .\n *\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 */\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 svgEl.setAttribute('tabindex', '0');\n var lineEl = createSvgElement(svgEl, tagName);\n lineEl.setAttribute('class', 'shape');\n var startcircleEl = createSvgElement(svgEl, 'circle');\n startcircleEl.setAttribute('class', 'startcircle shape');\n var endcirleEl = createSvgElement(svgEl, 'circle');\n endcirleEl.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], parseInt(linestartbits[0]), parseInt(linestartbits[1]), parseInt(startcoordinates[1]),\n labels[1], parseInt(lineendbits[0]), parseInt(lineendbits[1]), parseInt(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 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","svgClass","getAttribute","includes","style","stroke","points","newCoordinates","drawInfiniteLine","parentNode","infiniteLine","width","baseVal","value","height","xMin","yMin","xMax","yMax","slope","intercept","startcoordinates","endcoordinates","ratio","startcoordinatesbits","endcoordinatesbits","parseFloat","whichHandle","maxX","maxY","moveDrags","whichSVG","addToDropZone","eventType","selectedElement","svgDropZones","svgDragsHome","dropX","dropY","dropzoneNo","classattributes","isInsideSVG","replace","rect","getBoundingClientRect","left","right","top","bottom","edit","handleIndex","limit","min","getHandlePositions","moveHandles","editHandles","normalizeShape","make","linecoordinates","labels","linestartbits","lineendbits","getSimilar","line"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/question.min.js b/amd/build/question.min.js index c74a3ab..9ab4bff 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,Line){function DrawlinesQuestion(containerId,readOnly,visibleDropZones,questionLines){var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.questionLines=questionLines,this.lineSVGs=[],this.lines=[],this.svgEl=null,readOnly&&this.getRoot().classList.add("qtype_drawlines-readonly"),thisQ.allImagesLoaded=!1;const images=thisQ.getNotYetLoadedImages();images&&images.forEach((imgNode=>{imgNode.addEventListener("load",(function(){thisQ.waitForAllImagesToBeLoaded()}),{once:!0})})),thisQ.waitForAllImagesToBeLoaded()}DrawlinesQuestion.prototype.updateCoordinates=function(){for(var line=0;line');this.drawSVGLines(this.questionLines)},DrawlinesQuestion.prototype.drawSVGLines=function(questionLines){var height,startcoordinates,endcoordinates,draginitialcoords,bgImage=this.bgImage();this.getRoot().querySelector(".draghomes").innerHTML='';var draghomeSvg=this.getRoot().querySelector(".dragshome"),dropzoneSvg=this.getRoot().querySelector(".dropzones");for(let line=0;linebgImage.height-20,(isMoveFromDragsToDropzones||isMoveFromDropzonesToDrags)&&movingDrag.lines[dropzoneNo].addToDropZone("mouse",selectedElement,closestSVGs.svgDropZone,closestSVGs.svgDragsHome,dropX,dropY),closeTo=selectedElement.closest("svg");var dimensions=movingDrag.getSvgDimensionsByClass(closeTo,closeTo.getAttribute("class"));maxX=dimensions.maxX,maxY=dimensions.maxY,whichSVG=dimensions.whichSVG,movingDrag.lines[dropzoneNo].moveDrags(parseInt(pageX)-parseInt(lastX),parseInt(pageY)-parseInt(lastY),parseInt(maxX),parseInt(maxY),whichSVG),lastX=pageX,lastY=pageY,movingDrag.updateSvgEl(dropzoneNo),movingDrag.saveCoordsForChoice(dropzoneNo)}),(function(){document.body.removeChild(dragProxy)}))},DrawlinesQuestion.prototype.makeDragProxy=function(x,y){var dragProxy=document.createElement("div");return 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),dragProxy},DrawlinesQuestion.prototype.saveCoordsForChoice=function(choiceNo){let imageCoords=[];var items=this.getRoot().querySelector("svg g.choice"+choiceNo),gEleClassAttributes="";items&&(imageCoords=items.querySelector("polyline").getAttribute("points"),gEleClassAttributes=items.getAttribute("class")),""!==gEleClassAttributes&&gEleClassAttributes.includes("placed")?this.getRoot().querySelector("input.choice"+choiceNo).value=imageCoords:""!==gEleClassAttributes&&gEleClassAttributes.includes("inactive")&&(this.getRoot().querySelector("input.choice"+choiceNo).value="")},DrawlinesQuestion.prototype.handleKeyPress=function(e,drag,dropzoneNo,activeElement){var dropzoneElement,x=0,y=0,question=questionManager.getQuestionForEvent(e);switch(dropzoneElement=event.target.closest("g"),e.code){case"ArrowLeft":case"KeyA":x=-1;break;case"ArrowRight":case"KeyD":x=1;break;case"ArrowDown":case"KeyS":y=1;break;case"ArrowUp":case"KeyW":y=-1;break;case"Space":case"Escape":break;default:return}e.preventDefault();var maxX,maxY,whichSVG,closeTo=drag.closest("svg"),svgClass=closeTo.getAttribute("class"),bgImage=this.bgImage(),closestSVGs=this.getSvgsClosestToElement(drag),isMoveFromDragsToDropzones="dragshome"===svgClass,isMoveFromDropzonesToDrags="dropzones"===svgClass&&question.lines[dropzoneNo].centre1.y>bgImage.height-20;isMoveFromDragsToDropzones?question.lines[dropzoneNo].addToDropZone("keyboard",dropzoneElement,closestSVGs.svgDropZone,closestSVGs.svgDragsHome,null,null,"DragsSVG"):isMoveFromDropzonesToDrags&&question.lines[dropzoneNo].addToDropZone("keyboard",dropzoneElement,closestSVGs.svgDropZone,closestSVGs.svgDragsHome,null,null,"DropZonesSVG"),closeTo=drag.closest("svg");var dimensions=question.getSvgDimensionsByClass(closeTo,closeTo.getAttribute("class"));maxX=dimensions.maxX,maxY=dimensions.maxY,whichSVG=dimensions.whichSVG,"line"===activeElement?question.lines[dropzoneNo].moveDrags(x,y,parseInt(maxX),parseInt(maxY),whichSVG):question.lines[dropzoneNo].move(activeElement,x,y,parseInt(maxX),parseInt(maxY)),question.updateSvgEl(dropzoneNo),this.saveCoordsForChoice(dropzoneNo),drag.focus()},DrawlinesQuestion.prototype.getSvgDimensionsByClass=function(dragSVG,className){return{maxX:dragSVG.width.baseVal.value,maxY:dragSVG.height.baseVal.value,whichSVG:"dragshome"===className?"DragsSVG":"DropZonesSVG"}},DrawlinesQuestion.prototype.getSvgsClosestToElement=function(dragElement){var svgDragsHome,svgDropZone,svgElement=dragElement.closest("svg");return"dragshome"===svgElement.getAttribute("class")?(svgDragsHome=svgElement,svgDropZone=svgElement.closest(".ddarea").querySelector(".dropzones")):(svgDropZone=svgElement,svgDragsHome=svgElement.closest(".ddarea").querySelector(".dragshome")),{svgDropZone:svgDropZone,svgDragsHome:svgDragsHome}},DrawlinesQuestion.prototype.waitForAllImagesToBeLoaded=function(){if(this.allImagesLoaded)return;null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId);const images=this.getNotYetLoadedImages();images&&images.length>0?this.imageLoadingTimeoutId=setTimeout((function(){this.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,this.drawDropzone())},DrawlinesQuestion.prototype.getNotYetLoadedImages=function(){const images=this.getRoot().querySelectorAll(".drawlines img.dropbackground");Array.from(images).filter((imgNode=>!this.imageIsLoaded(imgNode)))},DrawlinesQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,lineEventHandlersInitialised:{},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.lineEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.lineEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);if(questionContainer.classList.contains("drawlines")&&!questionContainer.classList.contains("qtype_drawlines-readonly")){var dropArea=questionContainer.querySelector(".droparea");dropArea.addEventListener("mousedown",questionManager.handleDropZoneEventMove),dropArea.addEventListener("touchstart",questionManager.handleDropZoneEventMove),dropArea.addEventListener("keydown",questionManager.handleKeyPress),dropArea.addEventListener("keypress",questionManager.handleKeyPress);var drags=questionContainer.querySelector(".draghomes");drags.addEventListener("mousedown",questionManager.handleDragHomeEventMove),drags.addEventListener("touchstart",questionManager.handleDragHomeEventMove),drags.addEventListener("keydown",questionManager.handleKeyPress),drags.addEventListener("keypress",questionManager.handleKeyPress)}}},handleDropZoneEventMove:function(event){var dropzoneNo,question=questionManager.getQuestionForEvent(event);event.target.closest(".dropzone .startcircle.shape")?(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleCircleMove(event,"startcircle",dropzoneNo)):event.target.closest(".dropzone .endcircle.shape")?(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleCircleMove(event,"endcircle",dropzoneNo)):event.target.closest("polyline.shape")&&(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleLineMove(event,dropzoneNo))},handleDragHomeEventMove:function(event){var dropzoneNo,question=questionManager.getQuestionForEvent(event);event.target.closest("g")&&(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleLineMove(event,dropzoneNo),question.saveCoordsForChoice(dropzoneNo))},handleKeyPress:function(e){var dropzoneElement,dropzoneNo,drag,activeElement,question=questionManager.getQuestionForEvent(e);e.target.closest(".dropzone circle.startcircle")?(dropzoneNo=(dropzoneElement=e.target.closest(".dropzone")).dataset.dropzoneNo,drag=e.target.closest(".dropzone circle.startcircle"),activeElement="startcircle"):e.target.closest(".dropzone circle.endcircle")?(drag=e.target.closest(".dropzone circle.endcircle"),dropzoneNo=(dropzoneElement=e.target.closest(".dropzone")).dataset.dropzoneNo,activeElement="endcircle"):e.target.closest(".dropzone polyline.shape")&&(drag=e.target.closest(".dropzone polyline.shape"),dropzoneNo=(dropzoneElement=e.target.closest(".dropzone")).dataset.dropzoneNo,activeElement="line"),question&&dropzoneElement&&question.handleKeyPress(e,drag,dropzoneNo,activeElement)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.drawlines").attr("id");return questionManager.questions[containerId]}};return{init:questionManager.init}})); +define("qtype_drawlines/question",["jquery","core/dragdrop","qtype_drawlines/line","core/key_codes","core_form/changechecker"],(function($,dragDrop,Line){function DrawlinesQuestion(containerId,readOnly,visibleDropZones,questionLines){var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.questionLines=questionLines,this.lineSVGs=[],this.lines=[],this.svgEl=null,readOnly&&this.getRoot().classList.add("qtype_drawlines-readonly"),thisQ.allImagesLoaded=!1,thisQ.waitForAllImagesToBeLoaded().then((()=>{thisQ.drawDropzone()})).catch((error=>{throw error}))}DrawlinesQuestion.prototype.updateCoordinates=function(){for(var line=0;line');this.drawSVGLines(this.questionLines)},DrawlinesQuestion.prototype.drawSVGLines=function(questionLines){var height,startcoordinates,endcoordinates,draginitialcoords,bgImage=this.bgImage();this.getRoot().querySelector(".draghomes").innerHTML='';var draghomeSvg=this.getRoot().querySelector(".dragshome"),dropzoneSvg=this.getRoot().querySelector(".dropzones");for(let line=0;linebgImage.height-20,(isMoveFromDragsToDropzones||isMoveFromDropzonesToDrags)&&movingDrag.lines[dropzoneNo].addToDropZone("mouse",selectedElement,closestSVGs.svgDropZone,closestSVGs.svgDragsHome,dropX,dropY),closeTo=selectedElement.closest("svg");var dimensions=movingDrag.getSvgDimensionsByClass(closeTo,closeTo.getAttribute("class"));maxX=dimensions.maxX,maxY=dimensions.maxY,whichSVG=dimensions.whichSVG,movingDrag.lines[dropzoneNo].moveDrags(parseInt(pageX)-parseInt(lastX),parseInt(pageY)-parseInt(lastY),parseInt(maxX),parseInt(maxY),whichSVG),lastX=pageX,lastY=pageY,movingDrag.updateSvgEl(dropzoneNo),movingDrag.saveCoordsForChoice(dropzoneNo)}),(function(){document.body.removeChild(dragProxy)}))},DrawlinesQuestion.prototype.makeDragProxy=function(x,y){var dragProxy=document.createElement("div");return 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),dragProxy},DrawlinesQuestion.prototype.saveCoordsForChoice=function(choiceNo){let imageCoords=[];var items=this.getRoot().querySelector("svg g.choice"+choiceNo),gEleClassAttributes="";items&&(imageCoords=items.querySelector("polyline").getAttribute("points"),gEleClassAttributes=items.getAttribute("class")),""!==gEleClassAttributes&&gEleClassAttributes.includes("placed")?this.getRoot().querySelector("input.choice"+choiceNo).value=imageCoords:""!==gEleClassAttributes&&gEleClassAttributes.includes("inactive")&&(this.getRoot().querySelector("input.choice"+choiceNo).value="")},DrawlinesQuestion.prototype.handleKeyPress=function(e,drag,dropzoneNo,activeElement){var dropzoneElement,x=0,y=0,question=questionManager.getQuestionForEvent(e);switch(dropzoneElement=drag.closest("g"),e.code){case"ArrowLeft":case"KeyA":x=-1;break;case"ArrowRight":case"KeyD":x=1;break;case"ArrowDown":case"KeyS":y=1;break;case"ArrowUp":case"KeyW":y=-1;break;case"Space":case"Escape":break;default:return}e.preventDefault();var maxX,maxY,whichSVG,closeTo=drag.closest("svg"),svgClass=closeTo.getAttribute("class"),bgImage=this.bgImage(),closestSVGs=this.getSvgsClosestToElement(drag),isMoveFromDragsToDropzones="dragshome"===svgClass,isMoveFromDropzonesToDrags="dropzones"===svgClass&&question.lines[dropzoneNo].centre1.y>bgImage.height-20;isMoveFromDragsToDropzones?question.lines[dropzoneNo].addToDropZone("keyboard",dropzoneElement,closestSVGs.svgDropZone,closestSVGs.svgDragsHome,null,null,"DragsSVG"):isMoveFromDropzonesToDrags&&question.lines[dropzoneNo].addToDropZone("keyboard",dropzoneElement,closestSVGs.svgDropZone,closestSVGs.svgDragsHome,null,null,"DropZonesSVG"),closeTo=drag.closest("svg");var dimensions=question.getSvgDimensionsByClass(closeTo,closeTo.getAttribute("class"));maxX=dimensions.maxX,maxY=dimensions.maxY,whichSVG=dimensions.whichSVG,"line"===activeElement?question.lines[dropzoneNo].moveDrags(parseInt(x),parseInt(y),parseInt(maxX),parseInt(maxY),whichSVG):question.lines[dropzoneNo].move(activeElement,parseInt(x),parseInt(y),parseInt(maxX),parseInt(maxY)),question.updateSvgEl(dropzoneNo),this.saveCoordsForChoice(dropzoneNo),drag.focus()},DrawlinesQuestion.prototype.getSvgDimensionsByClass=function(dragSVG,className){return{maxX:dragSVG.width.baseVal.value,maxY:dragSVG.height.baseVal.value,whichSVG:"dragshome"===className?"DragsSVG":"DropZonesSVG"}},DrawlinesQuestion.prototype.getSvgsClosestToElement=function(dragElement){var svgDragsHome,svgDropZone,svgElement=dragElement.closest("svg");return"dragshome"===svgElement.getAttribute("class")?(svgDragsHome=svgElement,svgDropZone=svgElement.closest(".ddarea").querySelector(".dropzones")):(svgDropZone=svgElement,svgDragsHome=svgElement.closest(".ddarea").querySelector(".dragshome")),{svgDropZone:svgDropZone,svgDragsHome:svgDragsHome}},DrawlinesQuestion.prototype.waitForAllImagesToBeLoaded=function(){if(this.allImagesLoaded)return;const images=document.querySelectorAll("img"),promises=Array.from(images).map((img=>new Promise(((resolve,reject)=>{img.complete?resolve():(img.onload=()=>resolve(),img.onerror=()=>reject(new Error(`Failed to load image: ${img.src}`)))}))));return this.allImagesLoaded=!0,Promise.all(promises)},DrawlinesQuestion.prototype.getNotYetLoadedImages=function(){const images=this.getRoot().querySelectorAll(".drawlines img.dropbackground");Array.from(images).filter((imgNode=>!this.imageIsLoaded(imgNode)))},DrawlinesQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,lineEventHandlersInitialised:{},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.lineEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.lineEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);if(questionContainer.classList.contains("drawlines")&&!questionContainer.classList.contains("qtype_drawlines-readonly")){var dropArea=questionContainer.querySelector(".droparea");dropArea.addEventListener("mousedown",questionManager.handleDropZoneEventMove),dropArea.addEventListener("touchstart",questionManager.handleDropZoneEventMove),dropArea.addEventListener("keydown",questionManager.handleKeyPress),dropArea.addEventListener("keypress",questionManager.handleKeyPress);var drags=questionContainer.querySelector(".draghomes");drags.addEventListener("mousedown",questionManager.handleDragHomeEventMove),drags.addEventListener("touchstart",questionManager.handleDragHomeEventMove),drags.addEventListener("keydown",questionManager.handleKeyPress),drags.addEventListener("keypress",questionManager.handleKeyPress)}}},handleDropZoneEventMove:function(event){var dropzoneNo,question=questionManager.getQuestionForEvent(event);event.target.closest(".dropzone .startcircle.shape")?(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleCircleMove(event,"startcircle",dropzoneNo)):event.target.closest(".dropzone .endcircle.shape")?(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleCircleMove(event,"endcircle",dropzoneNo)):event.target.closest("polyline.shape")&&(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleLineMove(event,dropzoneNo))},handleDragHomeEventMove:function(event){var dropzoneNo,question=questionManager.getQuestionForEvent(event);event.target.closest("g")&&(dropzoneNo=event.target.closest("g").dataset.dropzoneNo,question.handleLineMove(event,dropzoneNo),question.saveCoordsForChoice(dropzoneNo))},handleKeyPress:function(e){var dropzoneElement,dropzoneNo,drag,activeElement,question=questionManager.getQuestionForEvent(e);e.target.closest(".dropzone circle.startcircle")?(dropzoneNo=(dropzoneElement=e.target.closest(".dropzone")).dataset.dropzoneNo,drag=e.target.closest(".dropzone circle.startcircle"),activeElement="startcircle"):e.target.closest(".dropzone circle.endcircle")?(drag=e.target.closest(".dropzone circle.endcircle"),dropzoneNo=(dropzoneElement=e.target.closest(".dropzone")).dataset.dropzoneNo,activeElement="endcircle"):e.target.closest("g.dropzone")&&(drag=e.target.closest("g.dropzone"),dropzoneNo=(dropzoneElement=e.target.closest(".dropzone")).dataset.dropzoneNo,activeElement="line"),question&&dropzoneElement&&question.handleKeyPress(e,drag,dropzoneNo,activeElement)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.drawlines").attr("id");return questionManager.questions[containerId]}};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 576f8c5..83928ab 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 for lines (using mouse down or touch) or tab through lines 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 Line,\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.svgEl = null;\n if (readOnly) {\n this.getRoot().classList.add('qtype_drawlines-readonly');\n }\n thisQ.allImagesLoaded = false;\n // Get all images that are not yet loaded\n const images = thisQ.getNotYetLoadedImages();\n\n // Loop over each image and add an event listener for the 'load' event\n if (images) {\n images.forEach((imgNode) => {\n imgNode.addEventListener('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n }, {once: true}); // The { once: true } option ensures the listener is called only once\n });\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.getSVGLineCoordinates(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 * Parse the coordinates from a particular string.\n *\n * @param {String} coordinates The coordinates to be parsed. The values are in the format: x1,y1 x2,y2.\n * Except for infinite line type where it's in the format x1,y1 x2,y2, x3,y3, x4,y4.\n * Here, x1,y1 and x4,y4 are the two very end points of the infinite line and\n * x2,y2 and x3,y3 are the pints with the handles.\n * @param {String} lineType The type of the line.\n */\n DrawlinesQuestion.prototype.parseCoordinates = function(coordinates, lineType) {\n var bits = coordinates.split(' ');\n if (lineType === 'lineinfinite' && bits.length !== 2) {\n // Remove the first and last coordinates.\n bits = bits.slice(1, -1);\n }\n if (bits.length !== 2) {\n throw new Error(coordinates + ' is not a valid point');\n }\n return bits;\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 = this.bgImage();\n var svg = this.getRoot().querySelector('svg.dropzones');\n var rootElement = this.getRoot();\n rootElement.querySelector('.que-dlines-dropzone').style.position = 'relative';\n rootElement.querySelector('.que-dlines-dropzone').style.top = (bgImage.height + 1) * -1 + \"px\";\n rootElement.querySelector('.que-dlines-dropzone').style.height = bgImage.height + \"px\";\n rootElement.querySelector('.droparea').style.height = bgImage.height + \"px\";\n if (!svg) {\n var dropZone = this.getRoot().querySelector('.que-dlines-dropzone');\n dropZone.innerHTML =\n '';\n }\n this.drawSVGLines(this.questionLines);\n };\n\n /**\n * Draws the svg lines of any drop zones.\n *\n * @param {Object[]} questionLines\n */\n DrawlinesQuestion.prototype.drawSVGLines = function(questionLines) {\n var bgImage = this.bgImage(),\n height, startcoordinates, endcoordinates, draginitialcoords;\n\n var drags = this.getRoot().querySelector('.draghomes');\n drags.innerHTML =\n '';\n\n var draghomeSvg = this.getRoot().querySelector('.dragshome');\n var dropzoneSvg = this.getRoot().querySelector('.dropzones');\n var initialHeight = 25;\n for (let line = 0; line < questionLines.length; line++) {\n height = initialHeight + line * 50;\n startcoordinates = '50,' + height + ';10';\n endcoordinates = '200,' + height + ';10';\n\n // Check if the lines are to be set with initial coordinates.\n draginitialcoords = this.visibleDropZones['c' + line];\n if (draginitialcoords !== undefined && draginitialcoords !== '') {\n // The visibleDropZones array holds the response in the format x1,y1 x2,y2 - to be added to svgdropzone.\n var coords = this.parseCoordinates(draginitialcoords, questionLines[line].type);\n startcoordinates = coords[0] + ';10';\n endcoordinates = coords[1] + ';10';\n this.lines[line] = Line.make(\n [startcoordinates, endcoordinates],\n [questionLines[line].labelstart, questionLines[line].labelend],\n questionLines[line].type\n );\n this.addToSvg(line, dropzoneSvg);\n } else {\n // Need to be added to draghomeSvg.\n this.lines[line] = Line.make(\n [startcoordinates, endcoordinates],\n [questionLines[line].labelstart, questionLines[line].labelend],\n questionLines[line].type\n );\n this.addToSvg(line, draghomeSvg);\n }\n }\n };\n\n // TODO: The below methods can be refractored for window resizing.\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 = Line.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.Line.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 // * 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] = Line.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 // * 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 Line.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 Line.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 // * 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 // /**\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 // * 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 Line.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 // * 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 // * 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 * Get the outer div for this question.\n *\n * @return {*}\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 *\n * @returns {element|undefined} the DOM element (if any)\n */\n DrawlinesQuestion.prototype.bgImage = function() {\n return this.getRoot().querySelector('img.dropbackground');\n };\n\n /**\n * Returns the coordinates for the line from the SVG.\n * @param {SVGElement} svgEl\n * @returns {Array} the coordinates.\n */\n DrawlinesQuestion.prototype.getSVGLineCoordinates = 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 * 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('data-dropzone-no', lineNumber);\n if (svg.getAttribute('class') === 'dropzones') {\n this.lineSVGs[lineNumber].setAttribute('class', 'dropzone choice' + lineNumber + ' placed');\n } else {\n this.lineSVGs[lineNumber].setAttribute('class', 'dropzone choice' + lineNumber + ' inactive');\n }\n };\n\n /**\n * Update the line of this drop zone in an SVG image.\n *\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 attached to the line ends (circles).\n *\n * @param {Event} e Event object\n * @param {String} whichHandle which circle handle was moved, i.e., startcircle or endcircle.\n * @param {int} dropzoneNo\n */\n DrawlinesQuestion.prototype.handleCircleMove = function(e, whichHandle, 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 = this.getRoot().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(whichHandle,\n parseInt(pageX) - parseInt(lastX), parseInt(pageY) - parseInt(lastY), parseInt(maxX), parseInt(maxY));\n lastX = pageX;\n lastY = pageY;\n movingDropZone.updateSvgEl(dropzoneNo);\n movingDropZone.saveCoordsForChoice(dropzoneNo);\n }, function() {\n document.body.removeChild(dragProxy);\n });\n };\n\n /**\n * Start responding to dragging the move handle attached to the line.\n *\n * @param {Event} e Event object\n * @param {int} dropzoneNo\n */\n DrawlinesQuestion.prototype.handleLineMove = 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 maxX,\n maxY,\n whichSVG = \"\",\n bgImage = this.bgImage(),\n isMoveFromDragsToDropzones,\n isMoveFromDropzonesToDrags,\n svgClass = '';\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\n // The svg's which are associated with this question.\n var closestSVGs = movingDrag.getSvgsClosestToElement(selectedElement);\n\n // Check if the drags need to be moved from one svg to another.\n var closeTo = selectedElement.closest('svg');\n svgClass = closeTo.getAttribute('class');\n\n // Moving the drags between the SVG's.\n // If true, the drag is moved from draghomes SVG to dropZone SVG.\n isMoveFromDragsToDropzones = (svgClass === \"dragshome\");\n\n // If true, the drag is moved from dropZone SVG to draghomes SVG.\n isMoveFromDropzonesToDrags = (svgClass === 'dropzones') &&\n (movingDrag.lines[dropzoneNo].centre1.y > (bgImage.height - 20));\n\n if (isMoveFromDragsToDropzones || isMoveFromDropzonesToDrags) {\n movingDrag.lines[dropzoneNo].addToDropZone('mouse', selectedElement,\n closestSVGs.svgDropZone, closestSVGs.svgDragsHome, dropX, dropY);\n }\n\n // Drag the lines within the SVG\n // Get the dimensions of the selected element's svg.\n closeTo = selectedElement.closest('svg');\n var dimensions = movingDrag.getSvgDimensionsByClass(closeTo, closeTo.getAttribute('class'));\n maxX = dimensions.maxX;\n maxY = dimensions.maxY;\n whichSVG = dimensions.whichSVG;\n\n movingDrag.lines[dropzoneNo].moveDrags(\n parseInt(pageX) - parseInt(lastX), parseInt(pageY) - parseInt(lastY),\n parseInt(maxX), parseInt(maxY), whichSVG);\n lastX = pageX;\n lastY = pageY;\n\n movingDrag.updateSvgEl(dropzoneNo);\n movingDrag.saveCoordsForChoice(dropzoneNo);\n }, function() {\n document.body.removeChild(dragProxy);\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 *\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().querySelector('svg g.choice' + choiceNo),\n gEleClassAttributes = '';\n if (items) {\n imageCoords = items.querySelector('polyline').getAttribute('points');\n gEleClassAttributes = items.getAttribute('class');\n // TODO: Kept the below comment as this could be needed for window resizing.\n\n // thiQ = this,\n // bgRatio = this.bgRatio();\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 Line.Point(drag.data('pagex'), drag.data('pagey'));\n // window.console.log(\"dragXY:\" + dragXY);\n //\n // window.console.log(\"thiQ:\" + thiQ);\n // if (thiQ.coordsInBgImg(dragXY)) {\n // var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n // bgImgXY = new Line.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n // imageCoords[imageCoords.length] = bgImgXY;\n // window.console.log(\"bgImgXY:\" + bgImgXY);\n // }\n // } else if (drag.data('imageCoords')) {\n // imageCoords[imageCoords.length] = drag.data('imageCoords');\n // }\n\n }\n if (gEleClassAttributes !== '' && gEleClassAttributes.includes('placed')) {\n this.getRoot().querySelector('input.choice' + choiceNo).value = imageCoords;\n } else if (gEleClassAttributes !== '' && gEleClassAttributes.includes('inactive')) {\n this.getRoot().querySelector('input.choice' + choiceNo).value = '';\n }\n };\n\n /**\n * Handle key down / press events on svg lines.\n * @param {KeyboardEvent} e\n * @param {SVGElement} drag SVG element being dragged.\n * @param {int} dropzoneNo\n * @param {String} activeElement The element being dragged, whether it is the line or the line endpoints.\n */\n DrawlinesQuestion.prototype.handleKeyPress = function(e, drag, dropzoneNo, activeElement) {\n\n var x = 0,\n y = 0,\n dropzoneElement,\n question = questionManager.getQuestionForEvent(e);\n\n dropzoneElement = event.target.closest('g');\n\n switch (e.code) {\n case 'ArrowLeft':\n case 'KeyA': // A.\n x = -1;\n break;\n case 'ArrowRight':\n case 'KeyD': // D.\n x = 1;\n break;\n case 'ArrowDown':\n case 'KeyS': // S.\n y = 1;\n break;\n case 'ArrowUp':\n case 'KeyW': // W.\n y = -1;\n break;\n case 'Space':\n case 'Escape':\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n // Moving the drags between the SVG's.\n var closeTo = drag.closest('svg');\n var svgClass = closeTo.getAttribute('class');\n var maxX,\n maxY,\n whichSVG;\n var bgImage = this.bgImage();\n var closestSVGs = this.getSvgsClosestToElement(drag);\n var isMoveFromDragsToDropzones = (svgClass === \"dragshome\");\n var isMoveFromDropzonesToDrags = (svgClass === 'dropzones') &&\n (question.lines[dropzoneNo].centre1.y > (bgImage.height - 20));\n\n if (isMoveFromDragsToDropzones) {\n question.lines[dropzoneNo].addToDropZone('keyboard', dropzoneElement,\n closestSVGs.svgDropZone, closestSVGs.svgDragsHome, null, null, 'DragsSVG');\n } else if (isMoveFromDropzonesToDrags) {\n question.lines[dropzoneNo].addToDropZone('keyboard', dropzoneElement,\n closestSVGs.svgDropZone, closestSVGs.svgDragsHome, null, null, 'DropZonesSVG');\n }\n\n // Get the dimensions of the selected element's svg.\n closeTo = drag.closest('svg');\n var dimensions = question.getSvgDimensionsByClass(closeTo, closeTo.getAttribute('class'));\n maxX = dimensions.maxX;\n maxY = dimensions.maxY;\n whichSVG = dimensions.whichSVG;\n\n if (activeElement === 'line') {\n // Move the entire line when the focus is on it.\n question.lines[dropzoneNo].moveDrags(x, y, parseInt(maxX), parseInt(maxY), whichSVG);\n } else {\n // Move the line endpoints.\n question.lines[dropzoneNo].move(activeElement, x, y, parseInt(maxX), parseInt(maxY));\n }\n question.updateSvgEl(dropzoneNo);\n this.saveCoordsForChoice(dropzoneNo);\n drag.focus();\n };\n\n /**\n * Returns the dimensions of the SVG image to which the drag element belongs.\n *\n * @param {SVG} dragSVG The SVG to which the drag element belongs.\n * @param {String} className Class asscociated with the SVG\n * @return {{whichSVG: (string), maxY: number, maxX: number}}\n */\n DrawlinesQuestion.prototype.getSvgDimensionsByClass = function(dragSVG, className) {\n return {\n maxX: dragSVG.width.baseVal.value,\n maxY: dragSVG.height.baseVal.value,\n whichSVG: className === 'dragshome' ? 'DragsSVG' : 'DropZonesSVG'\n };\n };\n\n /**\n * Returns the SVG's to which the drag element belongs.\n *\n * @param {SVGElement} dragElement The element which is being moved.\n * @return {{svgDragsHome, svgDropZone}}\n */\n DrawlinesQuestion.prototype.getSvgsClosestToElement = function(dragElement) {\n var svgElement = dragElement.closest('svg');\n var svgElementClass = svgElement.getAttribute('class');\n var svgDragsHome, svgDropZone, parent;\n if (svgElementClass === \"dragshome\") {\n svgDragsHome = svgElement;\n parent = svgElement.closest('.ddarea');\n svgDropZone = parent.querySelector('.dropzones');\n } else {\n svgDropZone = svgElement;\n parent = svgElement.closest('.ddarea');\n svgDragsHome = parent.querySelector('.dragshome');\n }\n return {\n svgDropZone: svgDropZone,\n svgDragsHome: svgDragsHome\n };\n };\n\n /**\n * Waits until all images are loaded before setting up question.\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 const images = this.getNotYetLoadedImages();\n if (images && images.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.drawDropzone();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {boolean} Returns true if images are loaded without errors.\n */\n DrawlinesQuestion.prototype.getNotYetLoadedImages = function() {\n // Get all 'img' elements with the class 'dropbackground' within '.drawlines' inside the root element\n const images = this.getRoot().querySelectorAll('.drawlines img.dropbackground');\n\n // Filter out the images that are already loaded\n Array.from(images).filter((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 DrawlinesQuestions 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 lineEventHandlersInitialised: {},\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.lineEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.lineEventHandlersInitialised[containerId] = true;\n\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('drawlines') &&\n !questionContainer.classList.contains('qtype_drawlines-readonly')) {\n\n // Add event listeners to the 'previewArea'.\n // For dropzone SVG.\n var dropArea = questionContainer.querySelector('.droparea');\n // Add event listener for mousedown and touchstart events.\n dropArea.addEventListener('mousedown', questionManager.handleDropZoneEventMove);\n dropArea.addEventListener('touchstart', questionManager.handleDropZoneEventMove);\n // Add event listener for keydown and keypress events.\n dropArea.addEventListener('keydown', questionManager.handleKeyPress);\n dropArea.addEventListener('keypress', questionManager.handleKeyPress);\n\n // For draghomes SVG.\n var drags = questionContainer.querySelector('.draghomes');\n // Add event listener for mousedown and touchstart events.\n drags.addEventListener('mousedown', questionManager.handleDragHomeEventMove);\n drags.addEventListener('touchstart', questionManager.handleDragHomeEventMove);\n // Add event listener for keydown and keypress events.\n drags.addEventListener('keydown', questionManager.handleKeyPress);\n drags.addEventListener('keypress', questionManager.handleKeyPress);\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 /**\n * Handle mouse and touch events for dropzone svg.\n *\n * @param {Event} event\n */\n handleDropZoneEventMove: function(event) {\n var dropzoneElement, dropzoneNo;\n var question = questionManager.getQuestionForEvent(event);\n if (event.target.closest('.dropzone .startcircle.shape')) {\n // Dragging the move handle circle attached to the start of the line.\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleCircleMove(event, 'startcircle', dropzoneNo);\n } else if (event.target.closest('.dropzone .endcircle.shape')) {\n // Dragging the move handle circle attached to the end of the line.\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleCircleMove(event, 'endcircle', dropzoneNo);\n } else if (event.target.closest('polyline.shape')) {\n // Dragging the entire line.\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleLineMove(event, dropzoneNo);\n }\n },\n\n /**\n * Handle mouse and touch events for dragshome svg.\n *\n * @param {Event} event\n */\n handleDragHomeEventMove: function(event) {\n var dropzoneElement, dropzoneNo;\n var question = questionManager.getQuestionForEvent(event);\n if (event.target.closest('g')) {\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleLineMove(event, dropzoneNo);\n question.saveCoordsForChoice(dropzoneNo);\n }\n },\n\n /**\n * Handle key down / press events on markers.\n *\n * @param {Event} e\n */\n handleKeyPress: function(e) {\n var question = questionManager.getQuestionForEvent(e);\n var dropzoneElement, dropzoneNo, drag, activeElement;\n if (e.target.closest('.dropzone circle.startcircle')) {\n dropzoneElement = e.target.closest('.dropzone');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n drag = e.target.closest('.dropzone circle.startcircle');\n activeElement = 'startcircle';\n } else if (e.target.closest('.dropzone circle.endcircle')) {\n drag = e.target.closest('.dropzone circle.endcircle');\n dropzoneElement = e.target.closest('.dropzone');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n activeElement = 'endcircle';\n } else if (e.target.closest('.dropzone polyline.shape')) {\n drag = e.target.closest('.dropzone polyline.shape');\n dropzoneElement = e.target.closest('.dropzone');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n activeElement = 'line';\n }\n if (question && dropzoneElement) {\n question.handleKeyPress(e, drag, dropzoneNo, activeElement);\n }\n },\n\n /**\n * Handle when the window is resized.\n *\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 * Given an event, work out which question it effects.\n *\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 /**\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});\n"],"names":["define","$","dragDrop","Line","DrawlinesQuestion","containerId","readOnly","visibleDropZones","questionLines","thisQ","this","lineSVGs","lines","svgEl","getRoot","classList","add","allImagesLoaded","images","getNotYetLoadedImages","forEach","imgNode","addEventListener","waitForAllImagesToBeLoaded","once","prototype","updateCoordinates","line","length","coordinates","getSVGLineCoordinates","parse","updateSvgEl","parseCoordinates","lineType","bits","split","slice","Error","drawDropzone","bgImage","svg","querySelector","rootElement","style","position","top","height","innerHTML","width","drawSVGLines","startcoordinates","endcoordinates","draginitialcoords","draghomeSvg","dropzoneSvg","undefined","coords","type","make","labelstart","labelend","addToSvg","document","getElementById","childNodes","getAttribute","bgRatio","bgImg","bgImgNaturalWidth","get","naturalWidth","lineNumber","makeSvg","setAttribute","dropzoneNo","updateSvg","handleCircleMove","e","whichHandle","info","prepare","start","movingDropZone","lastX","x","lastY","y","dragProxy","makeDragProxy","maxX","baseVal","value","maxY","pageX","pageY","move","parseInt","saveCoordsForChoice","body","removeChild","handleLineMove","isMoveFromDragsToDropzones","isMoveFromDropzonesToDrags","movingDrag","whichSVG","svgClass","selectedElement","dropX","clientX","dropY","clientY","closestSVGs","getSvgsClosestToElement","closeTo","closest","centre1","addToDropZone","svgDropZone","svgDragsHome","dimensions","getSvgDimensionsByClass","moveDrags","createElement","left","appendChild","choiceNo","imageCoords","items","gEleClassAttributes","includes","handleKeyPress","drag","activeElement","dropzoneElement","question","questionManager","getQuestionForEvent","event","target","code","preventDefault","focus","dragSVG","className","dragElement","svgElement","imageLoadingTimeoutId","clearTimeout","setTimeout","querySelectorAll","Array","from","filter","imageIsLoaded","imgElement","complete","naturalHeight","eventHandlersInitialised","lineEventHandlersInitialised","isPrinting","isKeyboardNavigation","questions","noOfLines","dropZones","init","hasOwnProperty","questionContainer","contains","dropArea","handleDropZoneEventMove","drags","handleDragHomeEventMove","dataset","handleWindowResize","handleResize","currentTarget","attr"],"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 for lines (using mouse down or touch) or tab through lines 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 Line,\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.svgEl = null;\n if (readOnly) {\n this.getRoot().classList.add('qtype_drawlines-readonly');\n }\n thisQ.allImagesLoaded = false;\n // Get all images that are not yet loaded\n thisQ.waitForAllImagesToBeLoaded()\n .then(() => {\n thisQ.drawDropzone(); // Call your function here\n })\n .catch((error) => {\n throw error;\n });\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.getSVGLineCoordinates(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 * Parse the coordinates from a particular string.\n *\n * @param {String} coordinates The coordinates to be parsed. The values are in the format: x1,y1 x2,y2.\n * Except for infinite line type where it's in the format x1,y1 x2,y2, x3,y3, x4,y4.\n * Here, x1,y1 and x4,y4 are the two very end points of the infinite line and\n * x2,y2 and x3,y3 are the pints with the handles.\n * @param {String} lineType The type of the line.\n */\n DrawlinesQuestion.prototype.parseCoordinates = function(coordinates, lineType) {\n var bits = coordinates.split(' ');\n if (lineType === 'lineinfinite' && bits.length !== 2) {\n // Remove the first and last coordinates.\n bits = bits.slice(1, -1);\n }\n if (bits.length !== 2) {\n throw new Error(coordinates + ' is not a valid point');\n }\n return bits;\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 = this.bgImage();\n var svg = this.getRoot().querySelector('svg.dropzones');\n var rootElement = this.getRoot();\n rootElement.querySelector('.que-dlines-dropzone').style.position = 'relative';\n rootElement.querySelector('.que-dlines-dropzone').style.top = (bgImage.height + 1) * -1 + \"px\";\n rootElement.querySelector('.que-dlines-dropzone').style.height = bgImage.height + \"px\";\n rootElement.querySelector('.droparea').style.height = bgImage.height + \"px\";\n if (!svg) {\n var dropZone = this.getRoot().querySelector('.que-dlines-dropzone');\n dropZone.innerHTML =\n '';\n }\n this.drawSVGLines(this.questionLines);\n };\n\n /**\n * Draws the svg lines of any drop zones.\n *\n * @param {Object[]} questionLines\n */\n DrawlinesQuestion.prototype.drawSVGLines = function(questionLines) {\n var bgImage = this.bgImage(),\n height, startcoordinates, endcoordinates, draginitialcoords;\n\n var drags = this.getRoot().querySelector('.draghomes');\n drags.innerHTML =\n '';\n\n var draghomeSvg = this.getRoot().querySelector('.dragshome');\n var dropzoneSvg = this.getRoot().querySelector('.dropzones');\n var initialHeight = 25;\n for (let line = 0; line < questionLines.length; line++) {\n height = initialHeight + line * 50;\n startcoordinates = '50,' + height + ';10';\n endcoordinates = '200,' + height + ';10';\n\n // Check if the lines are to be set with initial coordinates.\n draginitialcoords = this.visibleDropZones['c' + line];\n if (draginitialcoords !== undefined && draginitialcoords !== '') {\n // The visibleDropZones array holds the response in the format x1,y1 x2,y2 - to be added to svgdropzone.\n var coords = this.parseCoordinates(draginitialcoords, questionLines[line].type);\n startcoordinates = coords[0] + ';10';\n endcoordinates = coords[1] + ';10';\n this.lines[line] = Line.make(\n [startcoordinates, endcoordinates],\n [questionLines[line].labelstart, questionLines[line].labelend],\n questionLines[line].type\n );\n this.addToSvg(line, dropzoneSvg);\n } else {\n // Need to be added to draghomeSvg.\n this.lines[line] = Line.make(\n [startcoordinates, endcoordinates],\n [questionLines[line].labelstart, questionLines[line].labelend],\n questionLines[line].type\n );\n this.addToSvg(line, draghomeSvg);\n }\n }\n };\n\n // TODO: The below methods can be refractored for window resizing.\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 = Line.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.Line.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 // * 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] = Line.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 // * 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 Line.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 Line.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 // * 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 // /**\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 // * 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 Line.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 // * 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 // * 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 * Get the outer div for this question.\n *\n * @return {*}\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 *\n * @returns {element|undefined} the DOM element (if any)\n */\n DrawlinesQuestion.prototype.bgImage = function() {\n return this.getRoot().querySelector('img.dropbackground');\n };\n\n /**\n * Returns the coordinates for the line from the SVG.\n * @param {SVGElement} svgEl\n * @returns {Array} the coordinates.\n */\n DrawlinesQuestion.prototype.getSVGLineCoordinates = 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 * 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('data-dropzone-no', lineNumber);\n if (svg.getAttribute('class') === 'dropzones') {\n this.lineSVGs[lineNumber].setAttribute('class', 'dropzone choice' + lineNumber + ' placed');\n } else {\n this.lineSVGs[lineNumber].setAttribute('class', 'dropzone choice' + lineNumber + ' inactive');\n }\n };\n\n /**\n * Update the line of this drop zone in an SVG image.\n *\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 attached to the line ends (circles).\n *\n * @param {Event} e Event object\n * @param {String} whichHandle which circle handle was moved, i.e., startcircle or endcircle.\n * @param {int} dropzoneNo\n */\n DrawlinesQuestion.prototype.handleCircleMove = function(e, whichHandle, 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 = this.getRoot().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(whichHandle,\n parseInt(pageX) - parseInt(lastX), parseInt(pageY) - parseInt(lastY), parseInt(maxX), parseInt(maxY));\n lastX = pageX;\n lastY = pageY;\n movingDropZone.updateSvgEl(dropzoneNo);\n movingDropZone.saveCoordsForChoice(dropzoneNo);\n }, function() {\n document.body.removeChild(dragProxy);\n });\n };\n\n /**\n * Start responding to dragging the move handle attached to the line.\n *\n * @param {Event} e Event object\n * @param {int} dropzoneNo\n */\n DrawlinesQuestion.prototype.handleLineMove = 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 maxX,\n maxY,\n whichSVG = \"\",\n bgImage = this.bgImage(),\n isMoveFromDragsToDropzones,\n isMoveFromDropzonesToDrags,\n svgClass = '';\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\n // The svg's which are associated with this question.\n var closestSVGs = movingDrag.getSvgsClosestToElement(selectedElement);\n\n // Check if the drags need to be moved from one svg to another.\n var closeTo = selectedElement.closest('svg');\n svgClass = closeTo.getAttribute('class');\n\n // Moving the drags between the SVG's.\n // If true, the drag is moved from draghomes SVG to dropZone SVG.\n isMoveFromDragsToDropzones = (svgClass === \"dragshome\");\n\n // If true, the drag is moved from dropZone SVG to draghomes SVG.\n isMoveFromDropzonesToDrags = (svgClass === 'dropzones') &&\n (movingDrag.lines[dropzoneNo].centre1.y > (bgImage.height - 20));\n\n if (isMoveFromDragsToDropzones || isMoveFromDropzonesToDrags) {\n movingDrag.lines[dropzoneNo].addToDropZone('mouse', selectedElement,\n closestSVGs.svgDropZone, closestSVGs.svgDragsHome, dropX, dropY);\n }\n\n // Drag the lines within the SVG\n // Get the dimensions of the selected element's svg.\n closeTo = selectedElement.closest('svg');\n var dimensions = movingDrag.getSvgDimensionsByClass(closeTo, closeTo.getAttribute('class'));\n maxX = dimensions.maxX;\n maxY = dimensions.maxY;\n whichSVG = dimensions.whichSVG;\n\n movingDrag.lines[dropzoneNo].moveDrags(\n parseInt(pageX) - parseInt(lastX), parseInt(pageY) - parseInt(lastY),\n parseInt(maxX), parseInt(maxY), whichSVG);\n lastX = pageX;\n lastY = pageY;\n\n movingDrag.updateSvgEl(dropzoneNo);\n movingDrag.saveCoordsForChoice(dropzoneNo);\n }, function() {\n document.body.removeChild(dragProxy);\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 *\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().querySelector('svg g.choice' + choiceNo),\n gEleClassAttributes = '';\n if (items) {\n imageCoords = items.querySelector('polyline').getAttribute('points');\n gEleClassAttributes = items.getAttribute('class');\n // TODO: Kept the below comment as this could be needed for window resizing.\n\n // thiQ = this,\n // bgRatio = this.bgRatio();\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 Line.Point(drag.data('pagex'), drag.data('pagey'));\n // window.console.log(\"dragXY:\" + dragXY);\n //\n // window.console.log(\"thiQ:\" + thiQ);\n // if (thiQ.coordsInBgImg(dragXY)) {\n // var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n // bgImgXY = new Line.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n // imageCoords[imageCoords.length] = bgImgXY;\n // window.console.log(\"bgImgXY:\" + bgImgXY);\n // }\n // } else if (drag.data('imageCoords')) {\n // imageCoords[imageCoords.length] = drag.data('imageCoords');\n // }\n\n }\n if (gEleClassAttributes !== '' && gEleClassAttributes.includes('placed')) {\n this.getRoot().querySelector('input.choice' + choiceNo).value = imageCoords;\n } else if (gEleClassAttributes !== '' && gEleClassAttributes.includes('inactive')) {\n this.getRoot().querySelector('input.choice' + choiceNo).value = '';\n }\n };\n\n /**\n * Handle key down / press events on svg lines.\n * @param {KeyboardEvent} e\n * @param {SVGElement} drag SVG element being dragged.\n * @param {int} dropzoneNo\n * @param {String} activeElement The element being dragged, whether it is the line or the line endpoints.\n */\n DrawlinesQuestion.prototype.handleKeyPress = function(e, drag, dropzoneNo, activeElement) {\n\n var x = 0,\n y = 0,\n dropzoneElement,\n question = questionManager.getQuestionForEvent(e);\n\n dropzoneElement = drag.closest('g');\n switch (e.code) {\n case 'ArrowLeft':\n case 'KeyA': // A.\n x = -1;\n break;\n case 'ArrowRight':\n case 'KeyD': // D.\n x = 1;\n break;\n case 'ArrowDown':\n case 'KeyS': // S.\n y = 1;\n break;\n case 'ArrowUp':\n case 'KeyW': // W.\n y = -1;\n break;\n case 'Space':\n case 'Escape':\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n // Moving the drags between the SVG's.\n var closeTo = drag.closest('svg');\n var svgClass = closeTo.getAttribute('class');\n var maxX,\n maxY,\n whichSVG;\n var bgImage = this.bgImage();\n var closestSVGs = this.getSvgsClosestToElement(drag);\n var isMoveFromDragsToDropzones = (svgClass === \"dragshome\");\n var isMoveFromDropzonesToDrags = (svgClass === 'dropzones') &&\n (question.lines[dropzoneNo].centre1.y > (bgImage.height - 20));\n\n if (isMoveFromDragsToDropzones) {\n question.lines[dropzoneNo].addToDropZone('keyboard', dropzoneElement,\n closestSVGs.svgDropZone, closestSVGs.svgDragsHome, null, null, 'DragsSVG');\n } else if (isMoveFromDropzonesToDrags) {\n question.lines[dropzoneNo].addToDropZone('keyboard', dropzoneElement,\n closestSVGs.svgDropZone, closestSVGs.svgDragsHome, null, null, 'DropZonesSVG');\n }\n\n // Get the dimensions of the selected element's svg.\n closeTo = drag.closest('svg');\n var dimensions = question.getSvgDimensionsByClass(closeTo, closeTo.getAttribute('class'));\n maxX = dimensions.maxX;\n maxY = dimensions.maxY;\n whichSVG = dimensions.whichSVG;\n\n if (activeElement === 'line') {\n // Move the entire line when the focus is on it.\n question.lines[dropzoneNo].moveDrags(parseInt(x), parseInt(y), parseInt(maxX), parseInt(maxY), whichSVG);\n } else {\n // Move the line endpoints.\n question.lines[dropzoneNo].move(activeElement, parseInt(x), parseInt(y), parseInt(maxX), parseInt(maxY));\n }\n question.updateSvgEl(dropzoneNo);\n this.saveCoordsForChoice(dropzoneNo);\n drag.focus();\n };\n\n /**\n * Returns the dimensions of the SVG image to which the drag element belongs.\n *\n * @param {SVG} dragSVG The SVG to which the drag element belongs.\n * @param {String} className Class asscociated with the SVG\n * @return {{whichSVG: (string), maxY: number, maxX: number}}\n */\n DrawlinesQuestion.prototype.getSvgDimensionsByClass = function(dragSVG, className) {\n return {\n maxX: dragSVG.width.baseVal.value,\n maxY: dragSVG.height.baseVal.value,\n whichSVG: className === 'dragshome' ? 'DragsSVG' : 'DropZonesSVG'\n };\n };\n\n /**\n * Returns the SVG's to which the drag element belongs.\n *\n * @param {SVGElement} dragElement The element which is being moved.\n * @return {{svgDragsHome, svgDropZone}}\n */\n DrawlinesQuestion.prototype.getSvgsClosestToElement = function(dragElement) {\n var svgElement = dragElement.closest('svg');\n var svgElementClass = svgElement.getAttribute('class');\n var svgDragsHome, svgDropZone, parent;\n if (svgElementClass === \"dragshome\") {\n svgDragsHome = svgElement;\n parent = svgElement.closest('.ddarea');\n svgDropZone = parent.querySelector('.dropzones');\n } else {\n svgDropZone = svgElement;\n parent = svgElement.closest('.ddarea');\n svgDragsHome = parent.querySelector('.dragshome');\n }\n return {\n svgDropZone: svgDropZone,\n svgDragsHome: svgDragsHome\n };\n };\n\n /**\n * Waits until all images are loaded before setting up question.\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 * @return {Promise}\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 const images = document.querySelectorAll('img');\n const promises = Array.from(images).map((img) => {\n return new Promise((resolve, reject) => {\n if (img.complete) {\n resolve(); // If image is already loaded\n } else {\n img.onload = () => resolve();\n img.onerror = () => reject(new Error(`Failed to load image: ${img.src}`));\n }\n });\n });\n\n this.allImagesLoaded = true;\n return Promise.all(promises);\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {boolean} Returns true if images are loaded without errors.\n */\n DrawlinesQuestion.prototype.getNotYetLoadedImages = function() {\n // Get all 'img' elements with the class 'dropbackground' within '.drawlines' inside the root element\n const images = this.getRoot().querySelectorAll('.drawlines img.dropbackground');\n\n // Filter out the images that are already loaded\n Array.from(images).filter((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 DrawlinesQuestions 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 lineEventHandlersInitialised: {},\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.lineEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.lineEventHandlersInitialised[containerId] = true;\n\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('drawlines') &&\n !questionContainer.classList.contains('qtype_drawlines-readonly')) {\n\n // Add event listeners to the 'previewArea'.\n // For dropzone SVG.\n var dropArea = questionContainer.querySelector('.droparea');\n // Add event listener for mousedown and touchstart events.\n dropArea.addEventListener('mousedown', questionManager.handleDropZoneEventMove);\n dropArea.addEventListener('touchstart', questionManager.handleDropZoneEventMove);\n // Add event listener for keydown and keypress events.\n dropArea.addEventListener('keydown', questionManager.handleKeyPress);\n dropArea.addEventListener('keypress', questionManager.handleKeyPress);\n\n // For draghomes SVG.\n var drags = questionContainer.querySelector('.draghomes');\n // Add event listener for mousedown and touchstart events.\n drags.addEventListener('mousedown', questionManager.handleDragHomeEventMove);\n drags.addEventListener('touchstart', questionManager.handleDragHomeEventMove);\n // Add event listener for keydown and keypress events.\n drags.addEventListener('keydown', questionManager.handleKeyPress);\n drags.addEventListener('keypress', questionManager.handleKeyPress);\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 /**\n * Handle mouse and touch events for dropzone svg.\n *\n * @param {Event} event\n */\n handleDropZoneEventMove: function(event) {\n var dropzoneElement, dropzoneNo;\n var question = questionManager.getQuestionForEvent(event);\n if (event.target.closest('.dropzone .startcircle.shape')) {\n // Dragging the move handle circle attached to the start of the line.\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleCircleMove(event, 'startcircle', dropzoneNo);\n } else if (event.target.closest('.dropzone .endcircle.shape')) {\n // Dragging the move handle circle attached to the end of the line.\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleCircleMove(event, 'endcircle', dropzoneNo);\n } else if (event.target.closest('polyline.shape')) {\n // Dragging the entire line.\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleLineMove(event, dropzoneNo);\n }\n },\n\n /**\n * Handle mouse and touch events for dragshome svg.\n *\n * @param {Event} event\n */\n handleDragHomeEventMove: function(event) {\n var dropzoneElement, dropzoneNo;\n var question = questionManager.getQuestionForEvent(event);\n if (event.target.closest('g')) {\n dropzoneElement = event.target.closest('g');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n question.handleLineMove(event, dropzoneNo);\n question.saveCoordsForChoice(dropzoneNo);\n }\n },\n\n /**\n * Handle key down / press events on markers.\n *\n * @param {Event} e\n */\n handleKeyPress: function(e) {\n var question = questionManager.getQuestionForEvent(e);\n var dropzoneElement, dropzoneNo, drag, activeElement;\n if (e.target.closest('.dropzone circle.startcircle')) {\n dropzoneElement = e.target.closest('.dropzone');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n drag = e.target.closest('.dropzone circle.startcircle');\n activeElement = 'startcircle';\n } else if (e.target.closest('.dropzone circle.endcircle')) {\n drag = e.target.closest('.dropzone circle.endcircle');\n dropzoneElement = e.target.closest('.dropzone');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n activeElement = 'endcircle';\n } else if (e.target.closest('g.dropzone')) {\n drag = e.target.closest('g.dropzone');\n dropzoneElement = e.target.closest('.dropzone');\n dropzoneNo = dropzoneElement.dataset.dropzoneNo;\n activeElement = 'line';\n }\n if (question && dropzoneElement) {\n question.handleKeyPress(e, drag, dropzoneNo, activeElement);\n }\n },\n\n /**\n * Handle when the window is resized.\n *\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 * Given an event, work out which question it effects.\n *\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 /**\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});\n"],"names":["define","$","dragDrop","Line","DrawlinesQuestion","containerId","readOnly","visibleDropZones","questionLines","thisQ","this","lineSVGs","lines","svgEl","getRoot","classList","add","allImagesLoaded","waitForAllImagesToBeLoaded","then","drawDropzone","catch","error","prototype","updateCoordinates","line","length","coordinates","getSVGLineCoordinates","parse","updateSvgEl","parseCoordinates","lineType","bits","split","slice","Error","bgImage","svg","querySelector","rootElement","style","position","top","height","innerHTML","width","drawSVGLines","startcoordinates","endcoordinates","draginitialcoords","draghomeSvg","dropzoneSvg","undefined","coords","type","make","labelstart","labelend","addToSvg","document","getElementById","childNodes","getAttribute","bgRatio","bgImg","bgImgNaturalWidth","get","naturalWidth","lineNumber","makeSvg","setAttribute","dropzoneNo","updateSvg","handleCircleMove","e","whichHandle","info","prepare","start","movingDropZone","lastX","x","lastY","y","dragProxy","makeDragProxy","maxX","baseVal","value","maxY","pageX","pageY","move","parseInt","saveCoordsForChoice","body","removeChild","handleLineMove","isMoveFromDragsToDropzones","isMoveFromDropzonesToDrags","movingDrag","whichSVG","svgClass","selectedElement","dropX","clientX","dropY","clientY","closestSVGs","getSvgsClosestToElement","closeTo","closest","centre1","addToDropZone","svgDropZone","svgDragsHome","dimensions","getSvgDimensionsByClass","moveDrags","createElement","left","appendChild","choiceNo","imageCoords","items","gEleClassAttributes","includes","handleKeyPress","drag","activeElement","dropzoneElement","question","questionManager","getQuestionForEvent","code","preventDefault","focus","dragSVG","className","dragElement","svgElement","images","querySelectorAll","promises","Array","from","map","img","Promise","resolve","reject","complete","onload","onerror","src","all","getNotYetLoadedImages","filter","imgNode","imageIsLoaded","imgElement","naturalHeight","eventHandlersInitialised","lineEventHandlersInitialised","isPrinting","isKeyboardNavigation","questions","noOfLines","dropZones","init","hasOwnProperty","questionContainer","contains","dropArea","addEventListener","handleDropZoneEventMove","drags","handleDragHomeEventMove","event","target","dataset","handleWindowResize","handleResize","currentTarget","attr"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/line.js b/amd/src/line.js index e014b73..17c100a 100644 --- a/amd/src/line.js +++ b/amd/src/line.js @@ -481,7 +481,6 @@ define(function() { selectedElement.childNodes[1].setAttribute('tabindex', '-1'); selectedElement.childNodes[2].setAttribute('tabindex', '-1'); } - return ''; }; /** @@ -600,9 +599,9 @@ define(function() { */ function createSvgShapeGroup(svg, tagName) { var svgEl = createSvgElement(svg, 'g'); + svgEl.setAttribute('tabindex', '0'); var lineEl = createSvgElement(svgEl, tagName); lineEl.setAttribute('class', 'shape'); - lineEl.setAttribute('tabindex', '0'); var startcircleEl = createSvgElement(svgEl, 'circle'); startcircleEl.setAttribute('class', 'startcircle shape'); var endcirleEl = createSvgElement(svgEl, 'circle'); @@ -665,8 +664,8 @@ define(function() { var linestartbits = startcoordinates[0].split(','); var lineendbits = endcoordinates[0].split(','); - return new Line(labels[0], linestartbits[0], linestartbits[1], startcoordinates[1], labels[1], - lineendbits[0], lineendbits[1], endcoordinates[1], lineType); + return new Line(labels[0], parseInt(linestartbits[0]), parseInt(linestartbits[1]), parseInt(startcoordinates[1]), + labels[1], parseInt(lineendbits[0]), parseInt(lineendbits[1]), parseInt(endcoordinates[1]), lineType); }, /** diff --git a/amd/src/question.js b/amd/src/question.js index 3e6076e..5627cb4 100644 --- a/amd/src/question.js +++ b/amd/src/question.js @@ -58,17 +58,13 @@ define([ } thisQ.allImagesLoaded = false; // Get all images that are not yet loaded - const images = thisQ.getNotYetLoadedImages(); - - // Loop over each image and add an event listener for the 'load' event - if (images) { - images.forEach((imgNode) => { - imgNode.addEventListener('load', function() { - thisQ.waitForAllImagesToBeLoaded(); - }, {once: true}); // The { once: true } option ensures the listener is called only once + thisQ.waitForAllImagesToBeLoaded() + .then(() => { + thisQ.drawDropzone(); // Call your function here + }) + .catch((error) => { + throw error; }); - } - thisQ.waitForAllImagesToBeLoaded(); } /** @@ -731,8 +727,7 @@ define([ dropzoneElement, question = questionManager.getQuestionForEvent(e); - dropzoneElement = event.target.closest('g'); - + dropzoneElement = drag.closest('g'); switch (e.code) { case 'ArrowLeft': case 'KeyA': // A. @@ -787,10 +782,10 @@ define([ if (activeElement === 'line') { // Move the entire line when the focus is on it. - question.lines[dropzoneNo].moveDrags(x, y, parseInt(maxX), parseInt(maxY), whichSVG); + question.lines[dropzoneNo].moveDrags(parseInt(x), parseInt(y), parseInt(maxX), parseInt(maxY), whichSVG); } else { // Move the line endpoints. - question.lines[dropzoneNo].move(activeElement, x, y, parseInt(maxX), parseInt(maxY)); + question.lines[dropzoneNo].move(activeElement, parseInt(x), parseInt(y), parseInt(maxX), parseInt(maxY)); } question.updateSvgEl(dropzoneNo); this.saveCoordsForChoice(dropzoneNo); @@ -842,6 +837,8 @@ define([ * * This function is called from the onLoad of each image, and also polls with * a time-out, because image on-loads are allegedly unreliable. + * + * @return {Promise} */ DrawlinesQuestion.prototype.waitForAllImagesToBeLoaded = function() { @@ -850,26 +847,20 @@ define([ if (this.allImagesLoaded) { return; } + const images = document.querySelectorAll('img'); + const promises = Array.from(images).map((img) => { + return new Promise((resolve, reject) => { + if (img.complete) { + resolve(); // If image is already loaded + } else { + img.onload = () => resolve(); + img.onerror = () => reject(new Error(`Failed to load image: ${img.src}`)); + } + }); + }); - // Clear any current timeout, if set. - if (this.imageLoadingTimeoutId !== null) { - clearTimeout(this.imageLoadingTimeoutId); - } - - // If we have not yet loaded all images, set a timeout to - // call ourselves again, since apparently images on-load - // events are flakey. - const images = this.getNotYetLoadedImages(); - if (images && images.length > 0) { - this.imageLoadingTimeoutId = setTimeout(function() { - this.waitForAllImagesToBeLoaded(); - }, 100); - return; - } - - // We now have all images. Carry on, but only after giving the layout a chance to settle down. this.allImagesLoaded = true; - this.drawDropzone(); + return Promise.all(promises); }; /** @@ -1070,8 +1061,8 @@ define([ dropzoneElement = e.target.closest('.dropzone'); dropzoneNo = dropzoneElement.dataset.dropzoneNo; activeElement = 'endcircle'; - } else if (e.target.closest('.dropzone polyline.shape')) { - drag = e.target.closest('.dropzone polyline.shape'); + } else if (e.target.closest('g.dropzone')) { + drag = e.target.closest('g.dropzone'); dropzoneElement = e.target.closest('.dropzone'); dropzoneNo = dropzoneElement.dataset.dropzoneNo; activeElement = 'line'; diff --git a/classes/line.php b/classes/line.php index 1db1043..50af68e 100644 --- a/classes/line.php +++ b/classes/line.php @@ -46,10 +46,15 @@ class line { /** @var string validate-zone-coordinates for start and the end of the line */ const VALIDATE_ZONE_COORDINATES = "/^([0-9]+),([0-9]+);([0-9]+)$/"; - /** @var string validate-response-coordinates for a line - * as the start(scx,scy) and the end(ecx,ecy) coordinates of the line in 'scx,scy ecx,ecy' format. + /** @var string validate-response-coordinates for a line of type linesegment, linesinglearrow, linedoublearrows. + * as the start(x1,y1) and the end(x2,y2) coordinates of the line in 'x1,y1 x2,y2' format. */ - const VALIDATE_RESPONSE_COORDINATES = "/^([^-][0-9]+),([^-][0-9]+)\b([^-][0-9]+),([^-][0-9]+)$/"; + const VALIDATE_RESPONSE_COORDINATES = "/^(\d+,\d+)( \d+,\d+)$/"; + + /** @var string validate-response-coordinates for infinite line. + * as the coordinates of the line in the format 'x1,y1 x2,y2 x3,y3 x4,y4' format. + */ + const VALIDATE_INFINITE_RESPONSE_COORDINATES = "/^(\d+,\d+)( \d+,\d+)( \d+,\d+)( \d+,\d+)$/"; /** @var int The line id. */ public $id; @@ -219,14 +224,19 @@ public static function is_zone_coordinates_valid(string $zone): bool { * the start zone and ecx,ecy are the end zone coordinates of a line respectively. * * @param string $linecoordinates the coordinates for start and end of the line in 'scx,scy ecx,ecy' format. + * @param string $lineType the type of the line. * @return bool */ - public static function are_response_coordinates_valid(string $linecoordinates): bool { + public static function are_response_coordinates_valid(string $linecoordinates, string $lineType): bool { // If the line-coordinates is empty return false. if (trim($linecoordinates) === '') { return false; } - preg_match_all(self::VALIDATE_RESPONSE_COORDINATES, $linecoordinates, $matches, PREG_SPLIT_NO_EMPTY); + if ($lineType == 'lineinfinite') { + preg_match_all(self::VALIDATE_INFINITE_RESPONSE_COORDINATES, $linecoordinates, $matches, PREG_SPLIT_NO_EMPTY); + } else { + preg_match_all(self::VALIDATE_RESPONSE_COORDINATES, $linecoordinates, $matches, PREG_SPLIT_NO_EMPTY); + } // If there is no match return false. foreach ($matches as $i => $match) { diff --git a/question.php b/question.php index 42516ee..38f2ff9 100644 --- a/question.php +++ b/question.php @@ -76,7 +76,7 @@ public function is_complete_response(array $response): bool { } foreach ($this->lines as $key => $line) { if (isset($response[$this->choice($key)]) && - !line::are_response_coordinates_valid($response[$this->choice($key)])) { + !line::are_response_coordinates_valid($response[$this->choice($key)], $line->type)) { return false; } } diff --git a/styles.css b/styles.css index f713a76..66b76b3 100644 --- a/styles.css +++ b/styles.css @@ -32,7 +32,7 @@ form.mform fieldset#id_previewareaheader .dropbackground { } .que.drawlines .dropzone .startcircle:focus, .que.drawlines .dropzone .endcircle:focus, -.que.drawlines .dropzone polyline:focus { +.que.drawlines g.dropzone:focus { outline: 2px solid #facb4e; } diff --git a/tests/behat/behat_qtype_drawlines.php b/tests/behat/behat_qtype_drawlines.php new file mode 100644 index 0000000..9596b15 --- /dev/null +++ b/tests/behat/behat_qtype_drawlines.php @@ -0,0 +1,66 @@ +. + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. +/** + * Steps definitions related with the drawlines question type. + * + * @package qtype_drawlines + * @category test + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_qtype_drawlines extends behat_base { + + /** + * Get the xpath for a given drag item. + * + * @param string $line the line to drag. + * @param string $part part of the line being dragged. + * @param bool $iskeyboard is using keyboard or not. + * @return string the xpath expression. + */ + protected function line_xpath($line, $part, $iskeyboard = false) { + $lineNo = (int)$line - 1; + if ($iskeyboard) { + if ($part == 'line') { + return '//*[name()="svg"]/*[name()="g" and contains(@class, "choice' . $this->escape($lineNo) . '")]'; + } else { + return '//*[name()="svg"]/*[name()="g" and contains(@class, "choice' . $this->escape($lineNo) . '")]' . + '/*[name()="circle" and contains(@class, "' . $this->escape($part) . '")]'; + } + } + } + + /** + * Type some characters while focused on a given line. + * + * @param string $direction the direction key to press. + * @param int $ + * @param string $part the part of the line to move. + * @param string $line the line to drag. The label, optionally followed by , (int) if relevant. + * + * @Given /^I type "(?Pup|down|left|right)" "(?P\d+)" times on line "(?P\d+)" "(?Pline|startcircle|endcircle)" in the drawlines question$/ + */ + public function i_type_on_line_in_the_drawlines_question($direction, $repeats, $line, $part) { + $node = $this->get_selected_node('xpath_element', $this->line_xpath($line, $part, true)); + $this->ensure_node_is_visible($node); + $node->focus(); + for ($i = 0; $i < $repeats; $i++) { + $this->execute('behat_general::i_press_named_key', ['', $direction]); + } + } +} diff --git a/tests/behat/preview.feature b/tests/behat/preview.feature index 626daf8..62bae11 100644 --- a/tests/behat/preview.feature +++ b/tests/behat/preview.feature @@ -20,8 +20,14 @@ Feature: Preview a DrawLines question And the following "questions" exist: | questioncategory | qtype | name | template | | Test questions | drawlines | Drawlines to preview | mkmap_twolines | - @javascript @_bug_phantomjs - Scenario: Preview a question using the mouse + + @javascript + Scenario: Preview a question using the keyboard When I am on the "Drawlines to preview" "core_question > preview" page logged in as teacher - And I pause - # TODO: Finishing this scenario after Js completed and adding other possible scenarios. + And I type "up" "360" times on line "1" "line" in the drawlines question + And I type "left" "40" times on line "1" "line" in the drawlines question + And I type "down" "190" times on line "1" "endcircle" in the drawlines question + And I type "left" "200" times on line "1" "endcircle" in the drawlines question + And I press "Submit and finish" + Then the state of "Draw 2 lines on the map" question is shown as "Partially correct" + And I should see "Mark 0.50 out of 1.00"