From cde433c26a3f36edb960d4c92f9650738a238fd4 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 24 Apr 2024 16:03:06 +0800 Subject: [PATCH 001/119] MDL-81647 core: Replace partial with a more lightweight version --- lib/moodlelib.php | 53 +++------- lib/tests/moodlelib_partial_test.php | 138 +++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 lib/tests/moodlelib_partial_test.php diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 7e0b0d9513441..2cec4fc8ebe51 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -10016,9 +10016,11 @@ function check_consecutive_identical_characters($password, $maxchars) { /** * Helper function to do partial function binding. - * so we can use it for preg_replace_callback, for example - * this works with php functions, user functions, static methods and class methods - * it returns you a callback that you can pass on like so: + * + * This is useful for cases such as preg_replace_callback where you may want to partially bind values. + * + * The use of named arguments is recommended for clarity. + * Please note that providing arguments in a different order may have mixed results for built-in functions. * * $callback = partial('somefunction', $arg1, $arg2); * or @@ -10029,45 +10031,12 @@ function check_consecutive_identical_characters($password, $maxchars) { * * and then the arguments that are passed through at calltime are appended to the argument list. * - * @param mixed $function a php callback - * @param mixed $arg1,... $argv arguments to partially bind with - * @return array Array callback - */ -function partial() { - if (!class_exists('partial')) { - /** - * Used to manage function binding. - * @copyright 2009 Penny Leach - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - class partial{ - /** @var array */ - public $values = array(); - /** @var string The function to call as a callback. */ - public $func; - /** - * Constructor - * @param string $func - * @param array $args - */ - public function __construct($func, $args) { - $this->values = $args; - $this->func = $func; - } - /** - * Calls the callback function. - * @return mixed - */ - public function method() { - $args = func_get_args(); - return call_user_func_array($this->func, array_merge($this->values, $args)); - } - } - } - $args = func_get_args(); - $func = array_shift($args); - $p = new partial($func, $args); - return array($p, 'method'); + * @param callable $function a php callback + * @param mixed ...$initialargs The arguments to provide for the initial bind + * @return callable + */ +function partial(callable $callable, ...$initialargs): callable { + return fn (...$args) => $callable(...$initialargs, ...$args); } /** diff --git a/lib/tests/moodlelib_partial_test.php b/lib/tests/moodlelib_partial_test.php new file mode 100644 index 0000000000000..ec0c3d9ea8f72 --- /dev/null +++ b/lib/tests/moodlelib_partial_test.php @@ -0,0 +1,138 @@ +. + +namespace core; + +/** + * Unit tests for partial() in moodlelib.php. + * + * @package core + * @category test + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers ::partial + */ +final class moodlelib_partial_test extends \advanced_testcase { + /** + * Test that arguments to partial can be passed as anticipated. + * + * @param callable $callable The callable to partially apply. + * @param array $initialargs The initial arguments to pass to the callable. + * @param array $calledargs The arguments to pass to the partially applied callable. + * @param mixed $expected The expected return value. + * @dataProvider partial_args_provider + */ + public function test_partial_args( + callable $callable, + array $initialargs, + array $calledargs, + mixed $expected, + ): void { + $this->assertEquals($expected, partial($callable, ...$initialargs)(...$calledargs)); + } + + /** + * An example static method as part of the testcase. + * + * @param string $foo The first argument. + * @param string $bar The second argument. + * @param string $baz The third argument. + * @param string $bum The fourth argument. + */ + public static function example_static_method( + $foo, + $bar, + $baz, + $bum + ): string { + return implode('/', [$foo, $bar, $baz, $bum]); + } + + /** + * An example method as part of the testcase. + * + * @param string $foo The first argument. + * @param string $bar The second argument. + * @param string $baz The third argument. + * @param string $bum The fourth argument. + */ + public function example_instance_method( + $foo, + $bar, + $baz, + $bum + ): string { + return implode('/', [$foo, $bar, $baz, $bum]); + } + + /** + * Data provider for test_partial_args. + * + * @return array + */ + public static function partial_args_provider(): array { + return [ + 'Using positional args' => [ + 'str_contains', + ['foobar'], + ['foo'], + true, + ], + 'Using positional args swapped' => [ + 'str_contains', + ['foo'], + ['foobar'], + false, + ], + 'Using named args' => [ + 'str_contains', + ['needle' => 'foo'], + ['haystack' => 'foobar'], + true, + ], + 'Using named args on callable args only' => [ + 'str_contains', + ['foobar'], + ['needle' => 'foo'], + true, + ], + 'Using named args on initial args only - instance method' => [ + [new self(), 'example_instance_method'], + ['foo' => 'foo'], + ['bar', 'baz', 'bum'], + 'foo/bar/baz/bum', + ], + 'Using named args on called args only - instance method' => [ + [new self(), 'example_instance_method'], + ['foo'], + ['bar' => 'bar', 'baz' => 'baz', 'bum' => 'bum'], + 'foo/bar/baz/bum', + ], + 'Using named args on initial args only - static method' => [ + [self::class, 'example_static_method'], + ['foo' => 'foo'], + ['bar', 'baz', 'bum'], + 'foo/bar/baz/bum', + ], + 'Using named args on called args only - static method' => [ + [self::class, 'example_static_method'], + ['foo'], + ['bar' => 'bar', 'baz' => 'baz', 'bum' => 'bum'], + 'foo/bar/baz/bum', + ], + ]; + } +} From 6d34386ca9d2763c50e5d78e1d257293ed454d83 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Mon, 11 Mar 2024 11:03:07 +0700 Subject: [PATCH 002/119] MDL-78662 core_question: Create utility function to format question. --- question/engine/lib.php | 14 ++++++++++ question/engine/tests/questionutils_test.php | 28 ++++++++++++++++++++ question/upgrade.txt | 3 +++ 3 files changed, 45 insertions(+) diff --git a/question/engine/lib.php b/question/engine/lib.php index a181a51b90b63..35d13a9022218 100644 --- a/question/engine/lib.php +++ b/question/engine/lib.php @@ -1134,6 +1134,20 @@ public static function get_editor_options($context) { return $editoroptions; } + + /** + * Format question fragment string and apply filtering, + * + * @param string $text current text that we want to be apply filters. + * @param context $context of the page question are in. + * @return string result has been modified by filters. + */ + public static function format_question_fragment(string $text, context $context): string { + global $PAGE; + $filtermanager = \filter_manager::instance(); + $filtermanager->setup_page_for_filters($PAGE, $context); + return $filtermanager->filter_string($text, $context); + } } diff --git a/question/engine/tests/questionutils_test.php b/question/engine/tests/questionutils_test.php index cb8fa7e327012..02d0b4041fe27 100644 --- a/question/engine/tests/questionutils_test.php +++ b/question/engine/tests/questionutils_test.php @@ -226,4 +226,32 @@ public function test_clean_param_mark() { $this->assertSame(-1.5, question_utils::clean_param_mark('-1.5')); $this->assertSame(-1.5, question_utils::clean_param_mark('-1,5')); } + + /** + * Test the content is being filtered by filters. + * + * @covers ::format_question_fragment + */ + public function test_format_question_fragment(): void { + global $CFG; + require_once($CFG->libdir . '/filterlib.php'); + $this->resetAfterTest(); + // Set few filters on. + filter_set_global_state('multilang', TEXTFILTER_ON); + filter_set_global_state('mathjaxloader', TEXTFILTER_ON); + filter_set_applies_to_strings('multilang', 1); + filter_set_applies_to_strings('mathjaxloader', 1); + + $systemcontext = \context_system::instance(); + $input = 'Some inline math \\( y = x^2 \\) and multi lang with html tag + EnglishFrançais'; + + $expected = question_utils::format_question_fragment($input, $systemcontext); + + // The data should only be filtered by mathjax and multi lang filter. HTML tags should not be affeacted. + $this->assertStringContainsString('Some inline math', $expected); + $this->assertStringContainsString('\( y = x^2 \)', $expected); + $this->assertStringNotContainsString('', $expected); + $this->assertStringContainsString('English', $expected); + } } diff --git a/question/upgrade.txt b/question/upgrade.txt index b2a9d4281c81a..f6e59be6b413d 100644 --- a/question/upgrade.txt +++ b/question/upgrade.txt @@ -83,6 +83,9 @@ This files describes API changes for code that uses the question API. .question-bank-table. This applies to the same styles to preview on the qbank_columnsortorder admin screen. It is important that the styles match on these pages so that the defaults have the expected result in the question bank. +10) A new utility function format_question_fragment is created. + format_question_fragment is added so that question content can filter base on filters. + === 4.2 === 1) The question/qengine.js has been deprecated. We create core_question/question_engine From f1044b45bd1b07147ae97c8174bf1007b2877453 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Mon, 11 Mar 2024 11:05:26 +0700 Subject: [PATCH 003/119] MDL-78662 ddimageortext: Allow answer to support filter dynamic content. --- .../ddimageortext/amd/build/question.min.js | 2 +- .../amd/build/question.min.js.map | 2 +- .../type/ddimageortext/amd/src/question.js | 110 +++++++++++++++--- question/type/ddimageortext/rendererbase.php | 2 +- question/type/ddimageortext/styles.css | 4 + .../ddimageortext/tests/behat/preview.feature | 14 ++- question/type/ddimageortext/tests/helper.php | 50 +++++++- 7 files changed, 163 insertions(+), 21 deletions(-) diff --git a/question/type/ddimageortext/amd/build/question.min.js b/question/type/ddimageortext/amd/build/question.min.js index 9c293a384c9ef..94ab303b95cf9 100644 --- a/question/type/ddimageortext/amd/build/question.min.js +++ b/question/type/ddimageortext/amd/build/question.min.js @@ -5,6 +5,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_ddimageortext/question",["jquery","core/dragdrop","core/key_codes","core_form/changechecker"],(function($,dragDrop,keys,FormChangeChecker){function DragDropOntoImageQuestion(containerId,readOnly,places){this.containerId=containerId,this.questionAnswer={},M.util.js_pending("qtype_ddimageortext-init-"+this.containerId),this.places=places,this.allImagesLoaded=!1,this.imageLoadingTimeoutId=null,this.isPrinting=!1,readOnly&&this.getRoot().addClass("qtype_ddimageortext-readonly");var thisQ=this;this.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),this.waitForAllImagesToBeLoaded()}DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded=function(){var thisQ=this;this.allImagesLoaded||(null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId),this.getNotYetLoadedImages().length>0?this.imageLoadingTimeoutId=setTimeout((function(){thisQ.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,thisQ.setupQuestion()))},DragDropOntoImageQuestion.prototype.getNotYetLoadedImages=function(){var thisQ=this;return this.getRoot().find(".ddarea img").not((function(i,imgNode){return thisQ.imageIsLoaded(imgNode)}))},DragDropOntoImageQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight},DragDropOntoImageQuestion.prototype.setupQuestion=function(){this.resizeAllDragsAndDrops(),this.cloneDrags(),this.positionDragsAndDrops(),M.util.js_complete("qtype_ddimageortext-init-"+this.containerId)},DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops=function(){var thisQ=this;this.getRoot().find(".draghomes > div").each((function(i,node){thisQ.resizeAllDragsAndDropsInGroup(thisQ.getClassnameNumericSuffix($(node),"dragitemgroup"))}))},DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup=function(group){var root=this.getRoot(),dragHomes=root.find(".dragitemgroup"+group+" .draghome"),maxWidth=0,maxHeight=0;for(var i in dragHomes.each((function(i,drag){maxWidth=Math.max(maxWidth,Math.ceil(drag.offsetWidth)),maxHeight=Math.max(maxHeight,Math.ceil(drag.offsetHeight))})),maxWidth+=10,maxHeight+=10,dragHomes.each((function(i,drag){var left=Math.round((maxWidth-drag.offsetWidth)/2),top=Math.floor((maxHeight-drag.offsetHeight)/2);$(drag).css({"padding-left":left+"px","padding-right":maxWidth-drag.offsetWidth-left+"px","padding-top":top+"px","padding-bottom":maxHeight-drag.offsetHeight-top+"px"})})),this.places)if(this.places.hasOwnProperty(i)){var place=this.places[i],label=place.text;parseInt(place.group)===group&&(""===label&&(label=M.util.get_string("blank","qtype_ddimageortext")),root.find(".dropzones").append('
'+label+" 
"),root.find(".dropzone.place"+i).width(maxWidth-2).height(maxHeight-2))}},DragDropOntoImageQuestion.prototype.cloneDrags=function(){var thisQ=this;thisQ.getRoot().find(".draghome").each((function(index,dragHome){var drag=$(dragHome),placeHolder=drag.clone();placeHolder.removeClass(),placeHolder.addClass("draghome choice"+thisQ.getChoice(drag)+" group"+thisQ.getGroup(drag)+" dragplaceholder"),drag.before(placeHolder)}))},DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice=function(dragHome){if(dragHome.hasClass("infinite"))for(var noOfDrags=this.noOfDropsInGroup(this.getGroup(dragHome)),i=0;i0&&"0"===choice)){var place=thisQ.getPlace(input),unplacedDrag=thisQ.getUnplacedChoice(thisQ.getGroup(input),choice),hiddenDrag=thisQ.getDragClone(unplacedDrag);if(hiddenDrag.length)if(unplacedDrag.hasClass("infinite")){var noOfDrags=thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));if(thisQ.getInfiniteDragClones(unplacedDrag,!1).length{result[inputNode.id]=inputNode.value})),result},DragDropOntoImageQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropOntoImageQuestion.prototype.handleDragStart=function(e){var thisQ=this,drag=$(e.target).closest(".draghome"),newIndex=this.calculateZIndex()+2;if(dragDrop.prepare(e).start&&!drag.hasClass("beingdragged")){drag.addClass("beingdragged").css("transform","").css("z-index",newIndex);var currentPlace=this.getClassnameNumericSuffix(drag,"inplace");if(null!==currentPlace){this.setInputValue(currentPlace,0),drag.removeClass("inplace"+currentPlace);var hiddenDrop=thisQ.getDrop(drag,currentPlace);hiddenDrop.length&&(hiddenDrop.addClass("active"),drag.offset(hiddenDrop.offset()))}else{var hiddenDrag=thisQ.getDragClone(drag);if(hiddenDrag.length)if(drag.hasClass("infinite")){var noOfDrags=this.noOfDropsInGroup(thisQ.getGroup(drag));if(this.getInfiniteDragClones(drag,!1).length1;)choice--,previous=this.getUnplacedChoice(group,choice);return previous},DragDropOntoImageQuestion.prototype.animateTo=function(drag,target){var currentPos=drag.offset(),targetPos=target.offset(),thisQ=this;M.util.js_pending("qtype_ddimageortext-animate-"+thisQ.containerId),drag.animate({left:parseInt(drag.css("left"))+targetPos.left-currentPos.left,top:parseInt(drag.css("top"))+targetPos.top-currentPos.top},{duration:"fast",done:function(){$("body").trigger("qtype_ddimageortext-dragmoved",[drag,target,thisQ]),M.util.js_complete("qtype_ddimageortext-animate-"+thisQ.containerId)}})},DragDropOntoImageQuestion.prototype.isPointInDrop=function(pageX,pageY,drop){var position=drop.offset();return drop.hasClass("draghome")?pageX>=position.left&&pageX=position.top&&pageY=position.left&&pageX=position.top&&pageYzIndex&&(zIndex=itemZIndex)})),zIndex},DragDropOntoImageQuestion.prototype.isDragSameAsDrop=function(drag,drop){return this.getChoice(drag)===this.getChoice(drop)&&this.getGroup(drag)===this.getGroup(drop)};var questionManager={eventHandlersInitialised:!1,dragEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,places){if(questionManager.questions[containerId]=new DragDropOntoImageQuestion(containerId,readOnly,places),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.dragEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("ddimageortext")&&!questionContainer.classList.contains("qtype_ddimageortext-readonly")&&questionManager.addEventHandlersToDrag($(questionContainer).find(".draghome"))}},setupEventHandlers:function(){$("body").on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone",questionManager.handleKeyPress).on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)",questionManager.handleKeyPress).on("qtype_ddimageortext-dragmoved",questionManager.handleDragMoved),$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToDrag:function(element){element.unbind("mousedown touchstart"),element.on("mousedown touchstart",questionManager.handleDragStart)},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){if(!questionManager.isKeyboardNavigation){questionManager.isKeyboardNavigation=!0;var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)}},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},fixLayoutIfThingsMoved:function(){this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},handleDragMoved:function(e,drag,target,thisQ){drag.removeClass("beingdragged").css("z-index",""),drag.css("top",target.position().top).css("left",target.position().left),target.after(drag),target.removeClass("active"),void 0!==drag.data("unplaced")&&!0===drag.data("unplaced")?(drag.removeClass("placed").addClass("unplaced"),drag.removeAttr("tabindex"),drag.removeData("unplaced"),drag.css("top","").css("left","").css("transform",""),drag.hasClass("infinite")&&thisQ.getInfiniteDragClones(drag,!0).length>1&&thisQ.getInfiniteDragClones(drag,!0).first().remove()):(drag.data("originX",target.data("originX")).data("originY",target.data("originY")),thisQ.handleElementScale(drag,"left top")),void 0!==drag.data("isfocus")&&!0===drag.data("isfocus")&&(drag.focus(),drag.removeData("isfocus")),void 0!==target.data("isfocus")&&!0===target.data("isfocus")&&target.removeData("isfocus"),questionManager.isKeyboardNavigation&&(questionManager.isKeyboardNavigation=!1),thisQ.isQuestionInteracted()&&(questionManager.handleFormDirty(),thisQ.questionAnswer=thisQ.getQuestionAnsweredValues())},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.ddimageortext").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); +define("qtype_ddimageortext/question",["jquery","core/dragdrop","core/key_codes","core_form/changechecker","core_filters/events"],(function($,dragDrop,keys,FormChangeChecker,filterEvent){function DragDropOntoImageQuestion(containerId,readOnly,places){this.containerId=containerId,this.questionAnswer={},this.questionDragDropWidthHeight=[],M.util.js_pending("qtype_ddimageortext-init-"+this.containerId),this.places=places,this.allImagesLoaded=!1,this.imageLoadingTimeoutId=null,this.isPrinting=!1,readOnly&&this.getRoot().addClass("qtype_ddimageortext-readonly");var thisQ=this;this.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),this.waitForAllImagesToBeLoaded()}DragDropOntoImageQuestion.prototype.changeAllDragsAndDropsToFilteredContent=function(filteredElement){let currentFilteredItem=$(filteredElement);const parentIsDD=currentFilteredItem.parent().closest("div").hasClass("placed")||currentFilteredItem.parent().hasClass("draghome"),isDD=currentFilteredItem.hasClass("placed")||currentFilteredItem.hasClass("draghome");if(!parentIsDD&&!isDD)return;if(parentIsDD&&(currentFilteredItem=currentFilteredItem.parent().closest("div")),this.getRoot().find(currentFilteredItem).length<=0)return;const group=this.getGroup(currentFilteredItem),choice=this.getChoice(currentFilteredItem);let listOfModifiedDragDrop=[];this.getRoot().find(".group"+group+".choice"+choice).each((function(i,node){if($(node).get(0)===currentFilteredItem.get(0))return;const originalClass=$(node).attr("class"),originalStyle=$(node).attr("style"),filteredDragDropClone=currentFilteredItem.clone();questionManager.addEventHandlersToDrag(filteredDragDropClone),filteredDragDropClone.attr("class",originalClass),filteredDragDropClone.attr("style",originalStyle),$(node).before(filteredDragDropClone),listOfModifiedDragDrop.push(node)})),listOfModifiedDragDrop.forEach((function(node){$(node).remove()}));const currentHeight=currentFilteredItem.height(),currentWidth=currentFilteredItem.width();currentFilteredItem.height("auto"),currentFilteredItem.width("auto"),filteredElement.offsetWidth&&filteredElement.offsetHeight||filteredElement.classList.add("d-block"),this.questionDragDropWidthHeight[group].maxWidth0?this.imageLoadingTimeoutId=setTimeout((function(){thisQ.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,thisQ.setupQuestion(),document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete,(elements=>{elements.detail.nodes.forEach((element=>{thisQ.changeAllDragsAndDropsToFilteredContent(element)}))}))))},DragDropOntoImageQuestion.prototype.getNotYetLoadedImages=function(){var thisQ=this;return this.getRoot().find(".ddarea img").not((function(i,imgNode){return thisQ.imageIsLoaded(imgNode)}))},DragDropOntoImageQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight},DragDropOntoImageQuestion.prototype.setupQuestion=function(){this.resizeAllDragsAndDrops(),this.cloneDrags(),this.positionDragsAndDrops(),M.util.js_complete("qtype_ddimageortext-init-"+this.containerId)},DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops=function(){var thisQ=this;this.getRoot().find(".draghomes > div").each((function(i,node){thisQ.resizeAllDragsAndDropsInGroup(thisQ.getClassnameNumericSuffix($(node),"dragitemgroup"))}))},DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup=function(group){var root=this.getRoot(),dragHomes=root.find(".draghome.group"+group),maxWidth=0,maxHeight=0;for(var i in dragHomes.each((function(i,drag){maxWidth=Math.max(maxWidth,Math.ceil(drag.offsetWidth)),maxHeight=Math.max(maxHeight,Math.ceil(drag.offsetHeight))})),maxWidth+=10,maxHeight+=10,this.questionDragDropWidthHeight[group]={maxWidth:maxWidth,maxHeight:maxHeight},dragHomes.each((function(i,drag){$(drag).width(maxWidth).height(maxHeight).css("lineHeight",maxHeight+"px")})),this.places)if(this.places.hasOwnProperty(i)){var place=this.places[i],label=place.text;parseInt(place.group)===group&&(""===label&&(label=M.util.get_string("blank","qtype_ddimageortext")),0===root.find(".dropzones .dropzone.group"+place.group+".place"+i).length&&root.find(".dropzones").append('
'+label+" 
"),root.find(".dropzone.place"+i).width(maxWidth-2).height(maxHeight-2))}},DragDropOntoImageQuestion.prototype.cloneDrags=function(){var thisQ=this;thisQ.getRoot().find(".draghome").each((function(index,dragHome){var drag=$(dragHome),placeHolder=drag.clone();placeHolder.removeClass(),placeHolder.addClass("draghome choice"+thisQ.getChoice(drag)+" group"+thisQ.getGroup(drag)+" dragplaceholder"),drag.before(placeHolder)}))},DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice=function(dragHome){if(dragHome.hasClass("infinite"))for(var noOfDrags=this.noOfDropsInGroup(this.getGroup(dragHome)),i=0;i0&&"0"===choice)){var place=thisQ.getPlace(input),unplacedDrag=thisQ.getUnplacedChoice(thisQ.getGroup(input),choice),hiddenDrag=thisQ.getDragClone(unplacedDrag);if(hiddenDrag.length)if(unplacedDrag.hasClass("infinite")){var noOfDrags=thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));if(thisQ.getInfiniteDragClones(unplacedDrag,!1).length{result[inputNode.id]=inputNode.value})),result},DragDropOntoImageQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropOntoImageQuestion.prototype.handleDragStart=function(e){var thisQ=this,drag=$(e.target).closest(".draghome"),newIndex=this.calculateZIndex()+2;if(dragDrop.prepare(e).start&&!drag.hasClass("beingdragged")){drag.addClass("beingdragged").css("transform","").css("z-index",newIndex);var currentPlace=this.getClassnameNumericSuffix(drag,"inplace");if(null!==currentPlace){this.setInputValue(currentPlace,0),drag.removeClass("inplace"+currentPlace);var hiddenDrop=thisQ.getDrop(drag,currentPlace);hiddenDrop.length&&(hiddenDrop.addClass("active"),drag.offset(hiddenDrop.offset()))}else{var hiddenDrag=thisQ.getDragClone(drag);if(hiddenDrag.length)if(drag.hasClass("infinite")){var noOfDrags=this.noOfDropsInGroup(thisQ.getGroup(drag));if(this.getInfiniteDragClones(drag,!1).length1;)choice--,previous=this.getUnplacedChoice(group,choice);return previous},DragDropOntoImageQuestion.prototype.animateTo=function(drag,target){var currentPos=drag.offset(),targetPos=target.offset(),thisQ=this;M.util.js_pending("qtype_ddimageortext-animate-"+thisQ.containerId),drag.animate({left:parseInt(drag.css("left"))+targetPos.left-currentPos.left,top:parseInt(drag.css("top"))+targetPos.top-currentPos.top},{duration:"fast",done:function(){$("body").trigger("qtype_ddimageortext-dragmoved",[drag,target,thisQ]),M.util.js_complete("qtype_ddimageortext-animate-"+thisQ.containerId)}})},DragDropOntoImageQuestion.prototype.isPointInDrop=function(pageX,pageY,drop){var position=drop.offset();return drop.hasClass("draghome")?pageX>=position.left&&pageX=position.top&&pageY=position.left&&pageX=position.top&&pageYzIndex&&(zIndex=itemZIndex)})),zIndex},DragDropOntoImageQuestion.prototype.isDragSameAsDrop=function(drag,drop){return this.getChoice(drag)===this.getChoice(drop)&&this.getGroup(drag)===this.getGroup(drop)};var questionManager={eventHandlersInitialised:!1,dragEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,places){if(questionManager.questions[containerId]=new DragDropOntoImageQuestion(containerId,readOnly,places),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.dragEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("ddimageortext")&&!questionContainer.classList.contains("qtype_ddimageortext-readonly")&&questionManager.addEventHandlersToDrag($(questionContainer).find(".draghome"))}},setupEventHandlers:function(){$("body").on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone",questionManager.handleKeyPress).on("keydown",".que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)",questionManager.handleKeyPress).on("qtype_ddimageortext-dragmoved",questionManager.handleDragMoved),$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToDrag:function(element){element.unbind("mousedown touchstart"),element.on("mousedown touchstart",questionManager.handleDragStart)},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){if(!questionManager.isKeyboardNavigation){questionManager.isKeyboardNavigation=!0;var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)}},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},fixLayoutIfThingsMoved:function(){this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},handleDragMoved:function(e,drag,target,thisQ){drag.removeClass("beingdragged").css("z-index",""),drag.css("top",target.position().top).css("left",target.position().left),target.after(drag),target.removeClass("active"),void 0!==drag.data("unplaced")&&!0===drag.data("unplaced")?(drag.removeClass("placed").addClass("unplaced"),drag.removeAttr("tabindex"),drag.removeData("unplaced"),drag.css("top","").css("left","").css("transform",""),drag.hasClass("infinite")&&thisQ.getInfiniteDragClones(drag,!0).length>1&&thisQ.getInfiniteDragClones(drag,!0).first().remove()):(drag.data("originX",target.data("originX")).data("originY",target.data("originY")),thisQ.handleElementScale(drag,"left top")),void 0!==drag.data("isfocus")&&!0===drag.data("isfocus")&&(drag.focus(),drag.removeData("isfocus")),void 0!==target.data("isfocus")&&!0===target.data("isfocus")&&target.removeData("isfocus"),questionManager.isKeyboardNavigation&&(questionManager.isKeyboardNavigation=!1),thisQ.isQuestionInteracted()&&(questionManager.handleFormDirty(),thisQ.questionAnswer=thisQ.getQuestionAnsweredValues())},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.ddimageortext").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); //# sourceMappingURL=question.min.js.map \ No newline at end of file diff --git a/question/type/ddimageortext/amd/build/question.min.js.map b/question/type/ddimageortext/amd/build/question.min.js.map index a0bf39e5fbb71..9b89c089dc447 100644 --- a/question/type/ddimageortext/amd/build/question.min.js.map +++ b/question/type/ddimageortext/amd/build/question.min.js.map @@ -1 +1 @@ -{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/question\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'core/key_codes',\n 'core_form/changechecker'\n], function(\n $,\n dragDrop,\n keys,\n FormChangeChecker\n) {\n\n \"use strict\";\n\n /**\n * Initialise one drag-drop onto image 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 {Array} places Information about the drop places.\n * @constructor\n */\n function DragDropOntoImageQuestion(containerId, readOnly, places) {\n this.containerId = containerId;\n this.questionAnswer = {};\n M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);\n this.places = places;\n this.allImagesLoaded = false;\n this.imageLoadingTimeoutId = null;\n this.isPrinting = false;\n if (readOnly) {\n this.getRoot().addClass('qtype_ddimageortext-readonly');\n }\n\n var thisQ = this;\n this.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n this.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n var thisQ = this;\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n thisQ.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 thisQ.setupQuestion();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {\n var thisQ = this;\n return this.getRoot().find('.ddarea img').not(function(i, imgNode) {\n return thisQ.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 DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Set up the question, once all images have been loaded.\n */\n DragDropOntoImageQuestion.prototype.setupQuestion = function() {\n this.resizeAllDragsAndDrops();\n this.cloneDrags();\n this.positionDragsAndDrops();\n M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);\n };\n\n /**\n * In each group, resize all the items to be the same size.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {\n var thisQ = this;\n this.getRoot().find('.draghomes > div').each(function(i, node) {\n thisQ.resizeAllDragsAndDropsInGroup(\n thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));\n });\n };\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {\n var root = this.getRoot(),\n dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n dragHomes.each(function(i, drag) {\n maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n dragHomes.each(function(i, drag) {\n var left = Math.round((maxWidth - drag.offsetWidth) / 2),\n top = Math.floor((maxHeight - drag.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drag).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'\n });\n });\n\n // Create the drops and make them the right size.\n for (var i in this.places) {\n if (!this.places.hasOwnProperty((i))) {\n continue;\n }\n var place = this.places[i],\n label = place.text;\n if (parseInt(place.group) !== group) {\n continue;\n }\n if (label === '') {\n label = M.util.get_string('blank', 'qtype_ddimageortext');\n }\n root.find('.dropzones').append('
' +\n '' + label + ' 
');\n root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);\n }\n };\n\n /**\n * Invisible 'drag homes' are output by the renderer. These have the same properties\n * as the drag items but are invisible. We clone these invisible elements to make the\n * actual drag items.\n */\n DragDropOntoImageQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n thisQ.getRoot().find('.draghome').each(function(index, dragHome) {\n var drag = $(dragHome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('draghome choice' +\n thisQ.getChoice(drag) + ' group' +\n thisQ.getGroup(drag) + ' dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Clone drag item for one choice.\n *\n * @param {jQuery} dragHome the drag home to clone.\n */\n DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {\n if (dragHome.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));\n for (var i = 0; i < noOfDrags; i++) {\n this.cloneDrag(dragHome);\n }\n } else {\n this.cloneDrag(dragHome);\n }\n };\n\n /**\n * Clone drag item.\n *\n * @param {jQuery} dragHome\n */\n DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {\n var drag = dragHome.clone();\n drag.removeClass('draghome')\n .addClass('drag unplaced moodle-has-zindex')\n .offset(dragHome.offset());\n this.getRoot().find('.dragitems').append(drag);\n };\n\n /**\n * Update the position of drags.\n */\n DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {\n var thisQ = this,\n root = this.getRoot(),\n bgRatio = this.bgRatio();\n\n // Move the drops into position.\n root.find('.ddarea .dropzone').each(function(i, dropNode) {\n var drop = $(dropNode),\n place = thisQ.places[thisQ.getPlace(drop)];\n // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation.\n drop.css('left', parseInt(place.xy[0]) * bgRatio)\n .css('top', parseInt(place.xy[1]) * bgRatio);\n drop.data('originX', parseInt(place.xy[0]))\n .data('originY', parseInt(place.xy[1]));\n thisQ.handleElementScale(drop, 'left top');\n });\n\n // First move all items back home.\n root.find('.draghome').not('.dragplaceholder').each(function(i, dragNode) {\n var drag = $(dragNode),\n currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');\n drag.addClass('unplaced')\n .removeClass('placed');\n drag.removeAttr('tabindex');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n });\n\n // Then place the ones that should be placed.\n root.find('input.placeinput').each(function(i, inputNode) {\n var input = $(inputNode),\n choice = input.val();\n if (choice.length === 0 || (choice.length > 0 && choice === '0')) {\n // No item in this place.\n return;\n }\n\n var place = thisQ.getPlace(input);\n // Get the unplaced drag.\n var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);\n // Get the clone of the drag.\n var hiddenDrag = thisQ.getDragClone(unplacedDrag);\n if (hiddenDrag.length) {\n if (unplacedDrag.hasClass('infinite')) {\n var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));\n var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = unplacedDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n // Sometimes, for the question that has a lot of input groups and unlimited draggable items,\n // this 'clone' process takes longer than usual, so the questionManager.init() method\n // will not add the eventHandler for this cloned drag.\n // We need to make sure to add the eventHandler for the cloned drag too.\n questionManager.addEventHandlersToDrag(cloneDrag);\n } else {\n hiddenDrag.addClass('active');\n }\n } else {\n hiddenDrag.addClass('active');\n }\n }\n\n // Send the drag to drop.\n var drop = root.find('.dropzone.place' + place);\n thisQ.sendDragToDrop(unplacedDrag, drop);\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropOntoImageQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.placeinput').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropOntoImageQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Handles the start of dragging an item.\n *\n * @param {Event} e the touch start or mouse down event.\n */\n DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n drag = $(e.target).closest('.draghome'),\n currentIndex = this.calculateZIndex(),\n newIndex = currentIndex + 2;\n\n var info = dragDrop.prepare(e);\n if (!info.start || drag.hasClass('beingdragged')) {\n return;\n }\n\n drag.addClass('beingdragged').css('transform', '').css('z-index', newIndex);\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n this.setInputValue(currentPlace, 0);\n drag.removeClass('inplace' + currentPlace);\n var hiddenDrop = thisQ.getDrop(drag, currentPlace);\n if (hiddenDrop.length) {\n hiddenDrop.addClass('active');\n drag.offset(hiddenDrop.offset());\n }\n } else {\n var hiddenDrag = thisQ.getDragClone(drag);\n if (hiddenDrag.length) {\n if (drag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(thisQ.getGroup(drag));\n var cloneDrags = this.getInfiniteDragClones(drag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = drag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n drag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n }\n }\n\n dragDrop.start(e, drag, function(x, y, drag) {\n thisQ.dragMove(x, y, drag);\n }, function(x, y, drag) {\n thisQ.dragEnd(x, y, drag);\n });\n };\n\n /**\n * Called whenever the currently dragged items moves.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {\n var thisQ = this,\n highlighted = false;\n this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted) {\n highlighted = true;\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n this.getRoot().find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted && !thisQ.isDragSameAsDrop(drag, drop)) {\n highlighted = true;\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n };\n\n /**\n * Called when user drops a drag item.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {\n var thisQ = this,\n root = this.getRoot(),\n placed = false;\n\n // Looking for drag that was dropped on a dropzone.\n root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (!thisQ.isPointInDrop(pageX, pageY, drop)) {\n // Not this drop.\n return true;\n }\n\n // Now put this drag into the drop.\n drop.removeClass('valid-drag-over-drop');\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n\n if (!placed) {\n // Looking for drag that was dropped on a placed drag.\n root.find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) {\n var placedDrag = $(placedNode);\n if (!thisQ.isPointInDrop(pageX, pageY, placedDrag) || thisQ.isDragSameAsDrop(drag, placedDrag)) {\n // Not this placed drag.\n return true;\n }\n\n // Now put this drag into the drop.\n placedDrag.removeClass('valid-drag-over-drop');\n var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace');\n var drop = thisQ.getDrop(drag, currentPlace);\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n }\n\n if (!placed) {\n this.sendDragHome(drag);\n }\n };\n\n /**\n * Animate a drag item into a given place (or back home).\n *\n * @param {jQuery|null} drag the item to place. If null, clear the place.\n * @param {jQuery} drop the place to put it.\n */\n DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {\n // Is there already a drag in this drop? if so, evict it.\n var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));\n if (oldDrag.length !== 0) {\n oldDrag.addClass('beingdragged');\n oldDrag.offset(oldDrag.offset());\n var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');\n var hiddenDrop = this.getDrop(oldDrag, currentPlace);\n hiddenDrop.addClass('active');\n this.sendDragHome(oldDrag);\n }\n\n if (drag.length === 0) {\n this.setInputValue(this.getPlace(drop), 0);\n if (drop.data('isfocus')) {\n drop.focus();\n }\n } else {\n this.setInputValue(this.getPlace(drop), this.getChoice(drag));\n drag.removeClass('unplaced')\n .addClass('placed inplace' + this.getPlace(drop));\n drag.attr('tabindex', 0);\n this.animateTo(drag, drop);\n }\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n drag.data('unplaced', true);\n\n this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));\n };\n\n /**\n * Handles keyboard events on drops.\n *\n * Drops are focusable. Once focused, right/down/space switches to the next choice, and\n * left/up switches to the previous. Escape clear.\n *\n * @param {KeyboardEvent} e\n */\n DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {\n var drop = $(e.target).closest('.dropzone');\n if (drop.length === 0) {\n var placedDrag = $(e.target);\n var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');\n if (currentPlace !== null) {\n drop = this.getDrop(placedDrag, currentPlace);\n }\n }\n var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),\n nextDrag = $();\n\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.escape:\n questionManager.isKeyboardNavigation = false;\n break;\n\n default:\n questionManager.isKeyboardNavigation = false;\n return; // To avoid the preventDefault below.\n }\n\n if (nextDrag.length) {\n nextDrag.data('isfocus', true);\n nextDrag.addClass('beingdragged');\n var hiddenDrag = this.getDragClone(nextDrag);\n if (hiddenDrag.length) {\n if (nextDrag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));\n var cloneDrags = this.getInfiniteDragClones(nextDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = nextDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n nextDrag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n }\n } else {\n drop.data('isfocus', true);\n }\n\n e.preventDefault();\n this.sendDragToDrop(nextDrag, drop);\n };\n\n /**\n * Choose the next drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {\n var choice,\n numChoices = this.noOfChoicesInGroup(group);\n\n if (drag.length === 0) {\n choice = 1; // Was empty, so we want to select the first choice.\n } else {\n choice = this.getChoice(drag) + 1;\n }\n\n var next = this.getUnplacedChoice(group, choice);\n while (next.length === 0 && choice < numChoices) {\n choice++;\n next = this.getUnplacedChoice(group, choice);\n }\n\n return next;\n };\n\n /**\n * Choose the previous drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {\n var choice;\n\n if (drag.length === 0) {\n choice = this.noOfChoicesInGroup(group);\n } else {\n choice = this.getChoice(drag) - 1;\n }\n\n var previous = this.getUnplacedChoice(group, choice);\n while (previous.length === 0 && choice > 1) {\n choice--;\n previous = this.getUnplacedChoice(group, choice);\n }\n\n // Does this choice exist?\n return previous;\n };\n\n /**\n * Animate an object to the given destination.\n *\n * @param {jQuery} drag the element to be animated.\n * @param {jQuery} target element marking the place to move it to.\n */\n DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {\n var currentPos = drag.offset(),\n targetPos = target.offset(),\n thisQ = this;\n\n M.util.js_pending('qtype_ddimageortext-animate-' + thisQ.containerId);\n // Animate works in terms of CSS position, whereas locating an object\n // on the page works best with jQuery offset() function. So, to get\n // the right target position, we work out the required change in\n // offset() and then add that to the current CSS position.\n drag.animate(\n {\n left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,\n top: parseInt(drag.css('top')) + targetPos.top - currentPos.top\n },\n {\n duration: 'fast',\n done: function() {\n $('body').trigger('qtype_ddimageortext-dragmoved', [drag, target, thisQ]);\n M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId);\n }\n }\n );\n };\n\n /**\n * Detect if a point is inside a given DOM node.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drop the node to check (typically a drop).\n * @return {boolean} whether the point is inside the node.\n */\n DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {\n var position = drop.offset();\n if (drop.hasClass('draghome')) {\n return pageX >= position.left && pageX < position.left + drop.outerWidth()\n && pageY >= position.top && pageY < position.top + drop.outerHeight();\n }\n return pageX >= position.left && pageX < position.left + drop.width()\n && pageY >= position.top && pageY < position.top + drop.height();\n };\n\n /**\n * Set the value of the hidden input for a place, to record what is currently there.\n *\n * @param {int} place which place to set the input value for.\n * @param {int} choice the value to set.\n */\n DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {\n this.getRoot().find('input.placeinput.place' + place).val(choice);\n };\n\n /**\n * Get the outer div for this question.\n *\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DragDropOntoImageQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n /**\n * Get drag home for a given choice.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {\n if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {\n return this.getRoot().find('.dragitemgroup' + group +\n ' .draghome.infinite' +\n '.choice' + choice +\n '.group' + group);\n }\n return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);\n };\n\n /**\n * Get an unplaced choice for a particular group.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.\n */\n DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {\n return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);\n };\n\n /**\n * Get the drag that is currently in a given place.\n *\n * @param {int} place the place number.\n * @return {jQuery} the current drag (or an empty jQuery if none).\n */\n DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {\n return this.getRoot().find('.ddarea .draghome.inplace' + place);\n };\n\n /**\n * Return the number of blanks in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of drops.\n */\n DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {\n return this.getRoot().find('.dropzone.group' + group).length;\n };\n\n /**\n * Return the number of choices in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of choices.\n */\n DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {\n return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;\n };\n\n /**\n * Return the number at the end of the CSS class name with the given prefix.\n *\n * @param {jQuery} node\n * @param {String} prefix name prefix\n * @returns {Number|null} the suffix if found, else null.\n */\n DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = node.attr('class');\n if (classes !== '') {\n var classesArr = classes.split(' ');\n for (var index = 0; index < classesArr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesArr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesArr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Get the choice number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the choice number.\n */\n DragDropOntoImageQuestion.prototype.getChoice = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'choice');\n };\n\n /**\n * Given a DOM node that is significant to this question\n * (drag, drop, ...) get the group it belongs to.\n *\n * @param {jQuery} node a DOM node.\n * @returns {Number} the group it belongs to.\n */\n DragDropOntoImageQuestion.prototype.getGroup = function(node) {\n return this.getClassnameNumericSuffix(node, 'group');\n };\n\n /**\n * Get the place number of a drop, or its corresponding hidden input.\n *\n * @param {jQuery} node the DOM node.\n * @returns {Number} the place number.\n */\n DragDropOntoImageQuestion.prototype.getPlace = function(node) {\n return this.getClassnameNumericSuffix(node, 'place');\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropOntoImageQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.dragplaceholder');\n };\n\n /**\n * Get infinite drag clones for given drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} inHome in the home area or not.\n * @returns {jQuery} the drag's clones.\n */\n DragDropOntoImageQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {\n if (inHome) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n }\n return this.getRoot().find('.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n };\n\n /**\n * Get drop for a given drag and place.\n *\n * @param {jQuery} drag the drag.\n * @param {Integer} currentPlace the current place of drag.\n * @returns {jQuery} the drop's clone.\n */\n DragDropOntoImageQuestion.prototype.getDrop = function(drag, currentPlace) {\n return this.getRoot().find('.dropzone.group' + this.getGroup(drag) + '.place' + currentPlace);\n };\n\n /**\n * Handle when the window is resized.\n */\n DragDropOntoImageQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('.ddarea .dropzone').each(function(i, dropNode) {\n $(dropNode)\n .css('left', parseInt($(dropNode).data('originX')) * parseFloat(bgRatio))\n .css('top', parseInt($(dropNode).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(dropNode, 'left top');\n });\n\n this.getRoot().find('div.droparea .draghome').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\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropOntoImageQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DragDropOntoImageQuestion.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 * Calculate z-index value.\n *\n * @returns {number} z-index value\n */\n DragDropOntoImageQuestion.prototype.calculateZIndex = function() {\n var zIndex = 0;\n this.getRoot().find('.ddarea .dropzone, div.droparea .draghome').each(function(i, dropNode) {\n dropNode = $(dropNode);\n // Note that webkit browsers won't return the z-index value from the CSS stylesheet\n // if the element doesn't have a position specified. Instead it'll return \"auto\".\n var itemZIndex = dropNode.css('z-index') ? parseInt(dropNode.css('z-index')) : 0;\n\n if (itemZIndex > zIndex) {\n zIndex = itemZIndex;\n }\n });\n\n return zIndex;\n };\n\n /**\n * Check that the drag is drop to it's clone.\n *\n * @param {jQuery} drag The drag.\n * @param {jQuery} drop The drop.\n * @returns {boolean}\n */\n DragDropOntoImageQuestion.prototype.isDragSameAsDrop = function(drag, drop) {\n return this.getChoice(drag) === this.getChoice(drop) && this.getGroup(drag) === this.getGroup(drop);\n };\n\n /**\n * Singleton object that handles all the DragDropOntoImageQuestions\n * on the page, and deals with event dispatching.\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 drag event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n dragEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation or not.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @method\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 {Array} places data.\n */\n init: function(containerId, readOnly, places) {\n questionManager.questions[containerId] =\n new DragDropOntoImageQuestion(containerId, readOnly, places);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.dragEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('ddimageortext') &&\n !questionContainer.classList.contains('qtype_ddimageortext-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToDrag($(questionContainer).find('.draghome'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $('body')\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',\n questionManager.handleKeyPress)\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)',\n questionManager.handleKeyPress)\n .on('qtype_ddimageortext-dragmoved', questionManager.handleDragMoved);\n $(window).on('resize', function() {\n questionManager.handleWindowResize(false);\n });\n window.addEventListener('beforeprint', function() {\n questionManager.isPrinting = true;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n window.addEventListener('afterprint', function() {\n questionManager.isPrinting = false;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved();\n }, 100);\n },\n\n /**\n * Binding the drag/touch event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToDrag: function(element) {\n // Unbind all the mousedown and touchstart events to prevent double binding.\n element.unbind('mousedown touchstart');\n element.on('mousedown touchstart', questionManager.handleDragStart);\n },\n\n /**\n * Handle mouse down / touch start events on drags.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on drags.\n * @param {KeyboardEvent} e\n */\n handleKeyPress: function(e) {\n if (questionManager.isKeyboardNavigation) {\n return;\n }\n questionManager.isKeyboardNavigation = true;\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * 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 this.handleWindowResize(questionManager.isPrinting);\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 * Handle when drag moved.\n *\n * @param {Event} e the event.\n * @param {jQuery} drag the drag\n * @param {jQuery} target the target\n * @param {DragDropOntoImageQuestion} thisQ the question.\n */\n handleDragMoved: function(e, drag, target, thisQ) {\n drag.removeClass('beingdragged').css('z-index', '');\n drag.css('top', target.position().top).css('left', target.position().left);\n target.after(drag);\n target.removeClass('active');\n if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {\n drag.removeClass('placed').addClass('unplaced');\n drag.removeAttr('tabindex');\n drag.removeData('unplaced');\n drag.css('top', '')\n .css('left', '')\n .css('transform', '');\n if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {\n thisQ.getInfiniteDragClones(drag, true).first().remove();\n }\n } else {\n drag.data('originX', target.data('originX')).data('originY', target.data('originY'));\n thisQ.handleElementScale(drag, 'left top');\n }\n if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {\n drag.focus();\n drag.removeData('isfocus');\n }\n if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {\n target.removeData('isfocus');\n }\n if (questionManager.isKeyboardNavigation) {\n questionManager.isKeyboardNavigation = false;\n }\n if (thisQ.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n }\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/question\n */\n return {\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","keys","FormChangeChecker","DragDropOntoImageQuestion","containerId","readOnly","places","questionAnswer","M","util","js_pending","this","allImagesLoaded","imageLoadingTimeoutId","isPrinting","getRoot","addClass","thisQ","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","clearTimeout","length","setTimeout","setupQuestion","find","not","i","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","resizeAllDragsAndDrops","cloneDrags","positionDragsAndDrops","js_complete","each","node","resizeAllDragsAndDropsInGroup","getClassnameNumericSuffix","group","root","dragHomes","maxWidth","maxHeight","drag","Math","max","ceil","offsetWidth","offsetHeight","left","round","top","floor","css","hasOwnProperty","place","label","text","parseInt","get_string","append","width","height","index","dragHome","placeHolder","clone","removeClass","getChoice","getGroup","before","cloneDragsForOneChoice","hasClass","noOfDrags","noOfDropsInGroup","cloneDrag","offset","bgRatio","dropNode","drop","getPlace","xy","data","handleElementScale","dragNode","currentPlace","removeAttr","inputNode","input","choice","val","unplacedDrag","getUnplacedChoice","hiddenDrag","getDragClone","getInfiniteDragClones","after","questionManager","addEventHandlersToDrag","sendDragToDrop","getQuestionAnsweredValues","result","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","key","handleDragStart","e","target","closest","newIndex","calculateZIndex","prepare","start","setInputValue","hiddenDrop","getDrop","x","y","dragMove","dragEnd","pageX","pageY","highlighted","isPointInDrop","isDragSameAsDrop","placed","placedNode","placedDrag","sendDragHome","oldDrag","getCurrentDragInPlace","focus","attr","animateTo","getDragHome","handleKeyPress","currentDrag","nextDrag","keyCode","space","arrowRight","arrowDown","getNextDrag","arrowLeft","arrowUp","getPreviousDrag","escape","isKeyboardNavigation","preventDefault","numChoices","noOfChoicesInGroup","next","previous","currentPos","targetPos","animate","duration","done","trigger","position","outerWidth","outerHeight","document","getElementById","bgImage","is","slice","prefix","classes","classesArr","split","RegExp","test","match","exec","Number","inHome","handleResize","parseFloat","bgImg","bgImgNaturalWidth","get","naturalWidth","element","type","zIndex","itemZIndex","eventHandlersInitialised","dragEventHandlersInitialised","questions","init","setupEventHandlers","questionContainer","classList","contains","on","handleDragMoved","window","handleWindowResize","addEventListener","fixLayoutIfThingsMoved","unbind","question","getQuestionForEvent","removeData","first","remove","handleFormDirty","currentTarget","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/question\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'core/key_codes',\n 'core_form/changechecker',\n 'core_filters/events',\n], function(\n $,\n dragDrop,\n keys,\n FormChangeChecker,\n filterEvent\n) {\n\n \"use strict\";\n\n /**\n * Initialise one drag-drop onto image 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 {Array} places Information about the drop places.\n * @constructor\n */\n function DragDropOntoImageQuestion(containerId, readOnly, places) {\n this.containerId = containerId;\n this.questionAnswer = {};\n this.questionDragDropWidthHeight = [];\n M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);\n this.places = places;\n this.allImagesLoaded = false;\n this.imageLoadingTimeoutId = null;\n this.isPrinting = false;\n if (readOnly) {\n this.getRoot().addClass('qtype_ddimageortext-readonly');\n }\n\n var thisQ = this;\n this.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n this.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Change all the drags and drops related to the item that has been changed by filter to correct size and content.\n *\n * @param {object} filteredElement the element has been modified by filter.\n */\n DragDropOntoImageQuestion.prototype.changeAllDragsAndDropsToFilteredContent = function(filteredElement) {\n let currentFilteredItem = $(filteredElement);\n const parentIsDD = currentFilteredItem.parent().closest('div').hasClass('placed') ||\n currentFilteredItem.parent().hasClass('draghome');\n const isDD = currentFilteredItem.hasClass('placed') || currentFilteredItem.hasClass('draghome');\n // The filtered element or parent element should a drag or drop item.\n if (!parentIsDD && !isDD) {\n return;\n }\n if (parentIsDD) {\n currentFilteredItem = currentFilteredItem.parent().closest('div');\n }\n if (this.getRoot().find(currentFilteredItem).length <= 0) {\n // If the DD item doesn't belong to this question\n // In case we have multiple questions in the same page.\n return;\n }\n const group = this.getGroup(currentFilteredItem),\n choice = this.getChoice(currentFilteredItem);\n let listOfModifiedDragDrop = [];\n // Get the list of drag and drop item within the same group and choice.\n this.getRoot().find('.group' + group + '.choice' + choice).each(function(i, node) {\n // Same modified item, skip it.\n if ($(node).get(0) === currentFilteredItem.get(0)) {\n return;\n }\n const originalClass = $(node).attr('class');\n const originalStyle = $(node).attr('style');\n // We want to keep all the handler and event for filtered item, so using clone is the only choice.\n const filteredDragDropClone = currentFilteredItem.clone();\n // Sometimes, for the question that has a lot of input groups and unlimited draggable items,\n // this 'clone' process takes longer than usual,it will not add the eventHandler for this cloned drag.\n // We need to make sure to add the eventHandler for the cloned drag too.\n questionManager.addEventHandlersToDrag(filteredDragDropClone);\n // Replace the class and style of the drag drop item we want to replace for the clone.\n filteredDragDropClone.attr('class', originalClass);\n filteredDragDropClone.attr('style', originalStyle);\n // Insert into DOM.\n $(node).before(filteredDragDropClone);\n // Add the item has been replaced to a list so we can remove it later.\n listOfModifiedDragDrop.push(node);\n });\n\n listOfModifiedDragDrop.forEach(function(node) {\n $(node).remove();\n });\n // Save the current height and width.\n const currentHeight = currentFilteredItem.height();\n const currentWidth = currentFilteredItem.width();\n // Set to auto, so we can get the real height and width of the filtered item.\n currentFilteredItem.height('auto');\n currentFilteredItem.width('auto');\n // We need to set display block so we can get height and width.\n // Some browsers can't get the offsetWidth/Height if they are an inline element like span tag.\n if (!filteredElement.offsetWidth || !filteredElement.offsetHeight) {\n filteredElement.classList.add('d-block');\n }\n if (this.questionDragDropWidthHeight[group].maxWidth < Math.ceil(filteredElement.offsetWidth) ||\n this.questionDragDropWidthHeight[group].maxHeight < Math.ceil(0 + filteredElement.offsetHeight)) {\n // Remove the d-block class before calculation.\n filteredElement.classList.remove('d-block');\n // Now resize all the items in the same group if we have new maximum width or height.\n this.resizeAllDragsAndDropsInGroup(group);\n } else {\n currentFilteredItem.height(currentHeight);\n currentFilteredItem.width(currentWidth);\n }\n // Remove the d-block class after resize.\n filteredElement.classList.remove('d-block');\n };\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DragDropOntoImageQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n var thisQ = this;\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n thisQ.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 thisQ.setupQuestion();\n // Wait for all dynamic content loaded by filter to be completed.\n document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => {\n elements.detail.nodes.forEach((element) => {\n thisQ.changeAllDragsAndDropsToFilteredContent(element);\n });\n });\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DragDropOntoImageQuestion.prototype.getNotYetLoadedImages = function() {\n var thisQ = this;\n return this.getRoot().find('.ddarea img').not(function(i, imgNode) {\n return thisQ.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 DragDropOntoImageQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Set up the question, once all images have been loaded.\n */\n DragDropOntoImageQuestion.prototype.setupQuestion = function() {\n this.resizeAllDragsAndDrops();\n this.cloneDrags();\n this.positionDragsAndDrops();\n M.util.js_complete('qtype_ddimageortext-init-' + this.containerId);\n };\n\n /**\n * In each group, resize all the items to be the same size.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDrops = function() {\n var thisQ = this;\n this.getRoot().find('.draghomes > div').each(function(i, node) {\n thisQ.resizeAllDragsAndDropsInGroup(\n thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));\n });\n };\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {\n var root = this.getRoot(),\n dragHomes = root.find(\".draghome.group\" + group),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n dragHomes.each(function(i, drag) {\n maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drag.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n this.questionDragDropWidthHeight[group] = {maxWidth, maxHeight};\n\n // Set each drag home to that size.\n dragHomes.each(function(i, drag) {\n $(drag).width(maxWidth).height(maxHeight).css('lineHeight', maxHeight + 'px');\n });\n\n // Create the drops and make them the right size.\n for (var i in this.places) {\n if (!this.places.hasOwnProperty((i))) {\n continue;\n }\n var place = this.places[i],\n label = place.text;\n if (parseInt(place.group) !== group) {\n continue;\n }\n if (label === '') {\n label = M.util.get_string('blank', 'qtype_ddimageortext');\n }\n if (root.find('.dropzones .dropzone.group' + place.group + '.place' + i).length === 0) {\n root.find('.dropzones').append('
' +\n '' + label + ' 
');\n }\n root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);\n }\n };\n\n /**\n * Invisible 'drag homes' are output by the renderer. These have the same properties\n * as the drag items but are invisible. We clone these invisible elements to make the\n * actual drag items.\n */\n DragDropOntoImageQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n thisQ.getRoot().find('.draghome').each(function(index, dragHome) {\n var drag = $(dragHome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('draghome choice' +\n thisQ.getChoice(drag) + ' group' +\n thisQ.getGroup(drag) + ' dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Clone drag item for one choice.\n *\n * @param {jQuery} dragHome the drag home to clone.\n */\n DragDropOntoImageQuestion.prototype.cloneDragsForOneChoice = function(dragHome) {\n if (dragHome.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(dragHome));\n for (var i = 0; i < noOfDrags; i++) {\n this.cloneDrag(dragHome);\n }\n } else {\n this.cloneDrag(dragHome);\n }\n };\n\n /**\n * Clone drag item.\n *\n * @param {jQuery} dragHome\n */\n DragDropOntoImageQuestion.prototype.cloneDrag = function(dragHome) {\n var drag = dragHome.clone();\n drag.removeClass('draghome')\n .addClass('drag unplaced moodle-has-zindex')\n .offset(dragHome.offset());\n this.getRoot().find('.dragitems').append(drag);\n };\n\n /**\n * Update the position of drags.\n */\n DragDropOntoImageQuestion.prototype.positionDragsAndDrops = function() {\n var thisQ = this,\n root = this.getRoot(),\n bgRatio = this.bgRatio();\n\n // Move the drops into position.\n root.find('.ddarea .dropzone').each(function(i, dropNode) {\n var drop = $(dropNode),\n place = thisQ.places[thisQ.getPlace(drop)];\n // The xy values come from PHP as strings, so we need parseInt to stop JS doing string concatenation.\n drop.css('left', parseInt(place.xy[0]) * bgRatio)\n .css('top', parseInt(place.xy[1]) * bgRatio);\n drop.data('originX', parseInt(place.xy[0]))\n .data('originY', parseInt(place.xy[1]));\n thisQ.handleElementScale(drop, 'left top');\n });\n\n // First move all items back home.\n root.find('.draghome').not('.dragplaceholder').each(function(i, dragNode) {\n var drag = $(dragNode),\n currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');\n drag.addClass('unplaced')\n .removeClass('placed');\n drag.removeAttr('tabindex');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n });\n\n // Then place the ones that should be placed.\n root.find('input.placeinput').each(function(i, inputNode) {\n var input = $(inputNode),\n choice = input.val();\n if (choice.length === 0 || (choice.length > 0 && choice === '0')) {\n // No item in this place.\n return;\n }\n\n var place = thisQ.getPlace(input);\n // Get the unplaced drag.\n var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);\n // Get the clone of the drag.\n var hiddenDrag = thisQ.getDragClone(unplacedDrag);\n if (hiddenDrag.length) {\n if (unplacedDrag.hasClass('infinite')) {\n var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));\n var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = unplacedDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n // Sometimes, for the question that has a lot of input groups and unlimited draggable items,\n // this 'clone' process takes longer than usual, so the questionManager.init() method\n // will not add the eventHandler for this cloned drag.\n // We need to make sure to add the eventHandler for the cloned drag too.\n questionManager.addEventHandlersToDrag(cloneDrag);\n } else {\n hiddenDrag.addClass('active');\n }\n } else {\n hiddenDrag.addClass('active');\n }\n }\n\n // Send the drag to drop.\n var drop = root.find('.dropzone.place' + place);\n thisQ.sendDragToDrop(unplacedDrag, drop);\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropOntoImageQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.placeinput').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropOntoImageQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Handles the start of dragging an item.\n *\n * @param {Event} e the touch start or mouse down event.\n */\n DragDropOntoImageQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n drag = $(e.target).closest('.draghome'),\n currentIndex = this.calculateZIndex(),\n newIndex = currentIndex + 2;\n\n var info = dragDrop.prepare(e);\n if (!info.start || drag.hasClass('beingdragged')) {\n return;\n }\n\n drag.addClass('beingdragged').css('transform', '').css('z-index', newIndex);\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n this.setInputValue(currentPlace, 0);\n drag.removeClass('inplace' + currentPlace);\n var hiddenDrop = thisQ.getDrop(drag, currentPlace);\n if (hiddenDrop.length) {\n hiddenDrop.addClass('active');\n drag.offset(hiddenDrop.offset());\n }\n } else {\n var hiddenDrag = thisQ.getDragClone(drag);\n if (hiddenDrag.length) {\n if (drag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(thisQ.getGroup(drag));\n var cloneDrags = this.getInfiniteDragClones(drag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = drag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n drag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n }\n }\n\n dragDrop.start(e, drag, function(x, y, drag) {\n thisQ.dragMove(x, y, drag);\n }, function(x, y, drag) {\n thisQ.dragEnd(x, y, drag);\n });\n };\n\n /**\n * Called whenever the currently dragged items moves.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragMove = function(pageX, pageY, drag) {\n var thisQ = this,\n highlighted = false;\n this.getRoot().find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted) {\n highlighted = true;\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n this.getRoot().find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop) && !highlighted && !thisQ.isDragSameAsDrop(drag, drop)) {\n highlighted = true;\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n };\n\n /**\n * Called when user drops a drag item.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.dragEnd = function(pageX, pageY, drag) {\n var thisQ = this,\n root = this.getRoot(),\n placed = false;\n\n // Looking for drag that was dropped on a dropzone.\n root.find('.dropzone.group' + this.getGroup(drag)).each(function(i, dropNode) {\n var drop = $(dropNode);\n if (!thisQ.isPointInDrop(pageX, pageY, drop)) {\n // Not this drop.\n return true;\n }\n\n // Now put this drag into the drop.\n drop.removeClass('valid-drag-over-drop');\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n\n if (!placed) {\n // Looking for drag that was dropped on a placed drag.\n root.find('.draghome.placed.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, placedNode) {\n var placedDrag = $(placedNode);\n if (!thisQ.isPointInDrop(pageX, pageY, placedDrag) || thisQ.isDragSameAsDrop(drag, placedDrag)) {\n // Not this placed drag.\n return true;\n }\n\n // Now put this drag into the drop.\n placedDrag.removeClass('valid-drag-over-drop');\n var currentPlace = thisQ.getClassnameNumericSuffix(placedDrag, 'inplace');\n var drop = thisQ.getDrop(drag, currentPlace);\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n }\n\n if (!placed) {\n this.sendDragHome(drag);\n }\n };\n\n /**\n * Animate a drag item into a given place (or back home).\n *\n * @param {jQuery|null} drag the item to place. If null, clear the place.\n * @param {jQuery} drop the place to put it.\n */\n DragDropOntoImageQuestion.prototype.sendDragToDrop = function(drag, drop) {\n // Is there already a drag in this drop? if so, evict it.\n var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));\n if (oldDrag.length !== 0) {\n oldDrag.addClass('beingdragged');\n oldDrag.offset(oldDrag.offset());\n var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');\n var hiddenDrop = this.getDrop(oldDrag, currentPlace);\n hiddenDrop.addClass('active');\n this.sendDragHome(oldDrag);\n }\n\n if (drag.length === 0) {\n this.setInputValue(this.getPlace(drop), 0);\n if (drop.data('isfocus')) {\n drop.focus();\n }\n } else {\n this.setInputValue(this.getPlace(drop), this.getChoice(drag));\n drag.removeClass('unplaced')\n .addClass('placed inplace' + this.getPlace(drop));\n drag.attr('tabindex', 0);\n this.animateTo(drag, drop);\n }\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropOntoImageQuestion.prototype.sendDragHome = function(drag) {\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n drag.data('unplaced', true);\n\n this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));\n };\n\n /**\n * Handles keyboard events on drops.\n *\n * Drops are focusable. Once focused, right/down/space switches to the next choice, and\n * left/up switches to the previous. Escape clear.\n *\n * @param {KeyboardEvent} e\n */\n DragDropOntoImageQuestion.prototype.handleKeyPress = function(e) {\n var drop = $(e.target).closest('.dropzone');\n if (drop.length === 0) {\n var placedDrag = $(e.target);\n var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');\n if (currentPlace !== null) {\n drop = this.getDrop(placedDrag, currentPlace);\n }\n }\n var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),\n nextDrag = $();\n\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.escape:\n questionManager.isKeyboardNavigation = false;\n break;\n\n default:\n questionManager.isKeyboardNavigation = false;\n return; // To avoid the preventDefault below.\n }\n\n if (nextDrag.length) {\n nextDrag.data('isfocus', true);\n nextDrag.addClass('beingdragged');\n var hiddenDrag = this.getDragClone(nextDrag);\n if (hiddenDrag.length) {\n if (nextDrag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));\n var cloneDrags = this.getInfiniteDragClones(nextDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = nextDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n nextDrag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n }\n } else {\n drop.data('isfocus', true);\n }\n\n e.preventDefault();\n this.sendDragToDrop(nextDrag, drop);\n };\n\n /**\n * Choose the next drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getNextDrag = function(group, drag) {\n var choice,\n numChoices = this.noOfChoicesInGroup(group);\n\n if (drag.length === 0) {\n choice = 1; // Was empty, so we want to select the first choice.\n } else {\n choice = this.getChoice(drag) + 1;\n }\n\n var next = this.getUnplacedChoice(group, choice);\n while (next.length === 0 && choice < numChoices) {\n choice++;\n next = this.getUnplacedChoice(group, choice);\n }\n\n return next;\n };\n\n /**\n * Choose the previous drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropOntoImageQuestion.prototype.getPreviousDrag = function(group, drag) {\n var choice;\n\n if (drag.length === 0) {\n choice = this.noOfChoicesInGroup(group);\n } else {\n choice = this.getChoice(drag) - 1;\n }\n\n var previous = this.getUnplacedChoice(group, choice);\n while (previous.length === 0 && choice > 1) {\n choice--;\n previous = this.getUnplacedChoice(group, choice);\n }\n\n // Does this choice exist?\n return previous;\n };\n\n /**\n * Animate an object to the given destination.\n *\n * @param {jQuery} drag the element to be animated.\n * @param {jQuery} target element marking the place to move it to.\n */\n DragDropOntoImageQuestion.prototype.animateTo = function(drag, target) {\n var currentPos = drag.offset(),\n targetPos = target.offset(),\n thisQ = this;\n\n M.util.js_pending('qtype_ddimageortext-animate-' + thisQ.containerId);\n // Animate works in terms of CSS position, whereas locating an object\n // on the page works best with jQuery offset() function. So, to get\n // the right target position, we work out the required change in\n // offset() and then add that to the current CSS position.\n drag.animate(\n {\n left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,\n top: parseInt(drag.css('top')) + targetPos.top - currentPos.top\n },\n {\n duration: 'fast',\n done: function() {\n $('body').trigger('qtype_ddimageortext-dragmoved', [drag, target, thisQ]);\n M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId);\n }\n }\n );\n };\n\n /**\n * Detect if a point is inside a given DOM node.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drop the node to check (typically a drop).\n * @return {boolean} whether the point is inside the node.\n */\n DragDropOntoImageQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {\n var position = drop.offset();\n if (drop.hasClass('draghome')) {\n return pageX >= position.left && pageX < position.left + drop.outerWidth()\n && pageY >= position.top && pageY < position.top + drop.outerHeight();\n }\n return pageX >= position.left && pageX < position.left + drop.width()\n && pageY >= position.top && pageY < position.top + drop.height();\n };\n\n /**\n * Set the value of the hidden input for a place, to record what is currently there.\n *\n * @param {int} place which place to set the input value for.\n * @param {int} choice the value to set.\n */\n DragDropOntoImageQuestion.prototype.setInputValue = function(place, choice) {\n this.getRoot().find('input.placeinput.place' + place).val(choice);\n };\n\n /**\n * Get the outer div for this question.\n *\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DragDropOntoImageQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n /**\n * Get drag home for a given choice.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} containing that div.\n */\n DragDropOntoImageQuestion.prototype.getDragHome = function(group, choice) {\n if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {\n return this.getRoot().find('.dragitemgroup' + group +\n ' .draghome.infinite' +\n '.choice' + choice +\n '.group' + group);\n }\n return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);\n };\n\n /**\n * Get an unplaced choice for a particular group.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.\n */\n DragDropOntoImageQuestion.prototype.getUnplacedChoice = function(group, choice) {\n return this.getRoot().find('.ddarea .draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);\n };\n\n /**\n * Get the drag that is currently in a given place.\n *\n * @param {int} place the place number.\n * @return {jQuery} the current drag (or an empty jQuery if none).\n */\n DragDropOntoImageQuestion.prototype.getCurrentDragInPlace = function(place) {\n return this.getRoot().find('.ddarea .draghome.inplace' + place);\n };\n\n /**\n * Return the number of blanks in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of drops.\n */\n DragDropOntoImageQuestion.prototype.noOfDropsInGroup = function(group) {\n return this.getRoot().find('.dropzone.group' + group).length;\n };\n\n /**\n * Return the number of choices in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of choices.\n */\n DragDropOntoImageQuestion.prototype.noOfChoicesInGroup = function(group) {\n return this.getRoot().find('.dragitemgroup' + group + ' .draghome').length;\n };\n\n /**\n * Return the number at the end of the CSS class name with the given prefix.\n *\n * @param {jQuery} node\n * @param {String} prefix name prefix\n * @returns {Number|null} the suffix if found, else null.\n */\n DragDropOntoImageQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = node.attr('class');\n if (classes !== '') {\n var classesArr = classes.split(' ');\n for (var index = 0; index < classesArr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesArr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesArr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Get the choice number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the choice number.\n */\n DragDropOntoImageQuestion.prototype.getChoice = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'choice');\n };\n\n /**\n * Given a DOM node that is significant to this question\n * (drag, drop, ...) get the group it belongs to.\n *\n * @param {jQuery} node a DOM node.\n * @returns {Number} the group it belongs to.\n */\n DragDropOntoImageQuestion.prototype.getGroup = function(node) {\n return this.getClassnameNumericSuffix(node, 'group');\n };\n\n /**\n * Get the place number of a drop, or its corresponding hidden input.\n *\n * @param {jQuery} node the DOM node.\n * @returns {Number} the place number.\n */\n DragDropOntoImageQuestion.prototype.getPlace = function(node) {\n return this.getClassnameNumericSuffix(node, 'place');\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropOntoImageQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.dragplaceholder');\n };\n\n /**\n * Get infinite drag clones for given drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} inHome in the home area or not.\n * @returns {jQuery} the drag's clones.\n */\n DragDropOntoImageQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {\n if (inHome) {\n return this.getRoot().find('.dragitemgroup' +\n this.getGroup(drag) +\n ' .draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n }\n return this.getRoot().find('.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n };\n\n /**\n * Get drop for a given drag and place.\n *\n * @param {jQuery} drag the drag.\n * @param {Integer} currentPlace the current place of drag.\n * @returns {jQuery} the drop's clone.\n */\n DragDropOntoImageQuestion.prototype.getDrop = function(drag, currentPlace) {\n return this.getRoot().find('.dropzone.group' + this.getGroup(drag) + '.place' + currentPlace);\n };\n\n /**\n * Handle when the window is resized.\n */\n DragDropOntoImageQuestion.prototype.handleResize = function() {\n var thisQ = this,\n bgRatio = this.bgRatio();\n if (this.isPrinting) {\n bgRatio = 1;\n }\n\n this.getRoot().find('.ddarea .dropzone').each(function(i, dropNode) {\n $(dropNode)\n .css('left', parseInt($(dropNode).data('originX')) * parseFloat(bgRatio))\n .css('top', parseInt($(dropNode).data('originY')) * parseFloat(bgRatio));\n thisQ.handleElementScale(dropNode, 'left top');\n });\n\n this.getRoot().find('div.droparea .draghome').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\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropOntoImageQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DragDropOntoImageQuestion.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 * Calculate z-index value.\n *\n * @returns {number} z-index value\n */\n DragDropOntoImageQuestion.prototype.calculateZIndex = function() {\n var zIndex = 0;\n this.getRoot().find('.ddarea .dropzone, div.droparea .draghome').each(function(i, dropNode) {\n dropNode = $(dropNode);\n // Note that webkit browsers won't return the z-index value from the CSS stylesheet\n // if the element doesn't have a position specified. Instead it'll return \"auto\".\n var itemZIndex = dropNode.css('z-index') ? parseInt(dropNode.css('z-index')) : 0;\n\n if (itemZIndex > zIndex) {\n zIndex = itemZIndex;\n }\n });\n\n return zIndex;\n };\n\n /**\n * Check that the drag is drop to it's clone.\n *\n * @param {jQuery} drag The drag.\n * @param {jQuery} drop The drop.\n * @returns {boolean}\n */\n DragDropOntoImageQuestion.prototype.isDragSameAsDrop = function(drag, drop) {\n return this.getChoice(drag) === this.getChoice(drop) && this.getGroup(drag) === this.getGroup(drop);\n };\n\n /**\n * Singleton object that handles all the DragDropOntoImageQuestions\n * on the page, and deals with event dispatching.\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 drag event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n dragEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation or not.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @method\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 {Array} places data.\n */\n init: function(containerId, readOnly, places) {\n questionManager.questions[containerId] =\n new DragDropOntoImageQuestion(containerId, readOnly, places);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.dragEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('ddimageortext') &&\n !questionContainer.classList.contains('qtype_ddimageortext-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToDrag($(questionContainer).find('.draghome'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $('body')\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',\n questionManager.handleKeyPress)\n .on('keydown',\n '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)',\n questionManager.handleKeyPress)\n .on('qtype_ddimageortext-dragmoved', questionManager.handleDragMoved);\n $(window).on('resize', function() {\n questionManager.handleWindowResize(false);\n });\n window.addEventListener('beforeprint', function() {\n questionManager.isPrinting = true;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n window.addEventListener('afterprint', function() {\n questionManager.isPrinting = false;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved();\n }, 100);\n },\n\n /**\n * Binding the drag/touch event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToDrag: function(element) {\n // Unbind all the mousedown and touchstart events to prevent double binding.\n element.unbind('mousedown touchstart');\n element.on('mousedown touchstart', questionManager.handleDragStart);\n },\n\n /**\n * Handle mouse down / touch start events on drags.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on drags.\n * @param {KeyboardEvent} e\n */\n handleKeyPress: function(e) {\n if (questionManager.isKeyboardNavigation) {\n return;\n }\n questionManager.isKeyboardNavigation = true;\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * 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 this.handleWindowResize(questionManager.isPrinting);\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 * Handle when drag moved.\n *\n * @param {Event} e the event.\n * @param {jQuery} drag the drag\n * @param {jQuery} target the target\n * @param {DragDropOntoImageQuestion} thisQ the question.\n */\n handleDragMoved: function(e, drag, target, thisQ) {\n drag.removeClass('beingdragged').css('z-index', '');\n drag.css('top', target.position().top).css('left', target.position().left);\n target.after(drag);\n target.removeClass('active');\n if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {\n drag.removeClass('placed').addClass('unplaced');\n drag.removeAttr('tabindex');\n drag.removeData('unplaced');\n drag.css('top', '')\n .css('left', '')\n .css('transform', '');\n if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {\n thisQ.getInfiniteDragClones(drag, true).first().remove();\n }\n } else {\n drag.data('originX', target.data('originX')).data('originY', target.data('originY'));\n thisQ.handleElementScale(drag, 'left top');\n }\n if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {\n drag.focus();\n drag.removeData('isfocus');\n }\n if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {\n target.removeData('isfocus');\n }\n if (questionManager.isKeyboardNavigation) {\n questionManager.isKeyboardNavigation = false;\n }\n if (thisQ.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n }\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropOntoImageQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddimageortext').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/question\n */\n return {\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","keys","FormChangeChecker","filterEvent","DragDropOntoImageQuestion","containerId","readOnly","places","questionAnswer","questionDragDropWidthHeight","M","util","js_pending","this","allImagesLoaded","imageLoadingTimeoutId","isPrinting","getRoot","addClass","thisQ","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","changeAllDragsAndDropsToFilteredContent","filteredElement","currentFilteredItem","parentIsDD","parent","closest","hasClass","isDD","find","length","group","getGroup","choice","getChoice","listOfModifiedDragDrop","each","i","node","get","originalClass","attr","originalStyle","filteredDragDropClone","clone","questionManager","addEventHandlersToDrag","before","push","forEach","remove","currentHeight","height","currentWidth","width","offsetWidth","offsetHeight","classList","add","maxWidth","Math","ceil","maxHeight","resizeAllDragsAndDropsInGroup","clearTimeout","setTimeout","setupQuestion","document","addEventListener","eventTypes","filterContentRenderingComplete","elements","detail","nodes","element","not","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","resizeAllDragsAndDrops","cloneDrags","positionDragsAndDrops","js_complete","getClassnameNumericSuffix","root","dragHomes","drag","max","css","hasOwnProperty","place","label","text","parseInt","get_string","append","index","dragHome","placeHolder","removeClass","cloneDragsForOneChoice","noOfDrags","noOfDropsInGroup","cloneDrag","offset","bgRatio","dropNode","drop","getPlace","xy","data","handleElementScale","dragNode","currentPlace","removeAttr","inputNode","input","val","unplacedDrag","getUnplacedChoice","hiddenDrag","getDragClone","getInfiniteDragClones","after","sendDragToDrop","getQuestionAnsweredValues","result","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","key","handleDragStart","e","target","newIndex","calculateZIndex","prepare","start","setInputValue","hiddenDrop","getDrop","x","y","dragMove","dragEnd","pageX","pageY","highlighted","isPointInDrop","isDragSameAsDrop","placed","placedNode","placedDrag","sendDragHome","oldDrag","getCurrentDragInPlace","focus","animateTo","getDragHome","handleKeyPress","currentDrag","nextDrag","keyCode","space","arrowRight","arrowDown","getNextDrag","arrowLeft","arrowUp","getPreviousDrag","escape","isKeyboardNavigation","preventDefault","numChoices","noOfChoicesInGroup","next","previous","currentPos","targetPos","animate","left","top","duration","done","trigger","position","outerWidth","outerHeight","getElementById","bgImage","is","slice","prefix","classes","classesArr","split","RegExp","test","match","exec","Number","inHome","handleResize","parseFloat","bgImg","bgImgNaturalWidth","naturalWidth","type","zIndex","itemZIndex","eventHandlersInitialised","dragEventHandlersInitialised","questions","init","setupEventHandlers","questionContainer","contains","on","handleDragMoved","window","handleWindowResize","fixLayoutIfThingsMoved","unbind","question","getQuestionForEvent","removeData","first","handleFormDirty","currentTarget","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file diff --git a/question/type/ddimageortext/amd/src/question.js b/question/type/ddimageortext/amd/src/question.js index bfc1cecf395b6..05239c3169bd5 100644 --- a/question/type/ddimageortext/amd/src/question.js +++ b/question/type/ddimageortext/amd/src/question.js @@ -24,12 +24,14 @@ define([ 'jquery', 'core/dragdrop', 'core/key_codes', - 'core_form/changechecker' + 'core_form/changechecker', + 'core_filters/events', ], function( $, dragDrop, keys, - FormChangeChecker + FormChangeChecker, + filterEvent ) { "use strict"; @@ -45,6 +47,7 @@ define([ function DragDropOntoImageQuestion(containerId, readOnly, places) { this.containerId = containerId; this.questionAnswer = {}; + this.questionDragDropWidthHeight = []; M.util.js_pending('qtype_ddimageortext-init-' + this.containerId); this.places = places; this.allImagesLoaded = false; @@ -61,6 +64,82 @@ define([ this.waitForAllImagesToBeLoaded(); } + /** + * Change all the drags and drops related to the item that has been changed by filter to correct size and content. + * + * @param {object} filteredElement the element has been modified by filter. + */ + DragDropOntoImageQuestion.prototype.changeAllDragsAndDropsToFilteredContent = function(filteredElement) { + let currentFilteredItem = $(filteredElement); + const parentIsDD = currentFilteredItem.parent().closest('div').hasClass('placed') || + currentFilteredItem.parent().hasClass('draghome'); + const isDD = currentFilteredItem.hasClass('placed') || currentFilteredItem.hasClass('draghome'); + // The filtered element or parent element should a drag or drop item. + if (!parentIsDD && !isDD) { + return; + } + if (parentIsDD) { + currentFilteredItem = currentFilteredItem.parent().closest('div'); + } + if (this.getRoot().find(currentFilteredItem).length <= 0) { + // If the DD item doesn't belong to this question + // In case we have multiple questions in the same page. + return; + } + const group = this.getGroup(currentFilteredItem), + choice = this.getChoice(currentFilteredItem); + let listOfModifiedDragDrop = []; + // Get the list of drag and drop item within the same group and choice. + this.getRoot().find('.group' + group + '.choice' + choice).each(function(i, node) { + // Same modified item, skip it. + if ($(node).get(0) === currentFilteredItem.get(0)) { + return; + } + const originalClass = $(node).attr('class'); + const originalStyle = $(node).attr('style'); + // We want to keep all the handler and event for filtered item, so using clone is the only choice. + const filteredDragDropClone = currentFilteredItem.clone(); + // Sometimes, for the question that has a lot of input groups and unlimited draggable items, + // this 'clone' process takes longer than usual,it will not add the eventHandler for this cloned drag. + // We need to make sure to add the eventHandler for the cloned drag too. + questionManager.addEventHandlersToDrag(filteredDragDropClone); + // Replace the class and style of the drag drop item we want to replace for the clone. + filteredDragDropClone.attr('class', originalClass); + filteredDragDropClone.attr('style', originalStyle); + // Insert into DOM. + $(node).before(filteredDragDropClone); + // Add the item has been replaced to a list so we can remove it later. + listOfModifiedDragDrop.push(node); + }); + + listOfModifiedDragDrop.forEach(function(node) { + $(node).remove(); + }); + // Save the current height and width. + const currentHeight = currentFilteredItem.height(); + const currentWidth = currentFilteredItem.width(); + // Set to auto, so we can get the real height and width of the filtered item. + currentFilteredItem.height('auto'); + currentFilteredItem.width('auto'); + // We need to set display block so we can get height and width. + // Some browsers can't get the offsetWidth/Height if they are an inline element like span tag. + if (!filteredElement.offsetWidth || !filteredElement.offsetHeight) { + filteredElement.classList.add('d-block'); + } + if (this.questionDragDropWidthHeight[group].maxWidth < Math.ceil(filteredElement.offsetWidth) || + this.questionDragDropWidthHeight[group].maxHeight < Math.ceil(0 + filteredElement.offsetHeight)) { + // Remove the d-block class before calculation. + filteredElement.classList.remove('d-block'); + // Now resize all the items in the same group if we have new maximum width or height. + this.resizeAllDragsAndDropsInGroup(group); + } else { + currentFilteredItem.height(currentHeight); + currentFilteredItem.width(currentWidth); + } + // Remove the d-block class after resize. + filteredElement.classList.remove('d-block'); + }; + /** * Waits until all images are loaded before calling setupQuestion(). * @@ -94,6 +173,12 @@ define([ // We now have all images. Carry on, but only after giving the layout a chance to settle down. this.allImagesLoaded = true; thisQ.setupQuestion(); + // Wait for all dynamic content loaded by filter to be completed. + document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => { + elements.detail.nodes.forEach((element) => { + thisQ.changeAllDragsAndDropsToFilteredContent(element); + }); + }); }; /** @@ -135,7 +220,7 @@ define([ var thisQ = this; this.getRoot().find('.draghomes > div').each(function(i, node) { thisQ.resizeAllDragsAndDropsInGroup( - thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup')); + thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup')); }); }; @@ -146,7 +231,7 @@ define([ */ DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) { var root = this.getRoot(), - dragHomes = root.find('.dragitemgroup' + group + ' .draghome'), + dragHomes = root.find(".draghome.group" + group), maxWidth = 0, maxHeight = 0; @@ -159,18 +244,11 @@ define([ // The size we will want to set is a bit bigger than this. maxWidth += 10; maxHeight += 10; + this.questionDragDropWidthHeight[group] = {maxWidth, maxHeight}; // Set each drag home to that size. dragHomes.each(function(i, drag) { - var left = Math.round((maxWidth - drag.offsetWidth) / 2), - top = Math.floor((maxHeight - drag.offsetHeight) / 2); - // Set top and left padding so the item is centred. - $(drag).css({ - 'padding-left': left + 'px', - 'padding-right': (maxWidth - drag.offsetWidth - left) + 'px', - 'padding-top': top + 'px', - 'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px' - }); + $(drag).width(maxWidth).height(maxHeight).css('lineHeight', maxHeight + 'px'); }); // Create the drops and make them the right size. @@ -186,9 +264,11 @@ define([ if (label === '') { label = M.util.get_string('blank', 'qtype_ddimageortext'); } - root.find('.dropzones').append('
' + + if (root.find('.dropzones .dropzone.group' + place.group + '.place' + i).length === 0) { + root.find('.dropzones').append('
' + '' + label + ' 
'); + } root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2); } }; diff --git a/question/type/ddimageortext/rendererbase.php b/question/type/ddimageortext/rendererbase.php index 640600090a4b7..cb201cd397d8b 100644 --- a/question/type/ddimageortext/rendererbase.php +++ b/question/type/ddimageortext/rendererbase.php @@ -52,7 +52,6 @@ public function clear_wrong(question_attempt $qa) { public function formulation_and_controls(question_attempt $qa, question_display_options $options) { - $question = $qa->get_question(); $response = $qa->get_last_qt_data(); @@ -92,6 +91,7 @@ public function formulation_and_controls(question_attempt $qa, $classes[] = 'infinite'; } if ($dragimageurl === null) { + $dragimage->text = question_utils::format_question_fragment($dragimage->text, $this->page->context); $dragimagehomesgroup .= html_writer::div($dragimage->text, join(' ', $classes), ['src' => $dragimageurl]); } else { $dragimagehomesgroup .= html_writer::img($dragimageurl, $dragimage->text, ['class' => join(' ', $classes)]); diff --git a/question/type/ddimageortext/styles.css b/question/type/ddimageortext/styles.css index 8334a9d00bcfc..98ff22e892a7e 100644 --- a/question/type/ddimageortext/styles.css +++ b/question/type/ddimageortext/styles.css @@ -85,6 +85,10 @@ form.mform fieldset#id_previewareaheader .droppreview { display: none; } +.que.ddimageortext .MathJax_Display { + margin: 0; +} + .que.ddimageortext .draghomes .draghome.dragplaceholder.active { visibility: hidden; display: inline-block; diff --git a/question/type/ddimageortext/tests/behat/preview.feature b/question/type/ddimageortext/tests/behat/preview.feature index bb5c5c868de5b..a40b0ef9405f8 100644 --- a/question/type/ddimageortext/tests/behat/preview.feature +++ b/question/type/ddimageortext/tests/behat/preview.feature @@ -18,8 +18,9 @@ Feature: Preview a drag-drop onto image question | contextlevel | reference | name | | Course | C1 | Test questions | And the following "questions" exist: - | questioncategory | qtype | name | template | - | Test questions | ddimageortext | Drag onto image | xsection | + | questioncategory | qtype | name | template | + | Test questions | ddimageortext | Drag onto image | xsection | + | Test questions | ddimageortext | Drag to mathjax equation | mathjax | @javascript @_bug_phantomjs Scenario: Preview a question using the mouse. @@ -53,3 +54,12 @@ Feature: Preview a drag-drop onto image question And I press "Submit and finish" Then the state of "Identify the features" question is shown as "Correct" And I should see "Mark 1.00 out of 1.00" + + @javascript + Scenario: Preview a drag-drop into image question with mathjax question. + Given the "mathjaxloader" filter is "on" + And the "mathjaxloader" filter applies to "content and headings" + And I am on the "Drag to mathjax equation" "core_question > preview" page logged in as teacher + And I press "Fill in correct responses" + When I press "Submit and finish" + Then ".filter_mathjaxloader_equation" "css_element" should exist in the ".draghome" "css_element" diff --git a/question/type/ddimageortext/tests/helper.php b/question/type/ddimageortext/tests/helper.php index 367d2c0d801be..8f0ee8810fdbd 100644 --- a/question/type/ddimageortext/tests/helper.php +++ b/question/type/ddimageortext/tests/helper.php @@ -34,7 +34,7 @@ */ class qtype_ddimageortext_test_helper extends question_test_helper { public function get_test_questions() { - return array('fox', 'maths', 'xsection', 'mixedlang'); + return ['fox', 'maths', 'xsection', 'mixedlang', 'mathjax']; } /** @@ -251,6 +251,54 @@ public function get_ddimageortext_question_form_data_xsection() { return $fromform; } + /** + * Get data required to save a drag-drop into text question where the the answer contain equation + * + * + * @return stdClass data to create a ddwtos question. + */ + public function get_ddimageortext_question_form_data_mathjax() { + global $CFG, $USER; + $fromform = new stdClass(); + + $bgdraftitemid = 0; + file_prepare_draft_area($bgdraftitemid, null, null, null, null); + $fs = get_file_storage(); + $filerecord = new stdClass(); + $filerecord->contextid = context_user::instance($USER->id)->id; + $filerecord->component = 'user'; + $filerecord->filearea = 'draft'; + $filerecord->itemid = $bgdraftitemid; + $filerecord->filepath = '/'; + $filerecord->filename = 'oceanfloorbase.jpg'; + $fs->create_file_from_pathname($filerecord, $CFG->dirroot . + '/question/type/ddimageortext/tests/fixtures/oceanfloorbase.jpg'); + $fromform->name = 'Drag-and-drop words into image question with equation'; + $fromform->questiontext = ['text' => 'Fill in the correct mathjax equation: y = 2, x =4', 'format' => FORMAT_HTML]; + $fromform->defaultmark = 1.0; + $fromform->generalfeedback = ['text' => 'The right answer is: "y = x^2"', 'format' => FORMAT_HTML]; + $fromform->drags = [ + ['dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'], + ['dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'], + ]; + $fromform->bgimage = $bgdraftitemid; + $fromform->dragitem = [0, 0]; + $fromform->draglabel = + [ + '$$ y = x^2 $$', + '$$ y = x^5 $$', + ]; + $fromform->drops = [ + ['xleft' => '53', 'ytop' => '17', 'choice' => '1', 'droplabel' => ''], + ['xleft' => '172', 'ytop' => '2', 'choice' => '2', 'droplabel' => ''], + ]; + test_question_maker::set_standard_combined_feedback_form_data($fromform); + $fromform->shownumcorrect = 0; + $fromform->penalty = 0.3333333; + $fromform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY; + return $fromform; + } + /** * Make a test question where the drag items are a different language than the main question text. * From 85c7492db2454ae19f6b4e0f1f3f38295cc6d3a9 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Fri, 3 May 2024 09:20:05 +0700 Subject: [PATCH 004/119] MDL-78662 ddmarker: Allow answer to support filter dynamic content. --- .../type/ddmarker/amd/build/question.min.js | 2 +- .../ddmarker/amd/build/question.min.js.map | 2 +- question/type/ddmarker/amd/src/question.js | 66 ++++++++++++++++++- question/type/ddmarker/renderer.php | 10 ++- .../type/ddmarker/tests/behat/preview.feature | 14 +++- question/type/ddmarker/tests/helper.php | 46 ++++++++++++- 6 files changed, 129 insertions(+), 11 deletions(-) diff --git a/question/type/ddmarker/amd/build/question.min.js b/question/type/ddmarker/amd/build/question.min.js index 0d5612c648076..53ae4787ad366 100644 --- a/question/type/ddmarker/amd/build/question.min.js +++ b/question/type/ddmarker/amd/build/question.min.js @@ -5,6 +5,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_ddmarker/question",["jquery","core/dragdrop","qtype_ddmarker/shapes","core/key_codes","core_form/changechecker"],(function($,dragDrop,Shapes,keys,FormChangeChecker){function DragDropMarkersQuestion(containerId,readOnly,visibleDropZones){var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.shapes=[],this.shapeSVGs=[],this.isPrinting=!1,this.questionAnswer={},readOnly&&this.getRoot().addClass("qtype_ddmarker-readonly"),thisQ.allImagesLoaded=!1,thisQ.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),thisQ.waitForAllImagesToBeLoaded()}DragDropMarkersQuestion.prototype.drawDropzones=function(){if(this.visibleDropZones.length>0){var bgImage=this.bgImage();this.getRoot().find("div.dropzones").html('');for(var svg=this.getRoot().find("svg.dropzones"),nextColourIndex=0,dropZoneNo=0;dropZoneNo'+dropZone.markertext+"");var markerspan=this.getRoot().find("div.ddarea div.markertexts span.markertext"+dropZoneNo);if(markerspan.length){var handles=shape.getHandlePositions(),positionLeft=handles.moveHandle.x-markerspan.outerWidth()/2-4,positionTop=handles.moveHandle.y-markerspan.outerHeight()/2;markerspan.css("left",positionLeft).css("top",positionTop),markerspan.data("originX",markerspan.position().left/bgRatio).data("originY",markerspan.position().top/bgRatio),this.handleElementScale(markerspan,"center")}}var shapeSVG=shape.makeSvg(svg[0]);shapeSVG.setAttribute("class","dropzone "+colourClass),this.shapes[this.shapes.length]=shape,this.shapeSVGs[this.shapeSVGs.length]=shapeSVG}},DragDropMarkersQuestion.prototype.repositionDrags=function(){var root=this.getRoot(),thisQ=this;root.find("div.draghomes .marker").not(".dragplaceholder").each((function(key,item){$(item).addClass("unneeded")})),root.find("input.choices").each((function(key,input){var choiceNo=thisQ.getChoiceNoFromElement(input),imageCoords=thisQ.getImageCoords(input);if(imageCoords.length){var drag=thisQ.getRoot().find(".draghomes span.marker.choice"+choiceNo).not(".dragplaceholder");drag.remove();for(var i=0;i{result[inputNode.id]=inputNode.value})),result},DragDropMarkersQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropMarkersQuestion.prototype.getImageCoords=function(inputNode){var imageCoords=[],val=$(inputNode).val();if(""!==val)for(var coordsStrings=val.split(";"),i=0;i=bgPosition.left&&point.x=bgPosition.top&&point.y2&&void 0!==arguments[2]&&arguments[2];var dropArea=this.dropArea(),bgRatio=this.bgRatio();drag.removeClass("beingdragged").removeClass("unneeded");var dragXY=this.convertToBgImgXY(new Shapes.Point(drag.data("pagex"),drag.data("pagey")));isScaling?(drag.data("originX",dragXY.x/bgRatio).data("originY",dragXY.y/bgRatio),drag.css("left",dragXY.x).css("top",dragXY.y)):(drag.data("originX",dragXY.x).data("originY",dragXY.y),drag.css("left",dragXY.x*bgRatio).css("top",dragXY.y*bgRatio)),initialLoad||drag.data("scaleRatio",bgRatio),dropArea.append(drag),this.handleElementScale(drag,"left top")},DragDropMarkersQuestion.prototype.cloneDragIfNeeded=function(drag){var inputNode=this.getInput(drag),noOfDrags=Number(this.getClassnameNumericSuffix(inputNode,"noofdrags")),displayedDragsInDropArea=this.getRoot().find("div.droparea .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).length,displayedDragsInDragHomes=this.getRoot().find("div.draghomes .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).not(".dragplaceholder").length;if((this.isInfiniteDrag(drag)||!this.isInfiniteDrag(drag)&&displayedDragsInDropArea1;)dragsInHome.first().remove(),displayedDrags--},DragDropMarkersQuestion.prototype.getInput=function(drag){var choiceNo=this.getChoiceNoFromElement(drag);return this.getRoot().find("input.choices.choice"+choiceNo)},DragDropMarkersQuestion.prototype.bgRatio=function(){var bgImg=this.bgImage(),bgImgNaturalWidth=bgImg.get(0).naturalWidth;return bgImg.width()/bgImgNaturalWidth},DragDropMarkersQuestion.prototype.handleElementScale=function(element,type){var bgRatio=parseFloat(this.bgRatio());this.isPrinting&&(bgRatio=1),$(element).css({"-webkit-transform":"scale("+bgRatio+")","-moz-transform":"scale("+bgRatio+")","-ms-transform":"scale("+bgRatio+")","-o-transform":"scale("+bgRatio+")",transform:"scale("+bgRatio+")","transform-origin":type})},DragDropMarkersQuestion.prototype.isInfiniteDrag=function(drag){return drag.hasClass("infinite")},DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded=function(){this.allImagesLoaded||(null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId),this.getNotYetLoadedImages().length>0?this.imageLoadingTimeoutId=setTimeout((function(){this.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,this.cloneDrags(),this.repositionDrags(),this.drawDropzones()))},DragDropMarkersQuestion.prototype.getNotYetLoadedImages=function(){return this.getRoot().find(".ddmarker img.dropbackground").not((function(i,imgNode){return this.imageIsLoaded(imgNode)}))},DragDropMarkersQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,markerEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,visibleDropZones){if(questionManager.questions[containerId]=new DragDropMarkersQuestion(containerId,readOnly,visibleDropZones),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.markerEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("ddmarker")&&!questionContainer.classList.contains("qtype_ddmarker-readonly")&&(questionManager.addEventHandlersToMarker($(questionContainer).find("div.draghomes .marker")),questionManager.addEventHandlersToMarker($(questionContainer).find("div.droparea .marker")))}},setupEventHandlers:function(){$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToMarker:function(element){element.on("mousedown touchstart",questionManager.handleDragStart).on("keydown keypress",questionManager.handleKeyPress).focusin((function(e){questionManager.handleKeyboardFocus(e,!0)})).focusout((function(e){questionManager.handleKeyboardFocus(e,!1)}))},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},handleKeyboardFocus:function(e,isNavigating){questionManager.isKeyboardNavigation=isNavigating},fixLayoutIfThingsMoved:function(){questionManager.isKeyboardNavigation||this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.ddmarker").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); +define("qtype_ddmarker/question",["jquery","core/dragdrop","qtype_ddmarker/shapes","core/key_codes","core_form/changechecker","core_filters/events"],(function($,dragDrop,Shapes,keys,FormChangeChecker,filterEvent){function DragDropMarkersQuestion(containerId,readOnly,visibleDropZones){var thisQ=this;this.containerId=containerId,this.visibleDropZones=visibleDropZones,this.shapes=[],this.shapeSVGs=[],this.isPrinting=!1,this.questionAnswer={},readOnly&&this.getRoot().addClass("qtype_ddmarker-readonly"),thisQ.allImagesLoaded=!1,thisQ.getNotYetLoadedImages().one("load",(function(){thisQ.waitForAllImagesToBeLoaded()})),thisQ.waitForAllImagesToBeLoaded()}DragDropMarkersQuestion.prototype.drawDropzones=function(){if(this.visibleDropZones.length>0){var bgImage=this.bgImage();this.getRoot().find("div.dropzones").html('');for(var svg=this.getRoot().find("svg.dropzones"),nextColourIndex=0,dropZoneNo=0;dropZoneNo'+dropZone.markertext+"");var markerspan=this.getRoot().find("div.ddarea div.markertexts span.markertext"+dropZoneNo);if(markerspan.length){var handles=shape.getHandlePositions(),positionLeft=handles.moveHandle.x-markerspan.outerWidth()/2-4,positionTop=handles.moveHandle.y-markerspan.outerHeight()/2;markerspan.css("left",positionLeft).css("top",positionTop),markerspan.data("originX",markerspan.position().left/bgRatio).data("originY",markerspan.position().top/bgRatio),this.handleElementScale(markerspan,"center")}}var shapeSVG=shape.makeSvg(svg[0]);shapeSVG.setAttribute("class","dropzone "+colourClass),this.shapes[this.shapes.length]=shape,this.shapeSVGs[this.shapeSVGs.length]=shapeSVG}},DragDropMarkersQuestion.prototype.repositionDrags=function(){var root=this.getRoot(),thisQ=this;root.find("div.draghomes .marker").not(".dragplaceholder").each((function(key,item){$(item).addClass("unneeded")})),root.find("input.choices").each((function(key,input){var choiceNo=thisQ.getChoiceNoFromElement(input),imageCoords=thisQ.getImageCoords(input);if(imageCoords.length){var drag=thisQ.getRoot().find(".draghomes span.marker.choice"+choiceNo).not(".dragplaceholder");drag.remove();for(var i=0;i{result[inputNode.id]=inputNode.value})),result},DragDropMarkersQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropMarkersQuestion.prototype.getImageCoords=function(inputNode){var imageCoords=[],val=$(inputNode).val();if(""!==val)for(var coordsStrings=val.split(";"),i=0;i=bgPosition.left&&point.x=bgPosition.top&&point.y2&&void 0!==arguments[2]&&arguments[2];var dropArea=this.dropArea(),bgRatio=this.bgRatio();drag.removeClass("beingdragged").removeClass("unneeded");var dragXY=this.convertToBgImgXY(new Shapes.Point(drag.data("pagex"),drag.data("pagey")));isScaling?(drag.data("originX",dragXY.x/bgRatio).data("originY",dragXY.y/bgRatio),drag.css("left",dragXY.x).css("top",dragXY.y)):(drag.data("originX",dragXY.x).data("originY",dragXY.y),drag.css("left",dragXY.x*bgRatio).css("top",dragXY.y*bgRatio)),initialLoad||drag.data("scaleRatio",bgRatio),dropArea.append(drag),this.handleElementScale(drag,"left top")},DragDropMarkersQuestion.prototype.cloneDragIfNeeded=function(drag){var inputNode=this.getInput(drag),noOfDrags=Number(this.getClassnameNumericSuffix(inputNode,"noofdrags")),displayedDragsInDropArea=this.getRoot().find("div.droparea .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).length,displayedDragsInDragHomes=this.getRoot().find("div.draghomes .marker.choice"+this.getChoiceNoFromElement(drag)+this.getDragNoClass(drag,!0)).not(".dragplaceholder").length;if((this.isInfiniteDrag(drag)||!this.isInfiniteDrag(drag)&&displayedDragsInDropArea1;)dragsInHome.first().remove(),displayedDrags--},DragDropMarkersQuestion.prototype.getInput=function(drag){var choiceNo=this.getChoiceNoFromElement(drag);return this.getRoot().find("input.choices.choice"+choiceNo)},DragDropMarkersQuestion.prototype.bgRatio=function(){var bgImg=this.bgImage(),bgImgNaturalWidth=bgImg.get(0).naturalWidth;return bgImg.width()/bgImgNaturalWidth},DragDropMarkersQuestion.prototype.handleElementScale=function(element,type){var bgRatio=parseFloat(this.bgRatio());this.isPrinting&&(bgRatio=1),$(element).css({"-webkit-transform":"scale("+bgRatio+")","-moz-transform":"scale("+bgRatio+")","-ms-transform":"scale("+bgRatio+")","-o-transform":"scale("+bgRatio+")",transform:"scale("+bgRatio+")","transform-origin":type})},DragDropMarkersQuestion.prototype.isInfiniteDrag=function(drag){return drag.hasClass("infinite")},DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded=function(){var thisQ=this;this.allImagesLoaded||(null!==this.imageLoadingTimeoutId&&clearTimeout(this.imageLoadingTimeoutId),this.getNotYetLoadedImages().length>0?this.imageLoadingTimeoutId=setTimeout((function(){this.waitForAllImagesToBeLoaded()}),100):(this.allImagesLoaded=!0,this.cloneDrags(),this.repositionDrags(),this.drawDropzones(),document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete,(elements=>{elements.detail.nodes.forEach((element=>{thisQ.changeAllMakerToFilteredContent(element)}))}))))},DragDropMarkersQuestion.prototype.changeAllMakerToFilteredContent=function(filteredElement){let currentFilteredItem=$(filteredElement);const parentIsMarker=currentFilteredItem.parent().closest("span.marker"),isMarker=currentFilteredItem.hasClass("marker"),root=this.getRoot();if(!parentIsMarker&&!isMarker)return;if(parentIsMarker&&(currentFilteredItem=currentFilteredItem.parent().closest("span.marker")),root.find(currentFilteredItem).length<=0)return;const dragNo=this.getDragNo(currentFilteredItem),choiceNo=this.getChoiceNoFromElement(currentFilteredItem),listOfContainerToBeModifed=["div.draghomes .marker:not(.dragplaceholder).dragno"+dragNo+".choice"+choiceNo,"div.droparea .marker:not(.dragplaceholder).dragno"+dragNo+".choice"+choiceNo,"div.draghomes .marker:not(.dragplaceholder).infinite.choice"+choiceNo,"div.droparea .marker:not(.dragplaceholder).infinite.choice"+choiceNo];let listOfModifiedDragDrop=[];const filteredDragDropClone=currentFilteredItem.clone();listOfContainerToBeModifed.forEach((function(selector){root.find(selector).each((function(i,node){const originalClass=$(node).attr("class"),originalStyle=$(node).attr("style");filteredDragDropClone.attr("class",originalClass),filteredDragDropClone.attr("style",originalStyle),questionManager.addEventHandlersToMarker(filteredDragDropClone),$(node).before(filteredDragDropClone),listOfModifiedDragDrop.push(node)}))})),listOfModifiedDragDrop.forEach((function(node){$(node).remove()}))},DragDropMarkersQuestion.prototype.getNotYetLoadedImages=function(){return this.getRoot().find(".ddmarker img.dropbackground").not((function(i,imgNode){return this.imageIsLoaded(imgNode)}))},DragDropMarkersQuestion.prototype.imageIsLoaded=function(imgElement){return imgElement.complete&&0!==imgElement.naturalHeight};var questionManager={eventHandlersInitialised:!1,markerEventHandlersInitialised:{},isPrinting:!1,isKeyboardNavigation:!1,questions:{},init:function(containerId,readOnly,visibleDropZones){if(questionManager.questions[containerId]=new DragDropMarkersQuestion(containerId,readOnly,visibleDropZones),questionManager.eventHandlersInitialised||(questionManager.setupEventHandlers(),questionManager.eventHandlersInitialised=!0),!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)){questionManager.markerEventHandlersInitialised[containerId]=!0;var questionContainer=document.getElementById(containerId);questionContainer.classList.contains("ddmarker")&&!questionContainer.classList.contains("qtype_ddmarker-readonly")&&(questionManager.addEventHandlersToMarker($(questionContainer).find("div.draghomes .marker")),questionManager.addEventHandlersToMarker($(questionContainer).find("div.droparea .marker")))}},setupEventHandlers:function(){$(window).on("resize",(function(){questionManager.handleWindowResize(!1)})),window.addEventListener("beforeprint",(function(){questionManager.isPrinting=!0,questionManager.handleWindowResize(questionManager.isPrinting)})),window.addEventListener("afterprint",(function(){questionManager.isPrinting=!1,questionManager.handleWindowResize(questionManager.isPrinting)})),setTimeout((function(){questionManager.fixLayoutIfThingsMoved()}),100)},addEventHandlersToMarker:function(element){element.on("mousedown touchstart",questionManager.handleDragStart).on("keydown keypress",questionManager.handleKeyPress).focusin((function(e){questionManager.handleKeyboardFocus(e,!0)})).focusout((function(e){questionManager.handleKeyboardFocus(e,!1)}))},handleDragStart:function(e){e.preventDefault();var question=questionManager.getQuestionForEvent(e);question&&question.handleDragStart(e)},handleKeyPress:function(e){var question=questionManager.getQuestionForEvent(e);question&&question.handleKeyPress(e)},handleWindowResize:function(isPrinting){for(var containerId in questionManager.questions)questionManager.questions.hasOwnProperty(containerId)&&(questionManager.questions[containerId].isPrinting=isPrinting,questionManager.questions[containerId].handleResize())},handleKeyboardFocus:function(e,isNavigating){questionManager.isKeyboardNavigation=isNavigating},fixLayoutIfThingsMoved:function(){questionManager.isKeyboardNavigation||this.handleWindowResize(questionManager.isPrinting),setTimeout((function(){questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting)}),100)},getQuestionForEvent:function(e){var containerId=$(e.currentTarget).closest(".que.ddmarker").attr("id");return questionManager.questions[containerId]},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); //# sourceMappingURL=question.min.js.map \ No newline at end of file diff --git a/question/type/ddmarker/amd/build/question.min.js.map b/question/type/ddmarker/amd/build/question.min.js.map index 14a7916e0c614..b7f7ad0be6b1b 100644 --- a/question/type/ddmarker/amd/build/question.min.js.map +++ b/question/type/ddmarker/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 * Question class for drag and drop marker question type, used to support the question and preview pages.\n *\n * @module qtype_ddmarker/question\n * @copyright 2018 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_ddmarker/shapes',\n 'core/key_codes',\n 'core_form/changechecker'\n], function(\n $,\n dragDrop,\n Shapes,\n keys,\n FormChangeChecker\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {Object[]} visibleDropZones the geometry of any drop-zones to show.\n * Objects have fields shape, coords and markertext.\n * @constructor\n */\n function DragDropMarkersQuestion(containerId, readOnly, visibleDropZones) {\n var thisQ = this;\n this.containerId = containerId;\n this.visibleDropZones = visibleDropZones;\n this.shapes = [];\n this.shapeSVGs = [];\n this.isPrinting = false;\n this.questionAnswer = {};\n if (readOnly) {\n this.getRoot().addClass('qtype_ddmarker-readonly');\n }\n thisQ.allImagesLoaded = false;\n thisQ.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n thisQ.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Draws the svg shapes of any drop zones that should be visible for feedback purposes.\n */\n DragDropMarkersQuestion.prototype.drawDropzones = function() {\n if (this.visibleDropZones.length > 0) {\n var bgImage = this.bgImage();\n\n this.getRoot().find('div.dropzones').html('');\n var svg = this.getRoot().find('svg.dropzones');\n\n var nextColourIndex = 0;\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var colourClass = 'color' + nextColourIndex;\n nextColourIndex = (nextColourIndex + 1) % 8;\n this.addDropzone(svg, dropZoneNo, colourClass);\n }\n }\n };\n\n /**\n * Adds a dropzone shape with colour, coords and link provided to the array of shapes.\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 DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {\n var dropZone = this.visibleDropZones[dropZoneNo],\n shape = Shapes.make(dropZone.shape, ''),\n existingmarkertext,\n bgRatio = this.bgRatio();\n if (!shape.parse(dropZone.coords, bgRatio)) {\n return;\n }\n\n existingmarkertext = this.getRoot().find('div.markertexts span.markertext' + dropZoneNo);\n if (existingmarkertext.length) {\n if (dropZone.markertext !== '') {\n existingmarkertext.html(dropZone.markertext);\n } else {\n existingmarkertext.remove();\n }\n } else if (dropZone.markertext !== '') {\n var classnames = 'markertext markertext' + dropZoneNo;\n this.getRoot().find('div.markertexts').append('' +\n dropZone.markertext + '');\n var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n if (markerspan.length) {\n var handles = shape.getHandlePositions();\n var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;\n var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);\n markerspan\n .css('left', positionLeft)\n .css('top', positionTop);\n markerspan\n .data('originX', markerspan.position().left / bgRatio)\n .data('originY', markerspan.position().top / bgRatio);\n this.handleElementScale(markerspan, 'center');\n }\n }\n\n var shapeSVG = shape.makeSvg(svg[0]);\n shapeSVG.setAttribute('class', 'dropzone ' + colourClass);\n\n this.shapes[this.shapes.length] = shape;\n this.shapeSVGs[this.shapeSVGs.length] = shapeSVG;\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 DragDropMarkersQuestion.prototype.repositionDrags = function() {\n var root = this.getRoot(),\n thisQ = this;\n\n root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {\n $(item).addClass('unneeded');\n });\n\n root.find('input.choices').each(function(key, input) {\n var choiceNo = thisQ.getChoiceNoFromElement(input),\n imageCoords = thisQ.getImageCoords(input);\n if (imageCoords.length) {\n var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');\n drag.remove();\n for (var i = 0; i < imageCoords.length; i++) {\n var dragInDrop = drag.clone();\n // Convert image coords to screen coords.\n const screenCoords = thisQ.convertToWindowXY(imageCoords[i]);\n dragInDrop.data('pagex', screenCoords.x).data('pagey', screenCoords.y);\n // Save image coords to the drag item so we can use it later.\n dragInDrop.data('imageCoords', imageCoords[i]);\n // We always save the coordinates in the 1:1 ratio.\n // So we need to set the scale ratio to 1 for the initial load.\n dragInDrop.data('scaleRatio', 1);\n thisQ.sendDragToDrop(dragInDrop, false, true);\n }\n thisQ.getDragClone(drag).addClass('active');\n thisQ.cloneDragIfNeeded(drag);\n }\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropMarkersQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.choices').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropMarkersQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Determine what drag items need to be shown and\n * return coords of all drag items except any that are currently being dragged\n * based on contents of hidden inputs and whether drags are 'infinite' or how many\n * drags should be shown.\n *\n * @param {jQuery} inputNode\n * @returns {Point[]} image coordinates of however many copies of the drag item should be shown.\n */\n DragDropMarkersQuestion.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] = Shapes.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 DragDropMarkersQuestion.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 DragDropMarkersQuestion.prototype.convertToBgImgXY = function(point) {\n var bgImage = this.bgImage();\n return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);\n };\n\n /**\n * Is the point within the background image?\n *\n * @param {Point} point relative to the BG image.\n * @return {boolean} true it they are.\n */\n DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {\n var bgImage = this.bgImage();\n var bgPosition = bgImage.offset();\n\n return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()\n && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();\n };\n\n /**\n * Get the outer div for this question.\n * @returns {jQuery} containing that div.\n */\n DragDropMarkersQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DragDropMarkersQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n DragDropMarkersQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n dragged = $(e.target).closest('.marker');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragged.addClass('beingdragged').css('transform', '');\n\n var placed = !dragged.hasClass('unneeded');\n if (!placed) {\n var hiddenDrag = thisQ.getDragClone(dragged);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n dragged.offset(hiddenDrag.offset());\n }\n }\n\n dragDrop.start(e, dragged, function() {\n void (1);\n }, function(x, y, dragged) {\n thisQ.dragEnd(dragged);\n });\n };\n\n /**\n * Functionality at the end of a drag drop.\n * @param {jQuery} dragged the marker that was dragged.\n */\n DragDropMarkersQuestion.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 Shapes.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 Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);\n }\n\n if (!placed) {\n this.sendDragHome(dragged);\n this.removeDragIfNeeded(dragged);\n } else {\n this.cloneDragIfNeeded(dragged);\n }\n\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Save the coordinates for a dropped item in the form field.\n * @param {Number} choiceNo which copy of the choice this was.\n */\n DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) {\n let imageCoords = [];\n var items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),\n thiQ = this,\n bgRatio = this.bgRatio();\n\n if (items.length) {\n items.each(function() {\n var drag = $(this);\n if (!drag.hasClass('beingdragged') && !drag.data('imageCoords')) {\n if (drag.data('scaleRatio') !== bgRatio) {\n // The scale ratio for the draggable item was changed. We need to update that.\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n }\n var dragXY = new Shapes.Point(drag.data('pagex'), drag.data('pagey'));\n if (thiQ.coordsInBgImg(dragXY)) {\n var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n imageCoords[imageCoords.length] = bgImgXY;\n }\n } else if (drag.data('imageCoords')) {\n imageCoords[imageCoords.length] = drag.data('imageCoords');\n }\n });\n }\n\n this.getRoot().find('input.choice' + choiceNo).val(imageCoords.join(';'));\n if (this.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n this.questionAnswer = this.getQuestionAnsweredValues();\n }\n };\n\n /**\n * Handle key down / press events on markers.\n * @param {KeyboardEvent} e\n */\n DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {\n var drag = $(e.target).closest('.marker'),\n point = new Shapes.Point(drag.offset().left, drag.offset().top),\n choiceNo = this.getChoiceNoFromElement(drag);\n\n switch (e.keyCode) {\n case keys.arrowLeft:\n case 65: // A.\n point.x -= 1;\n break;\n case keys.arrowRight:\n case 68: // D.\n point.x += 1;\n break;\n case keys.arrowDown:\n case 83: // S.\n point.y += 1;\n break;\n case keys.arrowUp:\n case 87: // W.\n point.y -= 1;\n break;\n case keys.space:\n case keys.escape:\n point = null;\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n if (point !== null) {\n point = this.constrainToBgImg(point);\n drag.offset({'left': point.x, 'top': point.y});\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());\n if (this.coordsInBgImg(new Shapes.Point(drag.offset().left, drag.offset().top))) {\n if (drag.hasClass('unneeded')) {\n this.sendDragToDrop(drag, true);\n var hiddenDrag = this.getDragClone(drag);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n }\n this.cloneDragIfNeeded(drag);\n }\n }\n } else {\n drag.css('left', '').css('top', '');\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n this.sendDragHome(drag);\n this.removeDragIfNeeded(drag);\n }\n drag.focus();\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Makes sure the dragged item always exists within the background image area.\n *\n * @param {Point} windowxy\n * @returns {Point} coordinates\n */\n DragDropMarkersQuestion.prototype.constrainToBgImg = function(windowxy) {\n var bgImg = this.bgImage(),\n bgImgXY = this.convertToBgImgXY(windowxy);\n bgImgXY.x = Math.max(0, bgImgXY.x);\n bgImgXY.y = Math.max(0, bgImgXY.y);\n bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);\n bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);\n return this.convertToWindowXY(bgImgXY);\n };\n\n /**\n * Returns the choice number for a node.\n *\n * @param {Element|jQuery} node\n * @returns {Number}\n */\n DragDropMarkersQuestion.prototype.getChoiceNoFromElement = function(node) {\n return Number(this.getClassnameNumericSuffix(node, 'choice'));\n };\n\n /**\n * Returns the numeric part of a class with the given prefix.\n *\n * @param {Element|jQuery} node\n * @param {String} prefix\n * @returns {Number|null}\n */\n DragDropMarkersQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = $(node).attr('class');\n if (classes !== undefined && classes !== '') {\n var classesarr = classes.split(' ');\n for (var index = 0; index < classesarr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesarr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesarr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Handle when the window is resized.\n */\n DragDropMarkersQuestion.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 shape = thisQ.shapes[dropZoneNo];\n var shapeSVG = thisQ.shapeSVGs[dropZoneNo];\n shape.parse(originCoords, bgRatio);\n shape.updateSvg(shapeSVG);\n\n var handles = shape.getHandlePositions();\n var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n markerSpan\n .css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)\n .css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));\n thisQ.handleElementScale(markerSpan, 'center');\n }\n };\n\n /**\n * Clone the drag.\n */\n DragDropMarkersQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('marker');\n placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));\n placeHolder.addClass(thisQ.getDragNoClass(drag, false));\n placeHolder.addClass('dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Get the drag number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the drag number.\n */\n DragDropMarkersQuestion.prototype.getDragNo = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'dragno');\n };\n\n /**\n * Get the drag number prefix of a drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} includeSelector include the CSS selector prefix or not.\n * @return {String} Class name\n */\n DragDropMarkersQuestion.prototype.getDragNoClass = function(drag, includeSelector) {\n var className = 'dragno' + this.getDragNo(drag);\n if (this.isInfiniteDrag(drag)) {\n className = 'infinite';\n }\n\n if (includeSelector) {\n return '.' + className;\n }\n\n return className;\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropMarkersQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draghomes' + ' span.marker' +\n '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');\n };\n\n /**\n * Get the drop area element.\n * @returns {jQuery} droparea element.\n */\n DragDropMarkersQuestion.prototype.dropArea = function() {\n return this.getRoot().find('div.droparea');\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropMarkersQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('beingdragged')\n .addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n var placeHolder = this.getDragClone(drag);\n placeHolder.after(drag);\n placeHolder.removeClass('active');\n };\n\n /**\n * Animate a drag item into a given place.\n *\n * @param {jQuery} drag the item to place.\n * @param {boolean} isScaling Scaling or not.\n * @param {boolean} initialLoad Whether it is the initial load or not.\n */\n DragDropMarkersQuestion.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 Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n if (isScaling) {\n drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);\n drag.css('left', dragXY.x).css('top', dragXY.y);\n } else {\n drag.data('originX', dragXY.x).data('originY', dragXY.y);\n drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);\n }\n // We need to save the original scale ratio for each draggable item.\n if (!initialLoad) {\n // Only set the scale ratio for a current being-dragged item, not for the initial loading.\n drag.data('scaleRatio', bgRatio);\n }\n dropArea.append(drag);\n this.handleElementScale(drag, 'left top');\n };\n\n /**\n * Clone the drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.cloneDragIfNeeded = function(drag) {\n var inputNode = this.getInput(drag),\n noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),\n displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,\n displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;\n\n if ((this.isInfiniteDrag(drag) ||\n !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {\n var dragClone = drag.clone();\n dragClone.addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n this.getDragClone(drag)\n .removeClass('active')\n .after(dragClone);\n questionManager.addEventHandlersToMarker(dragClone);\n }\n };\n\n /**\n * Remove the clone drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {\n var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');\n var displayedDrags = dragsInHome.length;\n while (displayedDrags > 1) {\n dragsInHome.first().remove();\n displayedDrags--;\n }\n };\n\n /**\n * Get the input belong to drag.\n *\n * @param {jQuery} drag the item to place.\n * @returns {jQuery} input element.\n */\n DragDropMarkersQuestion.prototype.getInput = function(drag) {\n var choiceNo = this.getChoiceNoFromElement(drag);\n return this.getRoot().find('input.choices.choice' + choiceNo);\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropMarkersQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DragDropMarkersQuestion.prototype.handleElementScale = function(element, type) {\n var bgRatio = parseFloat(this.bgRatio());\n if (this.isPrinting) {\n bgRatio = 1;\n }\n $(element).css({\n '-webkit-transform': 'scale(' + bgRatio + ')',\n '-moz-transform': 'scale(' + bgRatio + ')',\n '-ms-transform': 'scale(' + bgRatio + ')',\n '-o-transform': 'scale(' + bgRatio + ')',\n 'transform': 'scale(' + bgRatio + ')',\n 'transform-origin': type\n });\n };\n\n /**\n * Check if the given drag is in infinite mode or not.\n *\n * @param {jQuery} drag The drag item need to check.\n */\n DragDropMarkersQuestion.prototype.isInfiniteDrag = function(drag) {\n return drag.hasClass('infinite');\n };\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n this.waitForAllImagesToBeLoaded();\n }, 100);\n return;\n }\n\n // We now have all images. Carry on, but only after giving the layout a chance to settle down.\n this.allImagesLoaded = true;\n this.cloneDrags();\n this.repositionDrags();\n this.drawDropzones();\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DragDropMarkersQuestion.prototype.getNotYetLoadedImages = function() {\n return this.getRoot().find('.ddmarker img.dropbackground').not(function(i, imgNode) {\n return this.imageIsLoaded(imgNode);\n });\n };\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n DragDropMarkersQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\n * @type {Object}\n */\n var questionManager = {\n\n /**\n * {boolean} ensures that the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} ensures that the marker event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n markerEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @param {String} containerId the id of the div.que that contains this question.\n * @param {boolean} readOnly whether the question is read-only.\n * @param {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.\n */\n init: function(containerId, readOnly, visibleDropZones) {\n questionManager.questions[containerId] =\n new DragDropMarkersQuestion(containerId, readOnly, visibleDropZones);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.markerEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('ddmarker') &&\n !questionContainer.classList.contains('qtype_ddmarker-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker'));\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $(window).on('resize', function() {\n questionManager.handleWindowResize(false);\n });\n window.addEventListener('beforeprint', function() {\n questionManager.isPrinting = true;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n window.addEventListener('afterprint', function() {\n questionManager.isPrinting = false;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved();\n }, 100);\n },\n\n /**\n * Binding the event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToMarker: function(element) {\n element\n .on('mousedown touchstart', questionManager.handleDragStart)\n .on('keydown keypress', questionManager.handleKeyPress)\n .focusin(function(e) {\n questionManager.handleKeyboardFocus(e, true);\n })\n .focusout(function(e) {\n questionManager.handleKeyboardFocus(e, false);\n });\n },\n\n /**\n * Handle mouse down / touch start events on markers.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on markers.\n * @param {Event} e\n */\n handleKeyPress: function(e) {\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * Handle focus lost events on markers.\n * @param {Event} e\n * @param {boolean} isNavigating\n */\n handleKeyboardFocus: function(e, isNavigating) {\n questionManager.isKeyboardNavigation = isNavigating;\n },\n\n /**\n * Sometimes, despite our best efforts, things change in a way that cannot\n * be specifically caught (e.g. dock expanding or collapsing in Boost).\n * Therefore, we need to periodically check everything is in the right position.\n */\n fixLayoutIfThingsMoved: function() {\n if (!questionManager.isKeyboardNavigation) {\n this.handleWindowResize(questionManager.isPrinting);\n }\n // We use setTimeout after finishing work, rather than setInterval,\n // in case positioning things is slow. We want 100 ms gap\n // between executions, not what setInterval does.\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);\n }, 100);\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropMarkersQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddmarker').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddmarker/question\n */\n return {\n /**\n * Initialise one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {String} bgImgUrl the URL of the background image.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {String[]} visibleDropZones the geometry of any drop-zones to show.\n */\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","Shapes","keys","FormChangeChecker","DragDropMarkersQuestion","containerId","readOnly","visibleDropZones","thisQ","this","shapes","shapeSVGs","isPrinting","questionAnswer","getRoot","addClass","allImagesLoaded","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","drawDropzones","length","bgImage","find","html","outerWidth","outerHeight","svg","nextColourIndex","dropZoneNo","colourClass","addDropzone","existingmarkertext","dropZone","shape","make","bgRatio","parse","coords","markertext","remove","classnames","append","markerspan","handles","getHandlePositions","positionLeft","moveHandle","x","positionTop","y","css","data","position","left","top","handleElementScale","shapeSVG","makeSvg","setAttribute","repositionDrags","root","not","each","key","item","input","choiceNo","getChoiceNoFromElement","imageCoords","getImageCoords","drag","i","dragInDrop","clone","screenCoords","convertToWindowXY","sendDragToDrop","getDragClone","cloneDragIfNeeded","getQuestionAnsweredValues","result","inputNode","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","val","coordsStrings","split","Point","point","offset","convertToBgImgXY","coordsInBgImg","bgPosition","width","height","document","getElementById","handleDragStart","e","dragged","target","closest","prepare","start","hasClass","hiddenDrag","dragEnd","dragXY","placed","bgImgXY","sendDragHome","removeDragIfNeeded","saveCoordsForChoice","items","thiQ","join","questionManager","handleFormDirty","handleKeyPress","keyCode","arrowLeft","arrowRight","arrowDown","arrowUp","space","escape","preventDefault","constrainToBgImg","focus","windowxy","bgImg","Math","max","min","node","Number","getClassnameNumericSuffix","prefix","classes","attr","undefined","classesarr","index","RegExp","test","match","exec","handleResize","parseFloat","originCoords","updateSvg","markerSpan","cloneDrags","draghome","placeHolder","removeClass","getDragNoClass","before","getDragNo","includeSelector","className","isInfiniteDrag","dropArea","after","isScaling","initialLoad","getInput","noOfDrags","displayedDragsInDropArea","displayedDragsInDragHomes","dragClone","addEventHandlersToMarker","dragsInHome","displayedDrags","first","bgImgNaturalWidth","get","naturalWidth","element","type","imageLoadingTimeoutId","clearTimeout","setTimeout","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","eventHandlersInitialised","markerEventHandlersInitialised","isKeyboardNavigation","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","classList","contains","window","on","handleWindowResize","addEventListener","fixLayoutIfThingsMoved","focusin","handleKeyboardFocus","focusout","question","getQuestionForEvent","isNavigating","currentTarget","responseForm","markFormAsDirty"],"mappings":";;;;;;;AAuBAA,iCAAO,CACH,SACA,gBACA,wBACA,iBACA,4BACD,SACCC,EACAC,SACAC,OACAC,KACAC,4BAcSC,wBAAwBC,YAAaC,SAAUC,sBAChDC,MAAQC,UACPJ,YAAcA,iBACdE,iBAAmBA,sBACnBG,OAAS,QACTC,UAAY,QACZC,YAAa,OACbC,eAAiB,GAClBP,eACKQ,UAAUC,SAAS,2BAE5BP,MAAMQ,iBAAkB,EACxBR,MAAMS,wBAAwBC,IAAI,QAAQ,WACtCV,MAAMW,gCAEVX,MAAMW,6BAMVf,wBAAwBgB,UAAUC,cAAgB,cAC1CZ,KAAKF,iBAAiBe,OAAS,EAAG,KAC9BC,QAAUd,KAAKc,eAEdT,UAAUU,KAAK,iBAAiBC,KAAK,oEAC1BF,QAAQG,aADkB,aAEzBH,QAAQI,cAAgB,oBACrCC,IAAMnB,KAAKK,UAAUU,KAAK,iBAE1BK,gBAAkB,EACbC,WAAa,EAAGA,WAAarB,KAAKF,iBAAiBe,OAAQQ,aAAc,KAC1EC,YAAc,QAAUF,gBAC5BA,iBAAmBA,gBAAkB,GAAK,OACrCG,YAAYJ,IAAKE,WAAYC,gBAY9C3B,wBAAwBgB,UAAUY,YAAc,SAASJ,IAAKE,WAAYC,iBAGlEE,mBAFAC,SAAWzB,KAAKF,iBAAiBuB,YACjCK,MAAQlC,OAAOmC,KAAKF,SAASC,MAAO,IAEpCE,QAAU5B,KAAK4B,aACdF,MAAMG,MAAMJ,SAASK,OAAQF,cAIlCJ,mBAAqBxB,KAAKK,UAAUU,KAAK,kCAAoCM,aACtDR,OACS,KAAxBY,SAASM,WACTP,mBAAmBR,KAAKS,SAASM,YAEjCP,mBAAmBQ,cAEpB,GAA4B,KAAxBP,SAASM,WAAmB,KAC/BE,WAAa,wBAA0BZ,gBACtChB,UAAUU,KAAK,mBAAmBmB,OAAO,gBAAkBD,WAAa,KACzER,SAASM,WAAa,eACtBI,WAAanC,KAAKK,UAAUU,KAAK,6CAA+CM,eAChFc,WAAWtB,OAAQ,KACfuB,QAAUV,MAAMW,qBAChBC,aAAeF,QAAQG,WAAWC,EAAKL,WAAWlB,aAAe,EAAK,EACtEwB,YAAcL,QAAQG,WAAWG,EAAKP,WAAWjB,cAAgB,EACrEiB,WACKQ,IAAI,OAAQL,cACZK,IAAI,MAAOF,aAChBN,WACKS,KAAK,UAAWT,WAAWU,WAAWC,KAAOlB,SAC7CgB,KAAK,UAAWT,WAAWU,WAAWE,IAAMnB,cAC5CoB,mBAAmBb,WAAY,eAIxCc,SAAWvB,MAAMwB,QAAQ/B,IAAI,IACjC8B,SAASE,aAAa,QAAS,YAAc7B,kBAExCrB,OAAOD,KAAKC,OAAOY,QAAUa,WAC7BxB,UAAUF,KAAKE,UAAUW,QAAUoC,WAQ5CtD,wBAAwBgB,UAAUyC,gBAAkB,eAC5CC,KAAOrD,KAAKK,UACZN,MAAQC,KAEZqD,KAAKtC,KAAK,yBAAyBuC,IAAI,oBAAoBC,MAAK,SAASC,IAAKC,MAC1EnE,EAAEmE,MAAMnD,SAAS,eAGrB+C,KAAKtC,KAAK,iBAAiBwC,MAAK,SAASC,IAAKE,WACtCC,SAAW5D,MAAM6D,uBAAuBF,OACxCG,YAAc9D,MAAM+D,eAAeJ,UACnCG,YAAYhD,OAAQ,KAChBkD,KAAOhE,MAAMM,UAAUU,KAAK,gCAA4C4C,UAAUL,IAAI,oBAC1FS,KAAK/B,aACA,IAAIgC,EAAI,EAAGA,EAAIH,YAAYhD,OAAQmD,IAAK,KACrCC,WAAaF,KAAKG,cAEhBC,aAAepE,MAAMqE,kBAAkBP,YAAYG,IACzDC,WAAWrB,KAAK,QAASuB,aAAa3B,GAAGI,KAAK,QAASuB,aAAazB,GAEpEuB,WAAWrB,KAAK,cAAeiB,YAAYG,IAG3CC,WAAWrB,KAAK,aAAc,GAC9B7C,MAAMsE,eAAeJ,YAAY,GAAO,GAE5ClE,MAAMuE,aAAaP,MAAMzD,SAAS,UAClCP,MAAMwE,kBAAkBR,UAKhChE,MAAMK,eAAiBL,MAAMyE,6BAQjC7E,wBAAwBgB,UAAU6D,0BAA4B,eACtDC,OAAS,eACRpE,UAAUU,KAAK,iBAAiBwC,MAAK,CAACS,EAAGU,aAC1CD,OAAOC,UAAUC,IAAMD,UAAUE,SAG9BH,QAQX9E,wBAAwBgB,UAAUkE,qBAAuB,iBAC/CC,UAAY9E,KAAKI,eACjB2E,UAAY/E,KAAKwE,gCACnBQ,cAAe,SAGfC,KAAKC,UAAUH,aAAeE,KAAKC,UAAUJ,YAC7CE,cAAe,EACRA,eAGXG,OAAO1F,KAAKsF,WAAWK,SAAQ5B,MACvBuB,UAAUvB,OAASsB,UAAUtB,OAC7BwB,cAAe,MAIhBA,eAYXrF,wBAAwBgB,UAAUmD,eAAiB,SAASY,eACpDb,YAAc,GACdwB,IAAM/F,EAAEoF,WAAWW,SACX,KAARA,YACIC,cAAgBD,IAAIE,MAAM,KACrBvB,EAAI,EAAGA,EAAIsB,cAAczE,OAAQmD,IACtCH,YAAYG,GAAKxE,OAAOgG,MAAM3D,MAAMyD,cAActB,WAGnDH,aAUXlE,wBAAwBgB,UAAUyD,kBAAoB,SAASqB,WACvD3E,QAAUd,KAAKc,iBAKZ2E,MAAMC,OAAO5E,QAAQ4E,SAAS5C,KAAO,EAAGhC,QAAQ4E,SAAS3C,IAAM,IAU1EpD,wBAAwBgB,UAAUgF,iBAAmB,SAASF,WACtD3E,QAAUd,KAAKc,iBACZ2E,MAAMC,QAAQ5E,QAAQ4E,SAAS5C,KAAO,GAAIhC,QAAQ4E,SAAS3C,IAAM,IAS5EpD,wBAAwBgB,UAAUiF,cAAgB,SAASH,WACnD3E,QAAUd,KAAKc,UACf+E,WAAa/E,QAAQ4E,gBAElBD,MAAMjD,GAAKqD,WAAW/C,MAAQ2C,MAAMjD,EAAIqD,WAAW/C,KAAOhC,QAAQgF,SAClEL,MAAM/C,GAAKmD,WAAW9C,KAAO0C,MAAM/C,EAAImD,WAAW9C,IAAMjC,QAAQiF,UAO3EpG,wBAAwBgB,UAAUN,QAAU,kBACjCf,EAAE0G,SAASC,eAAejG,KAAKJ,eAO1CD,wBAAwBgB,UAAUG,QAAU,kBACjCd,KAAKK,UAAUU,KAAK,uBAG/BpB,wBAAwBgB,UAAUuF,gBAAkB,SAASC,OACrDpG,MAAQC,KACRoG,QAAU9G,EAAE6G,EAAEE,QAAQC,QAAQ,cAEvB/G,SAASgH,QAAQJ,GAClBK,UAIVJ,QAAQ9F,SAAS,gBAAgBqC,IAAI,YAAa,MAEpCyD,QAAQK,SAAS,YAClB,KACLC,WAAa3G,MAAMuE,aAAa8B,SAChCM,WAAW7F,SACX6F,WAAWpG,SAAS,UACpB8F,QAAQV,OAAOgB,WAAWhB,WAIlCnG,SAASiH,MAAML,EAAGC,SAAS,eAExB,SAAS5D,EAAGE,EAAG0D,SACdrG,MAAM4G,QAAQP,cAQtBzG,wBAAwBgB,UAAUgG,QAAU,SAASP,aAI7CQ,OAHAC,QAAS,EACTlD,SAAW3D,KAAK4D,uBAAuBwC,SACvCxE,QAAU5B,KAAK4B,aAGnBwE,QAAQxD,KAAK,QAASwD,QAAQV,SAAS5C,MAAMF,KAAK,QAASwD,QAAQV,SAAS3C,KAC5E6D,OAAS,IAAIpH,OAAOgG,MAAMY,QAAQxD,KAAK,SAAUwD,QAAQxD,KAAK,UAC1D5C,KAAK4F,cAAcgB,QAAS,MACvBvC,eAAe+B,SAAS,GAC7BS,QAAS,EAILT,QAAQxD,KAAK,gBACbwD,QAAQxD,KAAK,cAAe,UAI5BkE,QAAU9G,KAAK2F,iBAAiBiB,QACpCE,QAAU,IAAItH,OAAOgG,MAAMsB,QAAQtE,EAAIZ,QAASkF,QAAQpE,EAAId,SAC5DwE,QAAQxD,KAAK,UAAWkE,QAAQtE,GAAGI,KAAK,UAAWkE,QAAQpE,GAG1DmE,YAIItC,kBAAkB6B,eAHlBW,aAAaX,cACbY,mBAAmBZ,eAKvBa,oBAAoBtD,WAO7BhE,wBAAwBgB,UAAUsG,oBAAsB,SAAStD,cACzDE,YAAc,OACdqD,MAAQlH,KAAKK,UAAUU,KAAK,kCAAoC4C,UAChEwD,KAAOnH,KACP4B,QAAU5B,KAAK4B,UAEfsF,MAAMrG,QACNqG,MAAM3D,MAAK,eACHQ,KAAOzE,EAAEU,SACR+D,KAAK0C,SAAS,iBAAoB1C,KAAKnB,KAAK,eAWtCmB,KAAKnB,KAAK,iBACjBiB,YAAYA,YAAYhD,QAAUkD,KAAKnB,KAAK,oBAZiB,CACzDmB,KAAKnB,KAAK,gBAAkBhB,SAE5BmC,KAAKnB,KAAK,QAASmB,KAAK2B,SAAS5C,MAAMF,KAAK,QAASmB,KAAK2B,SAAS3C,SAEnE6D,OAAS,IAAIpH,OAAOgG,MAAMzB,KAAKnB,KAAK,SAAUmB,KAAKnB,KAAK,aACxDuE,KAAKvB,cAAcgB,QAAS,KACxBE,QAAUK,KAAKxB,iBAAiBiB,QACpCE,QAAU,IAAItH,OAAOgG,MAAMsB,QAAQtE,EAAIZ,QAASkF,QAAQpE,EAAId,SAC5DiC,YAAYA,YAAYhD,QAAUiG,kBAQ7CzG,UAAUU,KAAK,eAAiB4C,UAAU0B,IAAIxB,YAAYuD,KAAK,MAChEpH,KAAK6E,yBAELwC,gBAAgBC,uBAEXlH,eAAiBJ,KAAKwE,8BAQnC7E,wBAAwBgB,UAAU4G,eAAiB,SAASpB,OACpDpC,KAAOzE,EAAE6G,EAAEE,QAAQC,QAAQ,WAC3Bb,MAAQ,IAAIjG,OAAOgG,MAAMzB,KAAK2B,SAAS5C,KAAMiB,KAAK2B,SAAS3C,KAC3DY,SAAW3D,KAAK4D,uBAAuBG,aAEnCoC,EAAEqB,cACD/H,KAAKgI,eACL,GACDhC,MAAMjD,GAAK,aAEV/C,KAAKiI,gBACL,GACDjC,MAAMjD,GAAK,aAEV/C,KAAKkI,eACL,GACDlC,MAAM/C,GAAK,aAEVjD,KAAKmI,aACL,GACDnC,MAAM/C,GAAK,aAEVjD,KAAKoI,WACLpI,KAAKqI,OACNrC,MAAQ,6BAKhBU,EAAE4B,iBAEY,OAAVtC,MAAgB,CAChBA,MAAQzF,KAAKgI,iBAAiBvC,OAC9B1B,KAAK2B,OAAO,MAASD,MAAMjD,MAAUiD,MAAM/C,IAC3CqB,KAAKnB,KAAK,QAASmB,KAAK2B,SAAS5C,MAAMF,KAAK,QAASmB,KAAK2B,SAAS3C,SAC/D6D,OAAS5G,KAAK2F,iBAAiB,IAAInG,OAAOgG,MAAMzB,KAAKnB,KAAK,SAAUmB,KAAKnB,KAAK,cAClFmB,KAAKnB,KAAK,UAAWgE,OAAOpE,EAAIxC,KAAK4B,WAAWgB,KAAK,UAAWgE,OAAOlE,EAAI1C,KAAK4B,WAC5E5B,KAAK4F,cAAc,IAAIpG,OAAOgG,MAAMzB,KAAK2B,SAAS5C,KAAMiB,KAAK2B,SAAS3C,OAClEgB,KAAK0C,SAAS,YAAa,MACtBpC,eAAeN,MAAM,OACtB2C,WAAa1G,KAAKsE,aAAaP,MAC/B2C,WAAW7F,QACX6F,WAAWpG,SAAS,eAEnBiE,kBAAkBR,YAI/BA,KAAKpB,IAAI,OAAQ,IAAIA,IAAI,MAAO,IAChCoB,KAAKnB,KAAK,QAASmB,KAAK2B,SAAS5C,MAAMF,KAAK,QAASmB,KAAK2B,SAAS3C,UAC9DgE,aAAahD,WACbiD,mBAAmBjD,MAE5BA,KAAKkE,aACAhB,oBAAoBtD,WAS7BhE,wBAAwBgB,UAAUqH,iBAAmB,SAASE,cACtDC,MAAQnI,KAAKc,UACbgG,QAAU9G,KAAK2F,iBAAiBuC,iBACpCpB,QAAQtE,EAAI4F,KAAKC,IAAI,EAAGvB,QAAQtE,GAChCsE,QAAQpE,EAAI0F,KAAKC,IAAI,EAAGvB,QAAQpE,GAChCoE,QAAQtE,EAAI4F,KAAKE,IAAIH,MAAMrC,QAASgB,QAAQtE,GAC5CsE,QAAQpE,EAAI0F,KAAKE,IAAIH,MAAMpC,SAAUe,QAAQpE,GACtC1C,KAAKoE,kBAAkB0C,UASlCnH,wBAAwBgB,UAAUiD,uBAAyB,SAAS2E,aACzDC,OAAOxI,KAAKyI,0BAA0BF,KAAM,YAUvD5I,wBAAwBgB,UAAU8H,0BAA4B,SAASF,KAAMG,YACrEC,QAAUrJ,EAAEiJ,MAAMK,KAAK,iBACXC,IAAZF,SAAqC,KAAZA,gBACrBG,WAAaH,QAAQpD,MAAM,KACtBwD,MAAQ,EAAGA,MAAQD,WAAWjI,OAAQkI,QAAS,IACxC,IAAIC,OAAO,IAAMN,OAAS,aAC5BO,KAAKH,WAAWC,QAAS,KAE3BG,MADQ,IAAIF,OAAO,aACLG,KAAKL,WAAWC,eAC3BP,OAAOU,MAAM,YAIzB,MAMXvJ,wBAAwBgB,UAAUyI,aAAe,eACzCrJ,MAAQC,KACR4B,QAAU5B,KAAK4B,UACf5B,KAAKG,aACLyB,QAAU,QAGTvB,UAAUU,KAAK,wBAAwBuC,IAAI,iBAAiBC,MAAK,SAASC,IAAKO,MAChFzE,EAAEyE,MACGpB,IAAI,OAAQ0G,WAAW/J,EAAEyE,MAAMnB,KAAK,YAAcyG,WAAWzH,UAC7De,IAAI,MAAO0G,WAAW/J,EAAEyE,MAAMnB,KAAK,YAAcyG,WAAWzH,UACjE7B,MAAMiD,mBAAmBe,KAAM,oBAG9B1D,UAAUU,KAAK,8BACf+E,MAAM9F,KAAKc,UAAUgF,SACrBC,OAAO/F,KAAKc,UAAUiF,cAEtB,IAAI1E,WAAa,EAAGA,WAAarB,KAAKF,iBAAiBe,OAAQQ,aAAc,KAE1EiI,aADWvJ,MAAMD,iBAAiBuB,YACVS,OACxBJ,MAAQ3B,MAAME,OAAOoB,YACrB4B,SAAWlD,MAAMG,UAAUmB,YAC/BK,MAAMG,MAAMyH,aAAc1H,SAC1BF,MAAM6H,UAAUtG,cAEZb,QAAUV,MAAMW,qBAChBmH,WAAaxJ,KAAKK,UAAUU,KAAK,6CAA+CM,YACpFmI,WACK7G,IAAI,OAAQP,QAAQG,WAAWC,EAAKgH,WAAWvI,aAAe,EAAK,GACnE0B,IAAI,MAAOP,QAAQG,WAAWG,EAAK8G,WAAWtI,cAAgB,GACnEnB,MAAMiD,mBAAmBwG,WAAY,YAO7C7J,wBAAwBgB,UAAU8I,WAAa,eACvC1J,MAAQC,UACPK,UAAUU,KAAK,6BAA6BwC,MAAK,SAASwF,MAAOW,cAC9D3F,KAAOzE,EAAEoK,UACTC,YAAc5F,KAAKG,QACvByF,YAAYC,cACZD,YAAYrJ,SAAS,UACrBqJ,YAAYrJ,SAAS,SAAWP,MAAM6D,uBAAuBG,OAC7D4F,YAAYrJ,SAASP,MAAM8J,eAAe9F,MAAM,IAChD4F,YAAYrJ,SAAS,mBACrByD,KAAK+F,OAAOH,iBAUpBhK,wBAAwBgB,UAAUoJ,UAAY,SAAShG,aAC5C/D,KAAKyI,0BAA0B1E,KAAM,WAUhDpE,wBAAwBgB,UAAUkJ,eAAiB,SAAS9F,KAAMiG,qBAC1DC,UAAY,SAAWjK,KAAK+J,UAAUhG,aACtC/D,KAAKkK,eAAenG,QACpBkG,UAAY,YAGZD,gBACO,IAAMC,UAGVA,WASXtK,wBAAwBgB,UAAU2D,aAAe,SAASP,aAC/C/D,KAAKK,UAAUU,KAAK,gCACXf,KAAK4D,uBAAuBG,MAAQ/D,KAAK6J,eAAe9F,MAAM,GAAQ,qBAO1FpE,wBAAwBgB,UAAUwJ,SAAW,kBAClCnK,KAAKK,UAAUU,KAAK,iBAQ/BpB,wBAAwBgB,UAAUoG,aAAe,SAAShD,MACtDA,KAAK6F,YAAY,gBACZtJ,SAAS,YACTqC,IAAI,MAAO,IACXA,IAAI,OAAQ,IACZA,IAAI,YAAa,QAClBgH,YAAc3J,KAAKsE,aAAaP,MACpC4F,YAAYS,MAAMrG,MAClB4F,YAAYC,YAAY,WAU5BjK,wBAAwBgB,UAAU0D,eAAiB,SAASN,KAAMsG,eAAWC,wEACrEH,SAAWnK,KAAKmK,WAChBvI,QAAU5B,KAAK4B,UACnBmC,KAAK6F,YAAY,gBAAgBA,YAAY,gBACzChD,OAAS5G,KAAK2F,iBAAiB,IAAInG,OAAOgG,MAAMzB,KAAKnB,KAAK,SAAUmB,KAAKnB,KAAK,WAC9EyH,WACAtG,KAAKnB,KAAK,UAAWgE,OAAOpE,EAAIZ,SAASgB,KAAK,UAAWgE,OAAOlE,EAAId,SACpEmC,KAAKpB,IAAI,OAAQiE,OAAOpE,GAAGG,IAAI,MAAOiE,OAAOlE,KAE7CqB,KAAKnB,KAAK,UAAWgE,OAAOpE,GAAGI,KAAK,UAAWgE,OAAOlE,GACtDqB,KAAKpB,IAAI,OAAQiE,OAAOpE,EAAIZ,SAASe,IAAI,MAAOiE,OAAOlE,EAAId,UAG1D0I,aAEDvG,KAAKnB,KAAK,aAAchB,SAE5BuI,SAASjI,OAAO6B,WACXf,mBAAmBe,KAAM,aAQlCpE,wBAAwBgB,UAAU4D,kBAAoB,SAASR,UACvDW,UAAY1E,KAAKuK,SAASxG,MAC1ByG,UAAYhC,OAAOxI,KAAKyI,0BAA0B/D,UAAW,cAC7D+F,yBAA2BzK,KAAKK,UAAUU,KAAK,8BAC3Cf,KAAK4D,uBAAuBG,MAAQ/D,KAAK6J,eAAe9F,MAAM,IAAOlD,OACzE6J,0BAA4B1K,KAAKK,UAAUU,KAAK,+BAC5Cf,KAAK4D,uBAAuBG,MAAQ/D,KAAK6J,eAAe9F,MAAM,IAAOT,IAAI,oBAAoBzC,WAEhGb,KAAKkK,eAAenG,QAChB/D,KAAKkK,eAAenG,OAAS0G,yBAA2BD,YAA4C,IAA9BE,0BAAiC,KACxGC,UAAY5G,KAAKG,QACrByG,UAAUrK,SAAS,YACdqC,IAAI,MAAO,IACXA,IAAI,OAAQ,IACZA,IAAI,YAAa,SACjB2B,aAAaP,MACb6F,YAAY,UACZQ,MAAMO,WACXtD,gBAAgBuD,yBAAyBD,aASjDhL,wBAAwBgB,UAAUqG,mBAAqB,SAASjD,cACxD8G,YAAc7K,KAAKK,UAAUU,KAAK,+BAClCf,KAAK4D,uBAAuBG,MAAQ/D,KAAK6J,eAAe9F,MAAM,IAAOT,IAAI,oBACzEwH,eAAiBD,YAAYhK,OAC1BiK,eAAiB,GACpBD,YAAYE,QAAQ/I,SACpB8I,kBAURnL,wBAAwBgB,UAAU4J,SAAW,SAASxG,UAC9CJ,SAAW3D,KAAK4D,uBAAuBG,aACpC/D,KAAKK,UAAUU,KAAK,uBAAyB4C,WAQxDhE,wBAAwBgB,UAAUiB,QAAU,eACpCuG,MAAQnI,KAAKc,UACbkK,kBAAoB7C,MAAM8C,IAAI,GAAGC,oBACd/C,MAAMrC,QAEHkF,mBAS9BrL,wBAAwBgB,UAAUqC,mBAAqB,SAASmI,QAASC,UACjExJ,QAAUyH,WAAWrJ,KAAK4B,WAC1B5B,KAAKG,aACLyB,QAAU,GAEdtC,EAAE6L,SAASxI,IAAI,qBACU,SAAWf,QAAU,qBACxB,SAAWA,QAAU,oBACtB,SAAWA,QAAU,mBACtB,SAAWA,QAAU,cACxB,SAAWA,QAAU,uBACdwJ,QAS5BzL,wBAAwBgB,UAAUuJ,eAAiB,SAASnG,aACjDA,KAAK0C,SAAS,aASzB9G,wBAAwBgB,UAAUD,2BAA6B,WAIvDV,KAAKO,kBAK0B,OAA/BP,KAAKqL,uBACLC,aAAatL,KAAKqL,uBAMlBrL,KAAKQ,wBAAwBK,OAAS,OACjCwK,sBAAwBE,YAAW,gBAC/B7K,+BACN,WAKFH,iBAAkB,OAClBkJ,kBACArG,uBACAxC,mBAQTjB,wBAAwBgB,UAAUH,sBAAwB,kBAC/CR,KAAKK,UAAUU,KAAK,gCAAgCuC,KAAI,SAASU,EAAGwH,gBAChExL,KAAKyL,cAAcD,aAUlC7L,wBAAwBgB,UAAU8K,cAAgB,SAASC,mBAChDA,WAAWC,UAAyC,IAA7BD,WAAWE,mBASzCvE,gBAAkB,CAKlBwE,0BAA0B,EAM1BC,+BAAgC,GAKhC3L,YAAY,EAKZ4L,sBAAsB,EAKtBC,UAAW,GASXC,KAAM,SAASrM,YAAaC,SAAUC,qBAClCuH,gBAAgB2E,UAAUpM,aACtB,IAAID,wBAAwBC,YAAaC,SAAUC,kBAClDuH,gBAAgBwE,2BACjBxE,gBAAgB6E,qBAChB7E,gBAAgBwE,0BAA2B,IAE1CxE,gBAAgByE,+BAA+BK,eAAevM,aAAc,CAC7EyH,gBAAgByE,+BAA+BlM,cAAe,MAE1DwM,kBAAoBpG,SAASC,eAAerG,aAC5CwM,kBAAkBC,UAAUC,SAAS,cACpCF,kBAAkBC,UAAUC,SAAS,6BAEtCjF,gBAAgBuD,yBAAyBtL,EAAE8M,mBAAmBrL,KAAK,0BACnEsG,gBAAgBuD,yBAAyBtL,EAAE8M,mBAAmBrL,KAAK,4BAQ/EmL,mBAAoB,WAChB5M,EAAEiN,QAAQC,GAAG,UAAU,WACnBnF,gBAAgBoF,oBAAmB,MAEvCF,OAAOG,iBAAiB,eAAe,WACnCrF,gBAAgBlH,YAAa,EAC7BkH,gBAAgBoF,mBAAmBpF,gBAAgBlH,eAEvDoM,OAAOG,iBAAiB,cAAc,WAClCrF,gBAAgBlH,YAAa,EAC7BkH,gBAAgBoF,mBAAmBpF,gBAAgBlH,eAEvDoL,YAAW,WACPlE,gBAAgBsF,2BACjB,MAQP/B,yBAA0B,SAASO,SAC/BA,QACKqB,GAAG,uBAAwBnF,gBAAgBnB,iBAC3CsG,GAAG,mBAAoBnF,gBAAgBE,gBACvCqF,SAAQ,SAASzG,GACdkB,gBAAgBwF,oBAAoB1G,GAAG,MAE1C2G,UAAS,SAAS3G,GACfkB,gBAAgBwF,oBAAoB1G,GAAG,OAQnDD,gBAAiB,SAASC,GACtBA,EAAE4B,qBACEgF,SAAW1F,gBAAgB2F,oBAAoB7G,GAC/C4G,UACAA,SAAS7G,gBAAgBC,IAQjCoB,eAAgB,SAASpB,OACjB4G,SAAW1F,gBAAgB2F,oBAAoB7G,GAC/C4G,UACAA,SAASxF,eAAepB,IAQhCsG,mBAAoB,SAAStM,gBACpB,IAAIP,eAAeyH,gBAAgB2E,UAChC3E,gBAAgB2E,UAAUG,eAAevM,eACzCyH,gBAAgB2E,UAAUpM,aAAaO,WAAaA,WACpDkH,gBAAgB2E,UAAUpM,aAAawJ,iBAUnDyD,oBAAqB,SAAS1G,EAAG8G,cAC7B5F,gBAAgB0E,qBAAuBkB,cAQ3CN,uBAAwB,WACftF,gBAAgB0E,2BACZU,mBAAmBpF,gBAAgBlH,YAK5CoL,YAAW,WACPlE,gBAAgBsF,uBAAuBtF,gBAAgBlH,cACxD,MAQP6M,oBAAqB,SAAS7G,OACtBvG,YAAcN,EAAE6G,EAAE+G,eAAe5G,QAAQ,iBAAiBsC,KAAK,aAC5DvB,gBAAgB2E,UAAUpM,cAMrC0H,gBAAiB,iBACP6F,aAAenH,SAASC,eAAe,gBAC7CvG,kBAAkB0N,gBAAgBD,sBAOnC,CASHlB,KAAM5E,gBAAgB4E"} \ 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 * Question class for drag and drop marker question type, used to support the question and preview pages.\n *\n * @module qtype_ddmarker/question\n * @copyright 2018 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_ddmarker/shapes',\n 'core/key_codes',\n 'core_form/changechecker',\n 'core_filters/events',\n], function(\n $,\n dragDrop,\n Shapes,\n keys,\n FormChangeChecker,\n filterEvent\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 shape, coords and markertext.\n * @constructor\n */\n function DragDropMarkersQuestion(containerId, readOnly, visibleDropZones) {\n var thisQ = this;\n this.containerId = containerId;\n this.visibleDropZones = visibleDropZones;\n this.shapes = [];\n this.shapeSVGs = [];\n this.isPrinting = false;\n this.questionAnswer = {};\n if (readOnly) {\n this.getRoot().addClass('qtype_ddmarker-readonly');\n }\n thisQ.allImagesLoaded = false;\n thisQ.getNotYetLoadedImages().one('load', function() {\n thisQ.waitForAllImagesToBeLoaded();\n });\n thisQ.waitForAllImagesToBeLoaded();\n }\n\n /**\n * Draws the svg shapes of any drop zones that should be visible for feedback purposes.\n */\n DragDropMarkersQuestion.prototype.drawDropzones = function() {\n if (this.visibleDropZones.length > 0) {\n var bgImage = this.bgImage();\n\n this.getRoot().find('div.dropzones').html('');\n var svg = this.getRoot().find('svg.dropzones');\n\n var nextColourIndex = 0;\n for (var dropZoneNo = 0; dropZoneNo < this.visibleDropZones.length; dropZoneNo++) {\n var colourClass = 'color' + nextColourIndex;\n nextColourIndex = (nextColourIndex + 1) % 8;\n this.addDropzone(svg, dropZoneNo, colourClass);\n }\n }\n };\n\n /**\n * Adds a dropzone shape with colour, coords and link provided to the array of shapes.\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 DragDropMarkersQuestion.prototype.addDropzone = function(svg, dropZoneNo, colourClass) {\n var dropZone = this.visibleDropZones[dropZoneNo],\n shape = Shapes.make(dropZone.shape, ''),\n existingmarkertext,\n bgRatio = this.bgRatio();\n if (!shape.parse(dropZone.coords, bgRatio)) {\n return;\n }\n\n existingmarkertext = this.getRoot().find('div.markertexts span.markertext' + dropZoneNo);\n if (existingmarkertext.length) {\n if (dropZone.markertext !== '') {\n existingmarkertext.html(dropZone.markertext);\n } else {\n existingmarkertext.remove();\n }\n } else if (dropZone.markertext !== '') {\n var classnames = 'markertext markertext' + dropZoneNo;\n this.getRoot().find('div.markertexts').append('' +\n dropZone.markertext + '');\n var markerspan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n if (markerspan.length) {\n var handles = shape.getHandlePositions();\n var positionLeft = handles.moveHandle.x - (markerspan.outerWidth() / 2) - 4;\n var positionTop = handles.moveHandle.y - (markerspan.outerHeight() / 2);\n markerspan\n .css('left', positionLeft)\n .css('top', positionTop);\n markerspan\n .data('originX', markerspan.position().left / bgRatio)\n .data('originY', markerspan.position().top / bgRatio);\n this.handleElementScale(markerspan, 'center');\n }\n }\n\n var shapeSVG = shape.makeSvg(svg[0]);\n shapeSVG.setAttribute('class', 'dropzone ' + colourClass);\n\n this.shapes[this.shapes.length] = shape;\n this.shapeSVGs[this.shapeSVGs.length] = shapeSVG;\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 DragDropMarkersQuestion.prototype.repositionDrags = function() {\n var root = this.getRoot(),\n thisQ = this;\n\n root.find('div.draghomes .marker').not('.dragplaceholder').each(function(key, item) {\n $(item).addClass('unneeded');\n });\n\n root.find('input.choices').each(function(key, input) {\n var choiceNo = thisQ.getChoiceNoFromElement(input),\n imageCoords = thisQ.getImageCoords(input);\n if (imageCoords.length) {\n var drag = thisQ.getRoot().find('.draghomes' + ' span.marker' + '.choice' + choiceNo).not('.dragplaceholder');\n drag.remove();\n for (var i = 0; i < imageCoords.length; i++) {\n var dragInDrop = drag.clone();\n // Convert image coords to screen coords.\n const screenCoords = thisQ.convertToWindowXY(imageCoords[i]);\n dragInDrop.data('pagex', screenCoords.x).data('pagey', screenCoords.y);\n // Save image coords to the drag item so we can use it later.\n dragInDrop.data('imageCoords', imageCoords[i]);\n // We always save the coordinates in the 1:1 ratio.\n // So we need to set the scale ratio to 1 for the initial load.\n dragInDrop.data('scaleRatio', 1);\n thisQ.sendDragToDrop(dragInDrop, false, true);\n }\n thisQ.getDragClone(drag).addClass('active');\n thisQ.cloneDragIfNeeded(drag);\n }\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropMarkersQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.choices').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropMarkersQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Determine what drag items need to be shown and\n * return coords of all drag items except any that are currently being dragged\n * based on contents of hidden inputs and whether drags are 'infinite' or how many\n * drags should be shown.\n *\n * @param {jQuery} inputNode\n * @returns {Point[]} image coordinates of however many copies of the drag item should be shown.\n */\n DragDropMarkersQuestion.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] = Shapes.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 DragDropMarkersQuestion.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 DragDropMarkersQuestion.prototype.convertToBgImgXY = function(point) {\n var bgImage = this.bgImage();\n return point.offset(-bgImage.offset().left - 1, -bgImage.offset().top - 1);\n };\n\n /**\n * Is the point within the background image?\n *\n * @param {Point} point relative to the BG image.\n * @return {boolean} true it they are.\n */\n DragDropMarkersQuestion.prototype.coordsInBgImg = function(point) {\n var bgImage = this.bgImage();\n var bgPosition = bgImage.offset();\n\n return point.x >= bgPosition.left && point.x < bgPosition.left + bgImage.width()\n && point.y >= bgPosition.top && point.y < bgPosition.top + bgImage.height();\n };\n\n /**\n * Get the outer div for this question.\n * @returns {jQuery} containing that div.\n */\n DragDropMarkersQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get the img that is the background image.\n * @returns {jQuery} containing that img.\n */\n DragDropMarkersQuestion.prototype.bgImage = function() {\n return this.getRoot().find('img.dropbackground');\n };\n\n DragDropMarkersQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n dragged = $(e.target).closest('.marker');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragged.addClass('beingdragged').css('transform', '');\n\n var placed = !dragged.hasClass('unneeded');\n if (!placed) {\n var hiddenDrag = thisQ.getDragClone(dragged);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n dragged.offset(hiddenDrag.offset());\n }\n }\n\n dragDrop.start(e, dragged, function() {\n void (1);\n }, function(x, y, dragged) {\n thisQ.dragEnd(dragged);\n });\n };\n\n /**\n * Functionality at the end of a drag drop.\n * @param {jQuery} dragged the marker that was dragged.\n */\n DragDropMarkersQuestion.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 Shapes.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 Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n dragged.data('originX', bgImgXY.x).data('originY', bgImgXY.y);\n }\n\n if (!placed) {\n this.sendDragHome(dragged);\n this.removeDragIfNeeded(dragged);\n } else {\n this.cloneDragIfNeeded(dragged);\n }\n\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Save the coordinates for a dropped item in the form field.\n * @param {Number} choiceNo which copy of the choice this was.\n */\n DragDropMarkersQuestion.prototype.saveCoordsForChoice = function(choiceNo) {\n let imageCoords = [];\n var items = this.getRoot().find('div.droparea span.marker.choice' + choiceNo),\n thiQ = this,\n bgRatio = this.bgRatio();\n\n if (items.length) {\n items.each(function() {\n var drag = $(this);\n if (!drag.hasClass('beingdragged') && !drag.data('imageCoords')) {\n if (drag.data('scaleRatio') !== bgRatio) {\n // The scale ratio for the draggable item was changed. We need to update that.\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n }\n var dragXY = new Shapes.Point(drag.data('pagex'), drag.data('pagey'));\n if (thiQ.coordsInBgImg(dragXY)) {\n var bgImgXY = thiQ.convertToBgImgXY(dragXY);\n bgImgXY = new Shapes.Point(bgImgXY.x / bgRatio, bgImgXY.y / bgRatio);\n imageCoords[imageCoords.length] = bgImgXY;\n }\n } else if (drag.data('imageCoords')) {\n imageCoords[imageCoords.length] = drag.data('imageCoords');\n }\n });\n }\n\n this.getRoot().find('input.choice' + choiceNo).val(imageCoords.join(';'));\n if (this.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n this.questionAnswer = this.getQuestionAnsweredValues();\n }\n };\n\n /**\n * Handle key down / press events on markers.\n * @param {KeyboardEvent} e\n */\n DragDropMarkersQuestion.prototype.handleKeyPress = function(e) {\n var drag = $(e.target).closest('.marker'),\n point = new Shapes.Point(drag.offset().left, drag.offset().top),\n choiceNo = this.getChoiceNoFromElement(drag);\n\n switch (e.keyCode) {\n case keys.arrowLeft:\n case 65: // A.\n point.x -= 1;\n break;\n case keys.arrowRight:\n case 68: // D.\n point.x += 1;\n break;\n case keys.arrowDown:\n case 83: // S.\n point.y += 1;\n break;\n case keys.arrowUp:\n case 87: // W.\n point.y -= 1;\n break;\n case keys.space:\n case keys.escape:\n point = null;\n break;\n default:\n return; // Ingore other keys.\n }\n e.preventDefault();\n\n if (point !== null) {\n point = this.constrainToBgImg(point);\n drag.offset({'left': point.x, 'top': point.y});\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n var dragXY = this.convertToBgImgXY(new Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n drag.data('originX', dragXY.x / this.bgRatio()).data('originY', dragXY.y / this.bgRatio());\n if (this.coordsInBgImg(new Shapes.Point(drag.offset().left, drag.offset().top))) {\n if (drag.hasClass('unneeded')) {\n this.sendDragToDrop(drag, true);\n var hiddenDrag = this.getDragClone(drag);\n if (hiddenDrag.length) {\n hiddenDrag.addClass('active');\n }\n this.cloneDragIfNeeded(drag);\n }\n }\n } else {\n drag.css('left', '').css('top', '');\n drag.data('pagex', drag.offset().left).data('pagey', drag.offset().top);\n this.sendDragHome(drag);\n this.removeDragIfNeeded(drag);\n }\n drag.focus();\n this.saveCoordsForChoice(choiceNo);\n };\n\n /**\n * Makes sure the dragged item always exists within the background image area.\n *\n * @param {Point} windowxy\n * @returns {Point} coordinates\n */\n DragDropMarkersQuestion.prototype.constrainToBgImg = function(windowxy) {\n var bgImg = this.bgImage(),\n bgImgXY = this.convertToBgImgXY(windowxy);\n bgImgXY.x = Math.max(0, bgImgXY.x);\n bgImgXY.y = Math.max(0, bgImgXY.y);\n bgImgXY.x = Math.min(bgImg.width(), bgImgXY.x);\n bgImgXY.y = Math.min(bgImg.height(), bgImgXY.y);\n return this.convertToWindowXY(bgImgXY);\n };\n\n /**\n * Returns the choice number for a node.\n *\n * @param {Element|jQuery} node\n * @returns {Number}\n */\n DragDropMarkersQuestion.prototype.getChoiceNoFromElement = function(node) {\n return Number(this.getClassnameNumericSuffix(node, 'choice'));\n };\n\n /**\n * Returns the numeric part of a class with the given prefix.\n *\n * @param {Element|jQuery} node\n * @param {String} prefix\n * @returns {Number|null}\n */\n DragDropMarkersQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = $(node).attr('class');\n if (classes !== undefined && classes !== '') {\n var classesarr = classes.split(' ');\n for (var index = 0; index < classesarr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesarr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesarr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Handle when the window is resized.\n */\n DragDropMarkersQuestion.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 shape = thisQ.shapes[dropZoneNo];\n var shapeSVG = thisQ.shapeSVGs[dropZoneNo];\n shape.parse(originCoords, bgRatio);\n shape.updateSvg(shapeSVG);\n\n var handles = shape.getHandlePositions();\n var markerSpan = this.getRoot().find('div.ddarea div.markertexts span.markertext' + dropZoneNo);\n markerSpan\n .css('left', handles.moveHandle.x - (markerSpan.outerWidth() / 2) - 4)\n .css('top', handles.moveHandle.y - (markerSpan.outerHeight() / 2));\n thisQ.handleElementScale(markerSpan, 'center');\n }\n };\n\n /**\n * Clone the drag.\n */\n DragDropMarkersQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n this.getRoot().find('div.draghomes span.marker').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('marker');\n placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));\n placeHolder.addClass(thisQ.getDragNoClass(drag, false));\n placeHolder.addClass('dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Get the drag number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the drag number.\n */\n DragDropMarkersQuestion.prototype.getDragNo = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'dragno');\n };\n\n /**\n * Get the drag number prefix of a drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} includeSelector include the CSS selector prefix or not.\n * @return {String} Class name\n */\n DragDropMarkersQuestion.prototype.getDragNoClass = function(drag, includeSelector) {\n var className = 'dragno' + this.getDragNo(drag);\n if (this.isInfiniteDrag(drag)) {\n className = 'infinite';\n }\n\n if (includeSelector) {\n return '.' + className;\n }\n\n return className;\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropMarkersQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draghomes' + ' span.marker' +\n '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');\n };\n\n /**\n * Get the drop area element.\n * @returns {jQuery} droparea element.\n */\n DragDropMarkersQuestion.prototype.dropArea = function() {\n return this.getRoot().find('div.droparea');\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropMarkersQuestion.prototype.sendDragHome = function(drag) {\n drag.removeClass('beingdragged')\n .addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n var placeHolder = this.getDragClone(drag);\n placeHolder.after(drag);\n placeHolder.removeClass('active');\n };\n\n /**\n * Animate a drag item into a given place.\n *\n * @param {jQuery} drag the item to place.\n * @param {boolean} isScaling Scaling or not.\n * @param {boolean} initialLoad Whether it is the initial load or not.\n */\n DragDropMarkersQuestion.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 Shapes.Point(drag.data('pagex'), drag.data('pagey')));\n if (isScaling) {\n drag.data('originX', dragXY.x / bgRatio).data('originY', dragXY.y / bgRatio);\n drag.css('left', dragXY.x).css('top', dragXY.y);\n } else {\n drag.data('originX', dragXY.x).data('originY', dragXY.y);\n drag.css('left', dragXY.x * bgRatio).css('top', dragXY.y * bgRatio);\n }\n // We need to save the original scale ratio for each draggable item.\n if (!initialLoad) {\n // Only set the scale ratio for a current being-dragged item, not for the initial loading.\n drag.data('scaleRatio', bgRatio);\n }\n dropArea.append(drag);\n this.handleElementScale(drag, 'left top');\n };\n\n /**\n * Clone the drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.cloneDragIfNeeded = function(drag) {\n var inputNode = this.getInput(drag),\n noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),\n displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,\n displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;\n\n if ((this.isInfiniteDrag(drag) ||\n !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {\n var dragClone = drag.clone();\n dragClone.addClass('unneeded')\n .css('top', '')\n .css('left', '')\n .css('transform', '');\n this.getDragClone(drag)\n .removeClass('active')\n .after(dragClone);\n questionManager.addEventHandlersToMarker(dragClone);\n }\n };\n\n /**\n * Remove the clone drag at the draghome area if needed.\n *\n * @param {jQuery} drag the item to place.\n */\n DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {\n var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +\n this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');\n var displayedDrags = dragsInHome.length;\n while (displayedDrags > 1) {\n dragsInHome.first().remove();\n displayedDrags--;\n }\n };\n\n /**\n * Get the input belong to drag.\n *\n * @param {jQuery} drag the item to place.\n * @returns {jQuery} input element.\n */\n DragDropMarkersQuestion.prototype.getInput = function(drag) {\n var choiceNo = this.getChoiceNoFromElement(drag);\n return this.getRoot().find('input.choices.choice' + choiceNo);\n };\n\n /**\n * Return the background ratio.\n *\n * @returns {number} Background ratio.\n */\n DragDropMarkersQuestion.prototype.bgRatio = function() {\n var bgImg = this.bgImage();\n var bgImgNaturalWidth = bgImg.get(0).naturalWidth;\n var bgImgClientWidth = bgImg.width();\n\n return bgImgClientWidth / bgImgNaturalWidth;\n };\n\n /**\n * Scale the drag if needed.\n *\n * @param {jQuery} element the item to place.\n * @param {String} type scaling type\n */\n DragDropMarkersQuestion.prototype.handleElementScale = function(element, type) {\n var bgRatio = parseFloat(this.bgRatio());\n if (this.isPrinting) {\n bgRatio = 1;\n }\n $(element).css({\n '-webkit-transform': 'scale(' + bgRatio + ')',\n '-moz-transform': 'scale(' + bgRatio + ')',\n '-ms-transform': 'scale(' + bgRatio + ')',\n '-o-transform': 'scale(' + bgRatio + ')',\n 'transform': 'scale(' + bgRatio + ')',\n 'transform-origin': type\n });\n };\n\n /**\n * Check if the given drag is in infinite mode or not.\n *\n * @param {jQuery} drag The drag item need to check.\n */\n DragDropMarkersQuestion.prototype.isInfiniteDrag = function(drag) {\n return drag.hasClass('infinite');\n };\n\n /**\n * Waits until all images are loaded before calling setupQuestion().\n *\n * This function is called from the onLoad of each image, and also polls with\n * a time-out, because image on-loads are allegedly unreliable.\n */\n DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded = function() {\n var thisQ = this;\n // This method may get called multiple times (via image on-loads or timeouts.\n // If we are already done, don't do it again.\n if (this.allImagesLoaded) {\n return;\n }\n\n // Clear any current timeout, if set.\n if (this.imageLoadingTimeoutId !== null) {\n clearTimeout(this.imageLoadingTimeoutId);\n }\n\n // If we have not yet loaded all images, set a timeout to\n // call ourselves again, since apparently images on-load\n // events are flakey.\n if (this.getNotYetLoadedImages().length > 0) {\n this.imageLoadingTimeoutId = setTimeout(function() {\n this.waitForAllImagesToBeLoaded();\n }, 100);\n return;\n }\n\n // We now have all images. Carry on, but only after giving the layout a chance to settle down.\n this.allImagesLoaded = true;\n this.cloneDrags();\n this.repositionDrags();\n this.drawDropzones();\n // Wait for all dynamic content loaded by filter to be completed.\n document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => {\n elements.detail.nodes.forEach((element) => {\n thisQ.changeAllMakerToFilteredContent(element);\n });\n });\n };\n\n /**\n * Change all the maker related to the item that has been changed by filter to correct size and content.\n *\n * @param {object} filteredElement the element has been modified by filter.\n */\n DragDropMarkersQuestion.prototype.changeAllMakerToFilteredContent = function(filteredElement) {\n let currentFilteredItem = $(filteredElement);\n const parentIsMarker = currentFilteredItem.parent().closest('span.marker');\n const isMarker = currentFilteredItem.hasClass('marker');\n const root = this.getRoot();\n // The filtered element or parent element should a drag or drop item.\n if (!parentIsMarker && !isMarker) {\n return;\n }\n if (parentIsMarker) {\n currentFilteredItem = currentFilteredItem.parent().closest('span.marker');\n }\n if (root.find(currentFilteredItem).length <= 0) {\n // If the maker doesn't belong to this question\n // In case we have multiple questions in the same page.\n return;\n }\n const dragNo = this.getDragNo(currentFilteredItem);\n const choiceNo = this.getChoiceNoFromElement(currentFilteredItem);\n const listOfContainerToBeModifed = [\n 'div.draghomes .marker:not(.dragplaceholder).dragno' + dragNo + '.choice' + choiceNo,\n 'div.droparea .marker:not(.dragplaceholder).dragno' + dragNo + '.choice' + choiceNo,\n 'div.draghomes .marker:not(.dragplaceholder).infinite.choice' + choiceNo,\n 'div.droparea .marker:not(.dragplaceholder).infinite.choice' + choiceNo\n ];\n let listOfModifiedDragDrop = [];\n // We want to keep all the handler and event for filtered item, so using clone is the only choice.\n const filteredDragDropClone = currentFilteredItem.clone();\n listOfContainerToBeModifed.forEach(function(selector) {\n root.find(selector).each(function(i, node) {\n const originalClass = $(node).attr('class');\n const originalStyle = $(node).attr('style');\n // Replace the class and style of the maker we want to replace for the clone.\n filteredDragDropClone.attr('class', originalClass);\n filteredDragDropClone.attr('style', originalStyle);\n // Add event for the clone.\n questionManager.addEventHandlersToMarker(filteredDragDropClone);\n // Insert into DOM.\n $(node).before(filteredDragDropClone);\n listOfModifiedDragDrop.push(node);\n });\n });\n listOfModifiedDragDrop.forEach(function(node) {\n $(node).remove();\n });\n };\n\n /**\n * Get any of the images in the drag-drop area that are not yet fully loaded.\n *\n * @returns {jQuery} those images.\n */\n DragDropMarkersQuestion.prototype.getNotYetLoadedImages = function() {\n return this.getRoot().find('.ddmarker img.dropbackground').not(function(i, imgNode) {\n return this.imageIsLoaded(imgNode);\n });\n };\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n DragDropMarkersQuestion.prototype.imageIsLoaded = function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\n * @type {Object}\n */\n var questionManager = {\n\n /**\n * {boolean} ensures that the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} ensures that the marker event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n markerEventHandlersInitialised: {},\n\n /**\n * {boolean} is printing or not.\n */\n isPrinting: false,\n\n /**\n * {boolean} is keyboard navigation.\n */\n isKeyboardNavigation: false,\n\n /**\n * {Object} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {}, // An object containing all the information about each question on the page.\n\n /**\n * Initialise one question.\n *\n * @param {String} containerId the id of the div.que that contains this question.\n * @param {boolean} readOnly whether the question is read-only.\n * @param {Object[]} visibleDropZones data on any drop zones to draw as part of the feedback.\n */\n init: function(containerId, readOnly, visibleDropZones) {\n questionManager.questions[containerId] =\n new DragDropMarkersQuestion(containerId, readOnly, visibleDropZones);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.markerEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.markerEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('ddmarker') &&\n !questionContainer.classList.contains('qtype_ddmarker-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.draghomes .marker'));\n questionManager.addEventHandlersToMarker($(questionContainer).find('div.droparea .marker'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $(window).on('resize', function() {\n questionManager.handleWindowResize(false);\n });\n window.addEventListener('beforeprint', function() {\n questionManager.isPrinting = true;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n window.addEventListener('afterprint', function() {\n questionManager.isPrinting = false;\n questionManager.handleWindowResize(questionManager.isPrinting);\n });\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved();\n }, 100);\n },\n\n /**\n * Binding the event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToMarker: function(element) {\n element\n .on('mousedown touchstart', questionManager.handleDragStart)\n .on('keydown keypress', questionManager.handleKeyPress)\n .focusin(function(e) {\n questionManager.handleKeyboardFocus(e, true);\n })\n .focusout(function(e) {\n questionManager.handleKeyboardFocus(e, false);\n });\n },\n\n /**\n * Handle mouse down / touch start events on markers.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press events on markers.\n * @param {Event} e\n */\n handleKeyPress: function(e) {\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Handle when the window is resized.\n * @param {boolean} isPrinting\n */\n handleWindowResize: function(isPrinting) {\n for (var containerId in questionManager.questions) {\n if (questionManager.questions.hasOwnProperty(containerId)) {\n questionManager.questions[containerId].isPrinting = isPrinting;\n questionManager.questions[containerId].handleResize();\n }\n }\n },\n\n /**\n * Handle focus lost events on markers.\n * @param {Event} e\n * @param {boolean} isNavigating\n */\n handleKeyboardFocus: function(e, isNavigating) {\n questionManager.isKeyboardNavigation = isNavigating;\n },\n\n /**\n * Sometimes, despite our best efforts, things change in a way that cannot\n * be specifically caught (e.g. dock expanding or collapsing in Boost).\n * Therefore, we need to periodically check everything is in the right position.\n */\n fixLayoutIfThingsMoved: function() {\n if (!questionManager.isKeyboardNavigation) {\n this.handleWindowResize(questionManager.isPrinting);\n }\n // We use setTimeout after finishing work, rather than setInterval,\n // in case positioning things is slow. We want 100 ms gap\n // between executions, not what setInterval does.\n setTimeout(function() {\n questionManager.fixLayoutIfThingsMoved(questionManager.isPrinting);\n }, 100);\n },\n\n /**\n * Given an event, work out which question it effects.\n * @param {Event} e the event.\n * @returns {DragDropMarkersQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddmarker').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddmarker/question\n */\n return {\n /**\n * Initialise one drag-drop markers question.\n *\n * @param {String} containerId id of the outer div for this question.\n * @param {String} bgImgUrl the URL of the background image.\n * @param {boolean} readOnly whether the question is being displayed read-only.\n * @param {String[]} visibleDropZones the geometry of any drop-zones to show.\n */\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","Shapes","keys","FormChangeChecker","filterEvent","DragDropMarkersQuestion","containerId","readOnly","visibleDropZones","thisQ","this","shapes","shapeSVGs","isPrinting","questionAnswer","getRoot","addClass","allImagesLoaded","getNotYetLoadedImages","one","waitForAllImagesToBeLoaded","prototype","drawDropzones","length","bgImage","find","html","outerWidth","outerHeight","svg","nextColourIndex","dropZoneNo","colourClass","addDropzone","existingmarkertext","dropZone","shape","make","bgRatio","parse","coords","markertext","remove","classnames","append","markerspan","handles","getHandlePositions","positionLeft","moveHandle","x","positionTop","y","css","data","position","left","top","handleElementScale","shapeSVG","makeSvg","setAttribute","repositionDrags","root","not","each","key","item","input","choiceNo","getChoiceNoFromElement","imageCoords","getImageCoords","drag","i","dragInDrop","clone","screenCoords","convertToWindowXY","sendDragToDrop","getDragClone","cloneDragIfNeeded","getQuestionAnsweredValues","result","inputNode","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","val","coordsStrings","split","Point","point","offset","convertToBgImgXY","coordsInBgImg","bgPosition","width","height","document","getElementById","handleDragStart","e","dragged","target","closest","prepare","start","hasClass","hiddenDrag","dragEnd","dragXY","placed","bgImgXY","sendDragHome","removeDragIfNeeded","saveCoordsForChoice","items","thiQ","join","questionManager","handleFormDirty","handleKeyPress","keyCode","arrowLeft","arrowRight","arrowDown","arrowUp","space","escape","preventDefault","constrainToBgImg","focus","windowxy","bgImg","Math","max","min","node","Number","getClassnameNumericSuffix","prefix","classes","attr","undefined","classesarr","index","RegExp","test","match","exec","handleResize","parseFloat","originCoords","updateSvg","markerSpan","cloneDrags","draghome","placeHolder","removeClass","getDragNoClass","before","getDragNo","includeSelector","className","isInfiniteDrag","dropArea","after","isScaling","initialLoad","getInput","noOfDrags","displayedDragsInDropArea","displayedDragsInDragHomes","dragClone","addEventHandlersToMarker","dragsInHome","displayedDrags","first","bgImgNaturalWidth","get","naturalWidth","element","type","imageLoadingTimeoutId","clearTimeout","setTimeout","addEventListener","eventTypes","filterContentRenderingComplete","elements","detail","nodes","changeAllMakerToFilteredContent","filteredElement","currentFilteredItem","parentIsMarker","parent","isMarker","dragNo","listOfContainerToBeModifed","listOfModifiedDragDrop","filteredDragDropClone","selector","originalClass","originalStyle","push","imgNode","imageIsLoaded","imgElement","complete","naturalHeight","eventHandlersInitialised","markerEventHandlersInitialised","isKeyboardNavigation","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","classList","contains","window","on","handleWindowResize","fixLayoutIfThingsMoved","focusin","handleKeyboardFocus","focusout","question","getQuestionForEvent","isNavigating","currentTarget","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file diff --git a/question/type/ddmarker/amd/src/question.js b/question/type/ddmarker/amd/src/question.js index 6ff3d7df7269b..efe2caed37efd 100644 --- a/question/type/ddmarker/amd/src/question.js +++ b/question/type/ddmarker/amd/src/question.js @@ -26,13 +26,15 @@ define([ 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes', - 'core_form/changechecker' + 'core_form/changechecker', + 'core_filters/events', ], function( $, dragDrop, Shapes, keys, - FormChangeChecker + FormChangeChecker, + filterEvent ) { "use strict"; @@ -757,7 +759,7 @@ define([ * a time-out, because image on-loads are allegedly unreliable. */ DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded = function() { - + var thisQ = this; // This method may get called multiple times (via image on-loads or timeouts. // If we are already done, don't do it again. if (this.allImagesLoaded) { @@ -784,6 +786,64 @@ define([ this.cloneDrags(); this.repositionDrags(); this.drawDropzones(); + // Wait for all dynamic content loaded by filter to be completed. + document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => { + elements.detail.nodes.forEach((element) => { + thisQ.changeAllMakerToFilteredContent(element); + }); + }); + }; + + /** + * Change all the maker related to the item that has been changed by filter to correct size and content. + * + * @param {object} filteredElement the element has been modified by filter. + */ + DragDropMarkersQuestion.prototype.changeAllMakerToFilteredContent = function(filteredElement) { + let currentFilteredItem = $(filteredElement); + const parentIsMarker = currentFilteredItem.parent().closest('span.marker'); + const isMarker = currentFilteredItem.hasClass('marker'); + const root = this.getRoot(); + // The filtered element or parent element should a drag or drop item. + if (!parentIsMarker && !isMarker) { + return; + } + if (parentIsMarker) { + currentFilteredItem = currentFilteredItem.parent().closest('span.marker'); + } + if (root.find(currentFilteredItem).length <= 0) { + // If the maker doesn't belong to this question + // In case we have multiple questions in the same page. + return; + } + const dragNo = this.getDragNo(currentFilteredItem); + const choiceNo = this.getChoiceNoFromElement(currentFilteredItem); + const listOfContainerToBeModifed = [ + 'div.draghomes .marker:not(.dragplaceholder).dragno' + dragNo + '.choice' + choiceNo, + 'div.droparea .marker:not(.dragplaceholder).dragno' + dragNo + '.choice' + choiceNo, + 'div.draghomes .marker:not(.dragplaceholder).infinite.choice' + choiceNo, + 'div.droparea .marker:not(.dragplaceholder).infinite.choice' + choiceNo + ]; + let listOfModifiedDragDrop = []; + // We want to keep all the handler and event for filtered item, so using clone is the only choice. + const filteredDragDropClone = currentFilteredItem.clone(); + listOfContainerToBeModifed.forEach(function(selector) { + root.find(selector).each(function(i, node) { + const originalClass = $(node).attr('class'); + const originalStyle = $(node).attr('style'); + // Replace the class and style of the maker we want to replace for the clone. + filteredDragDropClone.attr('class', originalClass); + filteredDragDropClone.attr('style', originalStyle); + // Add event for the clone. + questionManager.addEventHandlersToMarker(filteredDragDropClone); + // Insert into DOM. + $(node).before(filteredDragDropClone); + listOfModifiedDragDrop.push(node); + }); + }); + listOfModifiedDragDrop.forEach(function(node) { + $(node).remove(); + }); }; /** diff --git a/question/type/ddmarker/renderer.php b/question/type/ddmarker/renderer.php index 48f4c29859276..6b3c64b0bfc5a 100644 --- a/question/type/ddmarker/renderer.php +++ b/question/type/ddmarker/renderer.php @@ -68,6 +68,7 @@ public function formulation_and_controls(question_attempt $qa, $orderedgroup = $question->get_ordered_choices(1); $hiddenfields = ''; + $dragitems = ''; foreach ($orderedgroup as $choiceno => $drag) { $classes = ['marker', 'user-select-none', 'choice' . $choiceno]; $attr = []; @@ -81,14 +82,17 @@ public function formulation_and_controls(question_attempt $qa, } $dragoutput = html_writer::start_span(join(' ', $classes), $attr); $targeticonhtml = $this->output->image_icon('crosshairs', '', $componentname, ['class' => 'target']); - $markertext = html_writer::span($drag->text, 'markertext'); + $markertext = html_writer::span(question_utils::format_question_fragment($drag->text, $this->page->context), + 'markertext'); $dragoutput .= $targeticonhtml . $markertext; $dragoutput .= html_writer::end_span(); - $output .= $dragoutput; + $dragitems .= $dragoutput; $hiddenfields .= $this->hidden_field_choice($qa, $choiceno, $drag->infinite, $drag->noofdrags); } - + $output .= $dragitems; $output .= html_writer::end_div(); + // Add extra hidden drag items so we can make sure the filter will be applied. + $output .= html_writer::div($dragitems, 'dd-original d-none'); $output .= html_writer::end_div(); if ($question->showmisplaced && $qa->get_state()->is_finished()) { diff --git a/question/type/ddmarker/tests/behat/preview.feature b/question/type/ddmarker/tests/behat/preview.feature index 528c98e20ade9..68d4fd11d1775 100644 --- a/question/type/ddmarker/tests/behat/preview.feature +++ b/question/type/ddmarker/tests/behat/preview.feature @@ -18,8 +18,9 @@ Feature: Preview a drag-drop marker question | contextlevel | reference | name | | Course | C1 | Test questions | And the following "questions" exist: - | questioncategory | qtype | name | template | - | Test questions | ddmarker | Drag markers | mkmap | + | questioncategory | qtype | name | template | + | Test questions | ddmarker | Drag markers | mkmap | + | Test questions | ddmarker | Drag to mathjax equation | mathjax | @javascript @_bug_phantomjs Scenario: Preview a question using the mouse @@ -56,3 +57,12 @@ Feature: Preview a drag-drop marker question And I press "Submit and finish" Then the state of "Please place the markers on the map of Milton Keynes" question is shown as "Correct" And I should see "Mark 1.00 out of 1.00" + + @javascript + Scenario: Preview a drag-drop marker question with mathjax question. + Given the "mathjaxloader" filter is "on" + And the "mathjaxloader" filter applies to "content and headings" + And I am on the "Drag to mathjax equation" "core_question > preview" page logged in as teacher + And I press "Fill in correct responses" + When I press "Submit and finish" + Then ".filter_mathjaxloader_equation" "css_element" should exist in the ".droparea" "css_element" diff --git a/question/type/ddmarker/tests/helper.php b/question/type/ddmarker/tests/helper.php index 8863f11a595fa..e3f021fe923b6 100644 --- a/question/type/ddmarker/tests/helper.php +++ b/question/type/ddmarker/tests/helper.php @@ -35,7 +35,7 @@ */ class qtype_ddmarker_test_helper extends question_test_helper { public function get_test_questions() { - return array('fox', 'maths', 'mkmap', 'zerodrag'); + return ['fox', 'maths', 'mkmap', 'zerodrag', 'mathjax']; } /** @@ -198,6 +198,50 @@ public function get_ddmarker_question_form_data_mkmap() { return $fromform; } + /** + * Return the test data needed by the question generator to create question tempplate. + * + * @return stdClass date to create a ddmarkers question. + */ + public function get_ddmarker_question_form_data_mathjax() { + global $CFG, $USER; + $fromform = new stdClass(); + + $bgdraftitemid = 0; + file_prepare_draft_area($bgdraftitemid, null, null, null, null); + $fs = get_file_storage(); + $filerecord = new stdClass(); + $filerecord->contextid = context_user::instance($USER->id)->id; + $filerecord->component = 'user'; + $filerecord->filearea = 'draft'; + $filerecord->itemid = $bgdraftitemid; + $filerecord->filepath = '/'; + $filerecord->filename = 'mkmap.png'; + $fs->create_file_from_pathname($filerecord, $CFG->dirroot . + '/question/type/ddmarker/tests/fixtures/mkmap.png'); + + $fromform->name = 'Drag-and-drop into maker question with equation'; + $fromform->questiontext = ['text' => 'Fill in the correct mathjax equation: y = 2, x =4', 'format' => FORMAT_HTML]; + + $fromform->defaultmark = 1; + $fromform->generalfeedback = ['text' => 'The right answer is: "y = x^2"', 'format' => FORMAT_HTML]; + $fromform->bgimage = $bgdraftitemid; + $fromform->shuffleanswers = 0; + $fromform->drags = [ + ['label' => '$$ y = x^2 $$', 'noofdrags' => '1'], + ['label' => '$$ y = x^5 $$', 'noofdrags' => '1'], + ]; + $fromform->drops = [ + ['shape' => 'circle', 'coords' => '322,213;10', 'choice' => 1], + ['shape' => 'circle', 'coords' => '144,84;10', 'choice' => 2], + ]; + test_question_maker::set_standard_combined_feedback_form_data($fromform); + $fromform->penalty = '0.3333333'; + $fromform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY; + + return $fromform; + } + /** * Return the test data needed by the question generator (the data that * would come from saving the editing form). From b4a9ce93a6ea2dc70c6fee565e52c09ef9c35c54 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Fri, 3 May 2024 09:21:45 +0700 Subject: [PATCH 005/119] MDL-78662 ddwtos: Allow answer to support filter dynamic content. --- question/type/ddwtos/amd/build/ddwtos.min.js | 2 +- .../type/ddwtos/amd/build/ddwtos.min.js.map | 2 +- question/type/ddwtos/amd/src/ddwtos.js | 97 +++++++++++++++++-- question/type/ddwtos/renderer.php | 6 +- question/type/ddwtos/styles.css | 4 + .../type/ddwtos/tests/behat/preview.feature | 16 ++- question/type/ddwtos/tests/helper.php | 26 ++++- 7 files changed, 134 insertions(+), 19 deletions(-) diff --git a/question/type/ddwtos/amd/build/ddwtos.min.js b/question/type/ddwtos/amd/build/ddwtos.min.js index f2af9705969e6..2d5fdccec9702 100644 --- a/question/type/ddwtos/amd/build/ddwtos.min.js +++ b/question/type/ddwtos/amd/build/ddwtos.min.js @@ -22,6 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.6 */ -define("qtype_ddwtos/ddwtos",["jquery","core/dragdrop","core/key_codes","core_form/changechecker"],(function($,dragDrop,keys,FormChangeChecker){function DragDropToTextQuestion(containerId,readOnly){this.containerId=containerId,this.questionAnswer={},readOnly&&this.getRoot().addClass("qtype_ddwtos-readonly"),this.resizeAllDragsAndDrops(),this.cloneDrags(),this.positionDrags()}DragDropToTextQuestion.prototype.resizeAllDragsAndDrops=function(){var thisQ=this;this.getRoot().find(".answercontainer > div").each((function(i,node){thisQ.resizeAllDragsAndDropsInGroup(thisQ.getClassnameNumericSuffix($(node),"draggrouphomes"))}))},DragDropToTextQuestion.prototype.resizeAllDragsAndDropsInGroup=function(group){var thisQ=this,dragHomes=this.getRoot().find(".draggrouphomes"+group+" span.draghome"),maxWidth=0,maxHeight=0;dragHomes.each((function(i,drag){maxWidth=Math.max(maxWidth,Math.ceil(drag.offsetWidth)),maxHeight=Math.max(maxHeight,Math.ceil(0+drag.offsetHeight))})),maxWidth+=8,maxHeight+=2,dragHomes.each((function(i,drag){thisQ.setElementSize(drag,maxWidth,maxHeight)})),this.getRoot().find("span.drop.group"+group).each((function(i,drop){thisQ.setElementSize(drop,maxWidth,maxHeight)}))},DragDropToTextQuestion.prototype.setElementSize=function(element,width,height){$(element).width(width).height(height).css("lineHeight",height+"px")},DragDropToTextQuestion.prototype.cloneDrags=function(){var thisQ=this;thisQ.getRoot().find("span.draghome").each((function(index,draghome){var drag=$(draghome),placeHolder=drag.clone();placeHolder.removeClass(),placeHolder.addClass("draghome choice"+thisQ.getChoice(drag)+" group"+thisQ.getGroup(drag)+" dragplaceholder"),drag.before(placeHolder)}))},DragDropToTextQuestion.prototype.positionDrags=function(){var thisQ=this,root=this.getRoot();root.find("span.draghome").not(".dragplaceholder").each((function(i,dragNode){var drag=$(dragNode),currentPlace=thisQ.getClassnameNumericSuffix(drag,"inplace");drag.addClass("unplaced").removeClass("placed"),drag.removeAttr("tabindex"),null!==currentPlace&&drag.removeClass("inplace"+currentPlace)})),root.find("input.placeinput").each((function(i,inputNode){var input=$(inputNode),choice=input.val(),place=thisQ.getPlace(input),drop=root.find(".drop.place"+place),dropPosition=drop.offset();if(drop.data("prev-top",dropPosition.top).data("prev-left",dropPosition.left),"0"!==choice){var unplacedDrag=thisQ.getUnplacedChoice(thisQ.getGroup(input),choice),hiddenDrag=thisQ.getDragClone(unplacedDrag);if(hiddenDrag.length)if(unplacedDrag.hasClass("infinite")){var noOfDrags=thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));if(thisQ.getInfiniteDragClones(unplacedDrag,!1).length{result[inputNode.id]=inputNode.value})),result},DragDropToTextQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropToTextQuestion.prototype.handleDragStart=function(e){var thisQ=this,drag=$(e.target).closest(".draghome");if(dragDrop.prepare(e).start&&!drag.hasClass("beingdragged")){drag.addClass("beingdragged");var currentPlace=this.getClassnameNumericSuffix(drag,"inplace");if(null!==currentPlace){this.setInputValue(currentPlace,0),drag.removeClass("inplace"+currentPlace);var hiddenDrop=thisQ.getDrop(drag,currentPlace);hiddenDrop.length&&(hiddenDrop.addClass("active"),drag.offset(hiddenDrop.offset()))}else{var hiddenDrag=thisQ.getDragClone(drag);if(hiddenDrag.length)if(drag.hasClass("infinite")){var noOfDrags=this.noOfDropsInGroup(this.getGroup(drag));if(this.getInfiniteDragClones(drag,!1).length1;)choice--,previous=this.getUnplacedChoice(group,choice);return previous},DragDropToTextQuestion.prototype.animateTo=function(drag,target){var currentPos=drag.offset(),targetPos=target.offset(),thisQ=this;M.util.js_pending("qtype_ddwtos-animate-"+thisQ.containerId),drag.animate({left:parseInt(drag.css("left"))+targetPos.left-currentPos.left,top:parseInt(drag.css("top"))+targetPos.top-currentPos.top},{duration:"fast",done:function(){$("body").trigger("qtype_ddwtos-dragmoved",[drag,target,thisQ]),M.util.js_complete("qtype_ddwtos-animate-"+thisQ.containerId)}})},DragDropToTextQuestion.prototype.isPointInDrop=function(pageX,pageY,drop){var position=drop.offset();return pageX>=position.left&&pageX=position.top&&pageY1&&thisQ.getInfiniteDragClones(drag,!0).first().remove()),void 0!==drag.data("isfocus")&&!0===drag.data("isfocus")&&(drag.focus(),drag.removeData("isfocus")),void 0!==target.data("isfocus")&&!0===target.data("isfocus")&&target.removeData("isfocus"),questionManager.isKeyboardNavigation&&(questionManager.isKeyboardNavigation=!1),thisQ.isQuestionInteracted()&&(questionManager.handleFormDirty(),thisQ.questionAnswer=thisQ.getQuestionAnsweredValues())},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); +define("qtype_ddwtos/ddwtos",["jquery","core/dragdrop","core/key_codes","core_form/changechecker","core_filters/events"],(function($,dragDrop,keys,FormChangeChecker,filterEvent){function DragDropToTextQuestion(containerId,readOnly){const thisQ=this;this.containerId=containerId,this.questionAnswer={},this.questionDragDropWidthHeight=[],readOnly&&this.getRoot().addClass("qtype_ddwtos-readonly"),this.resizeAllDragsAndDrops(),this.cloneDrags(),this.positionDrags(),document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete,(elements=>{elements.detail.nodes.forEach((element=>{thisQ.changeAllDragsAndDropsToFilteredContent(element)}))}))}DragDropToTextQuestion.prototype.resizeAllDragsAndDrops=function(){var thisQ=this;this.getRoot().find(".answercontainer > div").each((function(i,node){thisQ.resizeAllDragsAndDropsInGroup(thisQ.getClassnameNumericSuffix($(node),"draggrouphomes"))}))},DragDropToTextQuestion.prototype.resizeAllDragsAndDropsInGroup=function(group){var thisQ=this,dragDropItems=this.getRoot().find("span.group"+group),maxWidth=0,maxHeight=0;dragDropItems.each((function(i,drag){maxWidth=Math.max(maxWidth,Math.ceil(drag.offsetWidth)),maxHeight=Math.max(maxHeight,Math.ceil(0+drag.offsetHeight))})),maxWidth+=8,maxHeight+=2,thisQ.questionDragDropWidthHeight[group]={maxWidth:maxWidth,maxHeight:maxHeight},dragDropItems.each((function(i,drag){thisQ.setElementSize(drag,maxWidth,maxHeight)}))},DragDropToTextQuestion.prototype.changeAllDragsAndDropsToFilteredContent=function(filteredElement){let currentFilteredItem=$(filteredElement);const parentIsDD=currentFilteredItem.parent().closest("span").hasClass("placed")||currentFilteredItem.parent().closest("span").hasClass("draghome"),isDD=currentFilteredItem.hasClass("placed")||currentFilteredItem.hasClass("draghome");if(!parentIsDD&&!isDD)return;parentIsDD&&(currentFilteredItem=currentFilteredItem.parent().closest("span"));const thisQ=this;if(thisQ.getRoot().find(currentFilteredItem).length<=0)return;const group=thisQ.getGroup(currentFilteredItem),choice=thisQ.getChoice(currentFilteredItem);let listOfModifiedDragDrop=[];this.getRoot().find(".group"+group+".choice"+choice).each((function(i,node){if($(node).get(0)===currentFilteredItem.get(0))return;const originalClass=$(node).attr("class"),originalStyle=$(node).attr("style"),filteredDragDropClone=currentFilteredItem.clone();filteredDragDropClone.attr("class",originalClass),filteredDragDropClone.attr("style",originalStyle),$(node).before(filteredDragDropClone),listOfModifiedDragDrop.push(node)})),listOfModifiedDragDrop.forEach((function(node){$(node).remove()}));const currentHeight=currentFilteredItem.height(),currentWidth=currentFilteredItem.width();currentFilteredItem.height("auto"),currentFilteredItem.width("auto"),filteredElement.offsetWidth&&filteredElement.offsetHeight||filteredElement.classList.add("d-block"),thisQ.questionDragDropWidthHeight[group].maxWidth{result[inputNode.id]=inputNode.value})),result},DragDropToTextQuestion.prototype.isQuestionInteracted=function(){const oldAnswer=this.questionAnswer,newAnswer=this.getQuestionAnsweredValues();let isInteracted=!1;return JSON.stringify(newAnswer)!==JSON.stringify(oldAnswer)?(isInteracted=!0,isInteracted):(Object.keys(newAnswer).forEach((key=>{newAnswer[key]!==oldAnswer[key]&&(isInteracted=!0)})),isInteracted)},DragDropToTextQuestion.prototype.handleDragStart=function(e){var thisQ=this,drag=$(e.target).closest(".draghome");if(dragDrop.prepare(e).start&&!drag.hasClass("beingdragged")){drag.addClass("beingdragged");var currentPlace=this.getClassnameNumericSuffix(drag,"inplace");if(null!==currentPlace){this.setInputValue(currentPlace,0),drag.removeClass("inplace"+currentPlace);var hiddenDrop=thisQ.getDrop(drag,currentPlace);hiddenDrop.length&&(hiddenDrop.addClass("active"),drag.offset(hiddenDrop.offset()))}else{var hiddenDrag=thisQ.getDragClone(drag);if(hiddenDrag.length)if(drag.hasClass("infinite")){var noOfDrags=this.noOfDropsInGroup(this.getGroup(drag));if(this.getInfiniteDragClones(drag,!1).length1;)choice--,previous=this.getUnplacedChoice(group,choice);return previous},DragDropToTextQuestion.prototype.animateTo=function(drag,target){var currentPos=drag.offset(),targetPos=target.offset(),thisQ=this;M.util.js_pending("qtype_ddwtos-animate-"+thisQ.containerId),drag.animate({left:parseInt(drag.css("left"))+targetPos.left-currentPos.left,top:parseInt(drag.css("top"))+targetPos.top-currentPos.top},{duration:"fast",done:function(){$("body").trigger("qtype_ddwtos-dragmoved",[drag,target,thisQ]),M.util.js_complete("qtype_ddwtos-animate-"+thisQ.containerId)}})},DragDropToTextQuestion.prototype.isPointInDrop=function(pageX,pageY,drop){var position=drop.offset();return pageX>=position.left&&pageX=position.top&&pageY1&&thisQ.getInfiniteDragClones(drag,!0).first().remove()),void 0!==drag.data("isfocus")&&!0===drag.data("isfocus")&&(drag.focus(),drag.removeData("isfocus")),void 0!==target.data("isfocus")&&!0===target.data("isfocus")&&target.removeData("isfocus"),questionManager.isKeyboardNavigation&&(questionManager.isKeyboardNavigation=!1),thisQ.isQuestionInteracted()&&(questionManager.handleFormDirty(),thisQ.questionAnswer=thisQ.getQuestionAnsweredValues())},handleFormDirty:function(){const responseForm=document.getElementById("responseform");FormChangeChecker.markFormAsDirty(responseForm)}};return{init:questionManager.init}})); //# sourceMappingURL=ddwtos.min.js.map \ No newline at end of file diff --git a/question/type/ddwtos/amd/build/ddwtos.min.js.map b/question/type/ddwtos/amd/build/ddwtos.min.js.map index 6c77f41e6c998..2918776c33eca 100644 --- a/question/type/ddwtos/amd/build/ddwtos.min.js.map +++ b/question/type/ddwtos/amd/build/ddwtos.min.js.map @@ -1 +1 @@ -{"version":3,"file":"ddwtos.min.js","sources":["../src/ddwtos.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 make drag-drop into text questions work.\n *\n * Some vocabulary to help understand this code:\n *\n * The question text contains 'drops' - blanks into which the 'drags', the missing\n * words, can be put.\n *\n * The thing that can be moved into the drops are called 'drags'. There may be\n * multiple copies of the 'same' drag which does not really cause problems.\n * Each drag has a 'choice' number which is the value set on the drop's hidden\n * input when this drag is placed in a drop.\n *\n * These may be in separate 'groups', distinguished by colour.\n * Things can only interact with other things in the same group.\n * The groups are numbered from 1.\n *\n * The place where a given drag started from is called its 'home'.\n *\n * @module qtype_ddwtos/ddwtos\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.6\n */\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'core/key_codes',\n 'core_form/changechecker'\n], function(\n $,\n dragDrop,\n keys,\n FormChangeChecker\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop into text 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 * @constructor\n */\n function DragDropToTextQuestion(containerId, readOnly) {\n this.containerId = containerId;\n this.questionAnswer = {};\n if (readOnly) {\n this.getRoot().addClass('qtype_ddwtos-readonly');\n }\n this.resizeAllDragsAndDrops();\n this.cloneDrags();\n this.positionDrags();\n }\n\n /**\n * In each group, resize all the items to be the same size.\n */\n DragDropToTextQuestion.prototype.resizeAllDragsAndDrops = function() {\n var thisQ = this;\n this.getRoot().find('.answercontainer > div').each(function(i, node) {\n thisQ.resizeAllDragsAndDropsInGroup(\n thisQ.getClassnameNumericSuffix($(node), 'draggrouphomes'));\n });\n };\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n DragDropToTextQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {\n var thisQ = this,\n dragHomes = this.getRoot().find('.draggrouphomes' + group + ' span.draghome'),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n dragHomes.each(function(i, drag) {\n maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(0 + drag.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 8;\n maxHeight += 2;\n\n // Set each drag home to that size.\n dragHomes.each(function(i, drag) {\n thisQ.setElementSize(drag, maxWidth, maxHeight);\n });\n\n // Set each drop to that size.\n this.getRoot().find('span.drop.group' + group).each(function(i, drop) {\n thisQ.setElementSize(drop, maxWidth, maxHeight);\n });\n };\n\n /**\n * Set a given DOM element to be a particular size.\n *\n * @param {HTMLElement} element\n * @param {int} width\n * @param {int} height\n */\n DragDropToTextQuestion.prototype.setElementSize = function(element, width, height) {\n $(element).width(width).height(height).css('lineHeight', height + 'px');\n };\n\n /**\n * Invisible 'drag homes' are output by the renderer. These have the same properties\n * as the drag items but are invisible. We clone these invisible elements to make the\n * actual drag items.\n */\n DragDropToTextQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n thisQ.getRoot().find('span.draghome').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('draghome choice' +\n thisQ.getChoice(drag) + ' group' +\n thisQ.getGroup(drag) + ' dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Update the position of drags.\n */\n DragDropToTextQuestion.prototype.positionDrags = function() {\n var thisQ = this,\n root = this.getRoot();\n\n // First move all items back home.\n root.find('span.draghome').not('.dragplaceholder').each(function(i, dragNode) {\n var drag = $(dragNode),\n currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');\n drag.addClass('unplaced')\n .removeClass('placed');\n drag.removeAttr('tabindex');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n });\n\n // Then place the once that should be placed.\n root.find('input.placeinput').each(function(i, inputNode) {\n var input = $(inputNode),\n choice = input.val(),\n place = thisQ.getPlace(input);\n\n // Record the last known position of the drop.\n var drop = root.find('.drop.place' + place),\n dropPosition = drop.offset();\n drop.data('prev-top', dropPosition.top).data('prev-left', dropPosition.left);\n\n if (choice === '0') {\n // No item in this place.\n return;\n }\n\n // Get the unplaced drag.\n var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);\n // Get the clone of the drag.\n var hiddenDrag = thisQ.getDragClone(unplacedDrag);\n if (hiddenDrag.length) {\n if (unplacedDrag.hasClass('infinite')) {\n var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));\n var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = unplacedDrag.clone();\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n } else {\n hiddenDrag.addClass('active');\n }\n } else {\n hiddenDrag.addClass('active');\n }\n }\n // Send the drag to drop.\n thisQ.sendDragToDrop(thisQ.getUnplacedChoice(thisQ.getGroup(input), choice), drop);\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropToTextQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.placeinput').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropToTextQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Handles the start of dragging an item.\n *\n * @param {Event} e the touch start or mouse down event.\n */\n DragDropToTextQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n drag = $(e.target).closest('.draghome');\n\n var info = dragDrop.prepare(e);\n if (!info.start || drag.hasClass('beingdragged')) {\n return;\n }\n\n drag.addClass('beingdragged');\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n this.setInputValue(currentPlace, 0);\n drag.removeClass('inplace' + currentPlace);\n var hiddenDrop = thisQ.getDrop(drag, currentPlace);\n if (hiddenDrop.length) {\n hiddenDrop.addClass('active');\n drag.offset(hiddenDrop.offset());\n }\n } else {\n var hiddenDrag = thisQ.getDragClone(drag);\n if (hiddenDrag.length) {\n if (drag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(drag));\n var cloneDrags = this.getInfiniteDragClones(drag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = drag.clone();\n cloneDrag.removeClass('beingdragged');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n drag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n }\n }\n\n dragDrop.start(e, drag, function(x, y, drag) {\n thisQ.dragMove(x, y, drag);\n }, function(x, y, drag) {\n thisQ.dragEnd(x, y, drag);\n });\n };\n\n /**\n * Called whenever the currently dragged items moves.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropToTextQuestion.prototype.dragMove = function(pageX, pageY, drag) {\n var thisQ = this;\n this.getRoot().find('span.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop)) {\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n };\n\n /**\n * Called when user drops a drag item.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropToTextQuestion.prototype.dragEnd = function(pageX, pageY, drag) {\n var thisQ = this,\n root = this.getRoot(),\n placed = false;\n root.find('span.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n if (placed) {\n return false;\n }\n const dropZone = $(dropNode);\n if (!thisQ.isPointInDrop(pageX, pageY, dropZone)) {\n // Not this drop zone.\n return true;\n }\n let drop = null;\n if (dropZone.hasClass('placed')) {\n // This is an placed drag item in a drop.\n dropZone.removeClass('valid-drag-over-drop');\n // Get the correct drop.\n drop = thisQ.getDrop(drag, thisQ.getClassnameNumericSuffix(dropZone, 'inplace'));\n } else {\n // Empty drop.\n drop = dropZone;\n }\n // Now put this drag into the drop.\n drop.removeClass('valid-drag-over-drop');\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n if (!placed) {\n this.sendDragHome(drag);\n }\n };\n\n /**\n * Animate a drag item into a given place (or back home).\n *\n * @param {jQuery|null} drag the item to place. If null, clear the place.\n * @param {jQuery} drop the place to put it.\n */\n DragDropToTextQuestion.prototype.sendDragToDrop = function(drag, drop) {\n // Send drag home if there is no place in drop.\n if (this.getPlace(drop) === null) {\n this.sendDragHome(drag);\n return;\n }\n\n // Is there already a drag in this drop? if so, evict it.\n var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));\n if (oldDrag.length !== 0) {\n var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');\n // When infinite group and there is already a drag in a drop, reject the exact clone in the same drop.\n if (this.hasDropSameDrag(currentPlace, drop, oldDrag, drag)) {\n this.sendDragHome(drag);\n return;\n }\n var hiddenDrop = this.getDrop(oldDrag, currentPlace);\n hiddenDrop.addClass('active');\n oldDrag.addClass('beingdragged');\n oldDrag.offset(hiddenDrop.offset());\n this.sendDragHome(oldDrag);\n }\n\n if (drag.length === 0) {\n this.setInputValue(this.getPlace(drop), 0);\n if (drop.data('isfocus')) {\n drop.focus();\n }\n } else {\n // Prevent the drag item drop into two drop-zone.\n if (this.getClassnameNumericSuffix(drag, 'inplace')) {\n return;\n }\n\n this.setInputValue(this.getPlace(drop), this.getChoice(drag));\n drag.removeClass('unplaced')\n .addClass('placed inplace' + this.getPlace(drop));\n drag.attr('tabindex', 0);\n this.animateTo(drag, drop);\n }\n };\n\n /**\n * When infinite group and there is already a drag in a drop, reject the exact clone in the same drop.\n *\n * @param {int} currentPlace the position of the current drop.\n * @param {jQuery} drop the drop containing a drag.\n * @param {jQuery} oldDrag the drag already placed in drop.\n * @param {jQuery} drag the new drag which is exactly the same (clone) as oldDrag .\n * @returns {boolean}\n */\n DragDropToTextQuestion.prototype.hasDropSameDrag = function(currentPlace, drop, oldDrag, drag) {\n if (drag.hasClass('infinite')) {\n return drop.hasClass('place' + currentPlace) &&\n this.getGroup(drag) === this.getGroup(drop) &&\n this.getChoice(drag) === this.getChoice(oldDrag) &&\n this.getGroup(drag) === this.getGroup(oldDrag);\n }\n return false;\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropToTextQuestion.prototype.sendDragHome = function(drag) {\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n drag.data('unplaced', true);\n\n this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));\n };\n\n /**\n * Handles keyboard events on drops.\n *\n * Drops are focusable. Once focused, right/down/space switches to the next choice, and\n * left/up switches to the previous. Escape clear.\n *\n * @param {KeyboardEvent} e\n */\n DragDropToTextQuestion.prototype.handleKeyPress = function(e) {\n var drop = $(e.target).closest('.drop');\n if (drop.length === 0) {\n var placedDrag = $(e.target);\n var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');\n if (currentPlace !== null) {\n drop = this.getDrop(placedDrag, currentPlace);\n }\n }\n var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),\n nextDrag = $();\n\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.escape:\n break;\n\n default:\n questionManager.isKeyboardNavigation = false;\n return; // To avoid the preventDefault below.\n }\n\n if (nextDrag.length) {\n nextDrag.data('isfocus', true);\n nextDrag.addClass('beingdragged');\n var hiddenDrag = this.getDragClone(nextDrag);\n if (hiddenDrag.length) {\n if (nextDrag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));\n var cloneDrags = this.getInfiniteDragClones(nextDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = nextDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n nextDrag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n }\n } else {\n drop.data('isfocus', true);\n }\n\n e.preventDefault();\n this.sendDragToDrop(nextDrag, drop);\n };\n\n /**\n * Choose the next drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropToTextQuestion.prototype.getNextDrag = function(group, drag) {\n var choice,\n numChoices = this.noOfChoicesInGroup(group);\n\n if (drag.length === 0) {\n choice = 1; // Was empty, so we want to select the first choice.\n } else {\n choice = this.getChoice(drag) + 1;\n }\n\n var next = this.getUnplacedChoice(group, choice);\n while (next.length === 0 && choice < numChoices) {\n choice++;\n next = this.getUnplacedChoice(group, choice);\n }\n\n return next;\n };\n\n /**\n * Choose the previous drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropToTextQuestion.prototype.getPreviousDrag = function(group, drag) {\n var choice;\n\n if (drag.length === 0) {\n choice = this.noOfChoicesInGroup(group);\n } else {\n choice = this.getChoice(drag) - 1;\n }\n\n var previous = this.getUnplacedChoice(group, choice);\n while (previous.length === 0 && choice > 1) {\n choice--;\n previous = this.getUnplacedChoice(group, choice);\n }\n\n // Does this choice exist?\n return previous;\n };\n\n /**\n * Animate an object to the given destination.\n *\n * @param {jQuery} drag the element to be animated.\n * @param {jQuery} target element marking the place to move it to.\n */\n DragDropToTextQuestion.prototype.animateTo = function(drag, target) {\n var currentPos = drag.offset(),\n targetPos = target.offset(),\n thisQ = this;\n\n M.util.js_pending('qtype_ddwtos-animate-' + thisQ.containerId);\n // Animate works in terms of CSS position, whereas locating an object\n // on the page works best with jQuery offset() function. So, to get\n // the right target position, we work out the required change in\n // offset() and then add that to the current CSS position.\n drag.animate(\n {\n left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,\n top: parseInt(drag.css('top')) + targetPos.top - currentPos.top\n },\n {\n duration: 'fast',\n done: function() {\n $('body').trigger('qtype_ddwtos-dragmoved', [drag, target, thisQ]);\n M.util.js_complete('qtype_ddwtos-animate-' + thisQ.containerId);\n }\n }\n );\n };\n\n /**\n * Detect if a point is inside a given DOM node.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drop the node to check (typically a drop).\n * @return {boolean} whether the point is inside the node.\n */\n DragDropToTextQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {\n var position = drop.offset();\n return pageX >= position.left && pageX < position.left + drop.width()\n && pageY >= position.top && pageY < position.top + drop.height();\n };\n\n /**\n * Set the value of the hidden input for a place, to record what is currently there.\n *\n * @param {int} place which place to set the input value for.\n * @param {int} choice the value to set.\n */\n DragDropToTextQuestion.prototype.setInputValue = function(place, choice) {\n this.getRoot().find('input.placeinput.place' + place).val(choice);\n };\n\n /**\n * Get the outer div for this question.\n *\n * @returns {jQuery} containing that div.\n */\n DragDropToTextQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get drag home for a given choice.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} containing that div.\n */\n DragDropToTextQuestion.prototype.getDragHome = function(group, choice) {\n if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {\n return this.getRoot().find('.draggrouphomes' + group +\n ' span.draghome.infinite' +\n '.choice' + choice +\n '.group' + group);\n }\n return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);\n };\n\n /**\n * Get an unplaced choice for a particular group.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.\n */\n DragDropToTextQuestion.prototype.getUnplacedChoice = function(group, choice) {\n return this.getRoot().find('.draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);\n };\n\n /**\n * Get the drag that is currently in a given place.\n *\n * @param {int} place the place number.\n * @return {jQuery} the current drag (or an empty jQuery if none).\n */\n DragDropToTextQuestion.prototype.getCurrentDragInPlace = function(place) {\n return this.getRoot().find('span.draghome.inplace' + place);\n };\n\n /**\n * Return the number of blanks in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of drops.\n */\n DragDropToTextQuestion.prototype.noOfDropsInGroup = function(group) {\n return this.getRoot().find('.drop.group' + group).length;\n };\n\n /**\n * Return the number of choices in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of choices.\n */\n DragDropToTextQuestion.prototype.noOfChoicesInGroup = function(group) {\n return this.getRoot().find('.draghome.group' + group).length;\n };\n\n /**\n * Return the number at the end of the CSS class name with the given prefix.\n *\n * @param {jQuery} node\n * @param {String} prefix name prefix\n * @returns {Number|null} the suffix if found, else null.\n */\n DragDropToTextQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = node.attr('class');\n if (classes !== undefined && classes !== '') {\n var classesArr = classes.split(' ');\n for (var index = 0; index < classesArr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesArr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesArr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Get the choice number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the choice number.\n */\n DragDropToTextQuestion.prototype.getChoice = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'choice');\n };\n\n /**\n * Given a DOM node that is significant to this question\n * (drag, drop, ...) get the group it belongs to.\n *\n * @param {jQuery} node a DOM node.\n * @returns {Number} the group it belongs to.\n */\n DragDropToTextQuestion.prototype.getGroup = function(node) {\n return this.getClassnameNumericSuffix(node, 'group');\n };\n\n /**\n * Get the place number of a drop, or its corresponding hidden input.\n *\n * @param {jQuery} node the DOM node.\n * @returns {Number} the place number.\n */\n DragDropToTextQuestion.prototype.getPlace = function(node) {\n return this.getClassnameNumericSuffix(node, 'place');\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropToTextQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draggrouphomes' +\n this.getGroup(drag) +\n ' span.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.dragplaceholder');\n };\n\n /**\n * Get infinite drag clones for given drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} inHome in the home area or not.\n * @returns {jQuery} the drag's clones.\n */\n DragDropToTextQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {\n if (inHome) {\n return this.getRoot().find('.draggrouphomes' +\n this.getGroup(drag) +\n ' span.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n }\n return this.getRoot().find('span.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n };\n\n /**\n * Get drop for a given drag and place.\n *\n * @param {jQuery} drag the drag.\n * @param {Integer} currentPlace the current place of drag.\n * @returns {jQuery} the drop's clone.\n */\n DragDropToTextQuestion.prototype.getDrop = function(drag, currentPlace) {\n return this.getRoot().find('.drop.group' + this.getGroup(drag) + '.place' + currentPlace);\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\n * @type {Object}\n */\n var questionManager = {\n /**\n * {boolean} used to ensure the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} ensures that the drag event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n dragEventHandlersInitialised: {},\n\n /**\n * {boolean} is keyboard navigation or not.\n */\n isKeyboardNavigation: false,\n\n /**\n * {DragDropToTextQuestion[]} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {},\n\n /**\n * Initialise questions.\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 */\n init: function(containerId, readOnly) {\n questionManager.questions[containerId] = new DragDropToTextQuestion(containerId, readOnly);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.dragEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('ddwtos') &&\n !questionContainer.classList.contains('qtype_ddwtos-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToDrag($(questionContainer).find('span.draghome'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $('body')\n .on('keydown',\n '.que.ddwtos:not(.qtype_ddwtos-readonly) span.drop',\n questionManager.handleKeyPress)\n .on('keydown',\n '.que.ddwtos:not(.qtype_ddwtos-readonly) span.draghome.placed:not(.beingdragged)',\n questionManager.handleKeyPress)\n .on('qtype_ddwtos-dragmoved', questionManager.handleDragMoved);\n },\n\n /**\n * Binding the drag/touch event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToDrag: function(element) {\n // Unbind all the mousedown and touchstart events to prevent double binding.\n element.unbind('mousedown touchstart');\n element.on('mousedown touchstart', questionManager.handleDragStart);\n },\n\n /**\n * Handle mouse down / touch start on drags.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press on drops.\n * @param {KeyboardEvent} e\n */\n handleKeyPress: function(e) {\n if (questionManager.isKeyboardNavigation) {\n return;\n }\n questionManager.isKeyboardNavigation = true;\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Given an event, work out which question it affects.\n *\n * @param {Event} e the event.\n * @returns {DragDropToTextQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddwtos').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when drag moved.\n *\n * @param {Event} e the event.\n * @param {jQuery} drag the drag\n * @param {jQuery} target the target\n * @param {DragDropToTextQuestion} thisQ the question.\n */\n handleDragMoved: function(e, drag, target, thisQ) {\n drag.removeClass('beingdragged');\n drag.css('top', '').css('left', '');\n target.after(drag);\n target.removeClass('active');\n if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {\n drag.removeClass('placed').addClass('unplaced');\n drag.removeAttr('tabindex');\n drag.removeData('unplaced');\n if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {\n thisQ.getInfiniteDragClones(drag, true).first().remove();\n }\n }\n if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {\n drag.focus();\n drag.removeData('isfocus');\n }\n if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {\n target.removeData('isfocus');\n }\n if (questionManager.isKeyboardNavigation) {\n questionManager.isKeyboardNavigation = false;\n }\n if (thisQ.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n }\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddwtos/ddwtos\n */\n return {\n /**\n * Initialise one drag-drop into text 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 */\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","keys","FormChangeChecker","DragDropToTextQuestion","containerId","readOnly","questionAnswer","getRoot","addClass","resizeAllDragsAndDrops","cloneDrags","positionDrags","prototype","thisQ","this","find","each","i","node","resizeAllDragsAndDropsInGroup","getClassnameNumericSuffix","group","dragHomes","maxWidth","maxHeight","drag","Math","max","ceil","offsetWidth","offsetHeight","setElementSize","drop","element","width","height","css","index","draghome","placeHolder","clone","removeClass","getChoice","getGroup","before","root","not","dragNode","currentPlace","removeAttr","inputNode","input","choice","val","place","getPlace","dropPosition","offset","data","top","left","unplacedDrag","getUnplacedChoice","hiddenDrag","getDragClone","length","hasClass","noOfDrags","noOfDropsInGroup","getInfiniteDragClones","cloneDrag","after","questionManager","addEventHandlersToDrag","sendDragToDrop","getQuestionAnsweredValues","result","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","forEach","key","handleDragStart","e","target","closest","prepare","start","setInputValue","hiddenDrop","getDrop","x","y","dragMove","dragEnd","pageX","pageY","dropNode","isPointInDrop","placed","dropZone","sendDragHome","oldDrag","getCurrentDragInPlace","hasDropSameDrag","focus","attr","animateTo","getDragHome","handleKeyPress","placedDrag","currentDrag","nextDrag","keyCode","space","arrowRight","arrowDown","getNextDrag","arrowLeft","arrowUp","getPreviousDrag","escape","isKeyboardNavigation","preventDefault","numChoices","noOfChoicesInGroup","next","previous","currentPos","targetPos","M","util","js_pending","animate","parseInt","duration","done","trigger","js_complete","position","document","getElementById","is","slice","prefix","classes","undefined","classesArr","split","RegExp","test","match","exec","Number","inHome","eventHandlersInitialised","dragEventHandlersInitialised","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","classList","contains","on","handleDragMoved","unbind","question","getQuestionForEvent","currentTarget","removeData","first","remove","handleFormDirty","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ddwtos.min.js","sources":["../src/ddwtos.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 make drag-drop into text questions work.\n *\n * Some vocabulary to help understand this code:\n *\n * The question text contains 'drops' - blanks into which the 'drags', the missing\n * words, can be put.\n *\n * The thing that can be moved into the drops are called 'drags'. There may be\n * multiple copies of the 'same' drag which does not really cause problems.\n * Each drag has a 'choice' number which is the value set on the drop's hidden\n * input when this drag is placed in a drop.\n *\n * These may be in separate 'groups', distinguished by colour.\n * Things can only interact with other things in the same group.\n * The groups are numbered from 1.\n *\n * The place where a given drag started from is called its 'home'.\n *\n * @module qtype_ddwtos/ddwtos\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.6\n */\ndefine([\n 'jquery',\n 'core/dragdrop',\n 'core/key_codes',\n 'core_form/changechecker',\n 'core_filters/events',\n], function(\n $,\n dragDrop,\n keys,\n FormChangeChecker,\n filterEvent\n) {\n\n \"use strict\";\n\n /**\n * Object to handle one drag-drop into text 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 * @constructor\n */\n function DragDropToTextQuestion(containerId, readOnly) {\n const thisQ = this;\n this.containerId = containerId;\n this.questionAnswer = {};\n this.questionDragDropWidthHeight = [];\n if (readOnly) {\n this.getRoot().addClass('qtype_ddwtos-readonly');\n }\n this.resizeAllDragsAndDrops();\n this.cloneDrags();\n this.positionDrags();\n // Wait for all dynamic content loaded by filter to be completed.\n document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => {\n elements.detail.nodes.forEach((element) => {\n thisQ.changeAllDragsAndDropsToFilteredContent(element);\n });\n });\n }\n\n /**\n * In each group, resize all the items to be the same size.\n */\n DragDropToTextQuestion.prototype.resizeAllDragsAndDrops = function() {\n var thisQ = this;\n this.getRoot().find('.answercontainer > div').each(function(i, node) {\n thisQ.resizeAllDragsAndDropsInGroup(\n thisQ.getClassnameNumericSuffix($(node), 'draggrouphomes'));\n });\n };\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n DragDropToTextQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {\n var thisQ = this,\n dragDropItems = this.getRoot().find('span.group' + group),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n dragDropItems.each(function(i, drag) {\n maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(0 + drag.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 8;\n maxHeight += 2;\n thisQ.questionDragDropWidthHeight[group] = {maxWidth: maxWidth, maxHeight: maxHeight};\n // Set each drag home to that size.\n dragDropItems.each(function(i, drag) {\n thisQ.setElementSize(drag, maxWidth, maxHeight);\n });\n };\n\n /**\n * Change all the drags and drops related to the item that has been changed by filter to correct size and content.\n *\n * @param {object} filteredElement the element has been modified by filter.\n */\n DragDropToTextQuestion.prototype.changeAllDragsAndDropsToFilteredContent = function(filteredElement) {\n let currentFilteredItem = $(filteredElement);\n const parentIsDD = currentFilteredItem.parent().closest('span').hasClass('placed') ||\n currentFilteredItem.parent().closest('span').hasClass('draghome');\n const isDD = currentFilteredItem.hasClass('placed') || currentFilteredItem.hasClass('draghome');\n // The filtered element or parent element should a drag or drop item.\n if (!parentIsDD && !isDD) {\n return;\n }\n if (parentIsDD) {\n currentFilteredItem = currentFilteredItem.parent().closest('span');\n }\n const thisQ = this;\n if (thisQ.getRoot().find(currentFilteredItem).length <= 0) {\n // If the DD item doesn't belong to this question\n // In case we have multiple questions in the same page.\n return;\n }\n const group = thisQ.getGroup(currentFilteredItem),\n choice = thisQ.getChoice(currentFilteredItem);\n let listOfModifiedDragDrop = [];\n // Get the list of drag and drop item within the same group and choice.\n this.getRoot().find('.group' + group + '.choice' + choice).each(function(i, node) {\n // Same modified item, skip it.\n if ($(node).get(0) === currentFilteredItem.get(0)) {\n return;\n }\n const originalClass = $(node).attr('class');\n const originalStyle = $(node).attr('style');\n // We want to keep all the handler and event for filtered item, so using clone is the only choice.\n const filteredDragDropClone = currentFilteredItem.clone();\n // Replace the class and style of the drag drop item we want to replace for the clone.\n filteredDragDropClone.attr('class', originalClass);\n filteredDragDropClone.attr('style', originalStyle);\n // Insert into DOM.\n $(node).before(filteredDragDropClone);\n // Add the item has been replaced to a list so we can remove it later.\n listOfModifiedDragDrop.push(node);\n });\n\n listOfModifiedDragDrop.forEach(function(node) {\n $(node).remove();\n });\n // Save the current height and width.\n const currentHeight = currentFilteredItem.height();\n const currentWidth = currentFilteredItem.width();\n // Set to auto so we can get the real height and width of the filtered item.\n currentFilteredItem.height('auto');\n currentFilteredItem.width('auto');\n // We need to set display block so we can get height and width.\n // Some browser can't get the offsetWidth/Height if they are an inline element like span tag.\n if (!filteredElement.offsetWidth || !filteredElement.offsetHeight) {\n filteredElement.classList.add('d-block');\n }\n if (thisQ.questionDragDropWidthHeight[group].maxWidth < Math.ceil(filteredElement.offsetWidth) ||\n thisQ.questionDragDropWidthHeight[group].maxHeight < Math.ceil(0 + filteredElement.offsetHeight)) {\n // Remove the d-block class before calculation.\n filteredElement.classList.remove('d-block');\n // Now resize all the items in the same group if we have new maximum width or height.\n thisQ.resizeAllDragsAndDropsInGroup(group);\n } else {\n // Return the original height and width in case the real height and width is not the maximum.\n currentFilteredItem.height(currentHeight);\n currentFilteredItem.width(currentWidth);\n }\n // Remove the d-block class after resize.\n filteredElement.classList.remove('d-block');\n };\n\n /**\n * Set a given DOM element to be a particular size.\n *\n * @param {HTMLElement} element\n * @param {int} width\n * @param {int} height\n */\n DragDropToTextQuestion.prototype.setElementSize = function(element, width, height) {\n $(element).width(width).height(height).css('lineHeight', height + 'px');\n };\n\n /**\n * Invisible 'drag homes' are output by the renderer. These have the same properties\n * as the drag items but are invisible. We clone these invisible elements to make the\n * actual drag items.\n */\n DragDropToTextQuestion.prototype.cloneDrags = function() {\n var thisQ = this;\n thisQ.getRoot().find('span.draghome').each(function(index, draghome) {\n var drag = $(draghome);\n var placeHolder = drag.clone();\n placeHolder.removeClass();\n placeHolder.addClass('draghome choice' +\n thisQ.getChoice(drag) + ' group' +\n thisQ.getGroup(drag) + ' dragplaceholder');\n drag.before(placeHolder);\n });\n };\n\n /**\n * Update the position of drags.\n */\n DragDropToTextQuestion.prototype.positionDrags = function() {\n var thisQ = this,\n root = this.getRoot();\n\n // First move all items back home.\n root.find('span.draghome').not('.dragplaceholder').each(function(i, dragNode) {\n var drag = $(dragNode),\n currentPlace = thisQ.getClassnameNumericSuffix(drag, 'inplace');\n drag.addClass('unplaced')\n .removeClass('placed');\n drag.removeAttr('tabindex');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n });\n\n // Then place the once that should be placed.\n root.find('input.placeinput').each(function(i, inputNode) {\n var input = $(inputNode),\n choice = input.val(),\n place = thisQ.getPlace(input);\n\n // Record the last known position of the drop.\n var drop = root.find('.drop.place' + place),\n dropPosition = drop.offset();\n drop.data('prev-top', dropPosition.top).data('prev-left', dropPosition.left);\n\n if (choice === '0') {\n // No item in this place.\n return;\n }\n\n // Get the unplaced drag.\n var unplacedDrag = thisQ.getUnplacedChoice(thisQ.getGroup(input), choice);\n // Get the clone of the drag.\n var hiddenDrag = thisQ.getDragClone(unplacedDrag);\n if (hiddenDrag.length) {\n if (unplacedDrag.hasClass('infinite')) {\n var noOfDrags = thisQ.noOfDropsInGroup(thisQ.getGroup(unplacedDrag));\n var cloneDrags = thisQ.getInfiniteDragClones(unplacedDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = unplacedDrag.clone();\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n } else {\n hiddenDrag.addClass('active');\n }\n } else {\n hiddenDrag.addClass('active');\n }\n }\n // Send the drag to drop.\n thisQ.sendDragToDrop(thisQ.getUnplacedChoice(thisQ.getGroup(input), choice), drop);\n });\n\n // Save the question answer.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n };\n\n /**\n * Get the question answered values.\n *\n * @return {Object} Contain key-value with key is the input id and value is the input value.\n */\n DragDropToTextQuestion.prototype.getQuestionAnsweredValues = function() {\n let result = {};\n this.getRoot().find('input.placeinput').each((i, inputNode) => {\n result[inputNode.id] = inputNode.value;\n });\n\n return result;\n };\n\n /**\n * Check if the question is being interacted or not.\n *\n * @return {boolean} Return true if the user has changed the question-answer.\n */\n DragDropToTextQuestion.prototype.isQuestionInteracted = function() {\n const oldAnswer = this.questionAnswer;\n const newAnswer = this.getQuestionAnsweredValues();\n let isInteracted = false;\n\n // First, check both answers have the same structure or not.\n if (JSON.stringify(newAnswer) !== JSON.stringify(oldAnswer)) {\n isInteracted = true;\n return isInteracted;\n }\n // Check the values.\n Object.keys(newAnswer).forEach(key => {\n if (newAnswer[key] !== oldAnswer[key]) {\n isInteracted = true;\n }\n });\n\n return isInteracted;\n };\n\n /**\n * Handles the start of dragging an item.\n *\n * @param {Event} e the touch start or mouse down event.\n */\n DragDropToTextQuestion.prototype.handleDragStart = function(e) {\n var thisQ = this,\n drag = $(e.target).closest('.draghome');\n\n var info = dragDrop.prepare(e);\n if (!info.start || drag.hasClass('beingdragged')) {\n return;\n }\n\n drag.addClass('beingdragged');\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n this.setInputValue(currentPlace, 0);\n drag.removeClass('inplace' + currentPlace);\n var hiddenDrop = thisQ.getDrop(drag, currentPlace);\n if (hiddenDrop.length) {\n hiddenDrop.addClass('active');\n drag.offset(hiddenDrop.offset());\n }\n } else {\n var hiddenDrag = thisQ.getDragClone(drag);\n if (hiddenDrag.length) {\n if (drag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(drag));\n var cloneDrags = this.getInfiniteDragClones(drag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = drag.clone();\n cloneDrag.removeClass('beingdragged');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n drag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n drag.offset(hiddenDrag.offset());\n }\n }\n }\n\n dragDrop.start(e, drag, function(x, y, drag) {\n thisQ.dragMove(x, y, drag);\n }, function(x, y, drag) {\n thisQ.dragEnd(x, y, drag);\n });\n };\n\n /**\n * Called whenever the currently dragged items moves.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropToTextQuestion.prototype.dragMove = function(pageX, pageY, drag) {\n var thisQ = this;\n this.getRoot().find('span.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n var drop = $(dropNode);\n if (thisQ.isPointInDrop(pageX, pageY, drop)) {\n drop.addClass('valid-drag-over-drop');\n } else {\n drop.removeClass('valid-drag-over-drop');\n }\n });\n };\n\n /**\n * Called when user drops a drag item.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drag the item being moved.\n */\n DragDropToTextQuestion.prototype.dragEnd = function(pageX, pageY, drag) {\n var thisQ = this,\n root = this.getRoot(),\n placed = false;\n root.find('span.group' + this.getGroup(drag)).not('.beingdragged').each(function(i, dropNode) {\n if (placed) {\n return false;\n }\n const dropZone = $(dropNode);\n if (!thisQ.isPointInDrop(pageX, pageY, dropZone)) {\n // Not this drop zone.\n return true;\n }\n let drop = null;\n if (dropZone.hasClass('placed')) {\n // This is an placed drag item in a drop.\n dropZone.removeClass('valid-drag-over-drop');\n // Get the correct drop.\n drop = thisQ.getDrop(drag, thisQ.getClassnameNumericSuffix(dropZone, 'inplace'));\n } else {\n // Empty drop.\n drop = dropZone;\n }\n // Now put this drag into the drop.\n drop.removeClass('valid-drag-over-drop');\n thisQ.sendDragToDrop(drag, drop);\n placed = true;\n return false; // Stop the each() here.\n });\n if (!placed) {\n this.sendDragHome(drag);\n }\n };\n\n /**\n * Animate a drag item into a given place (or back home).\n *\n * @param {jQuery|null} drag the item to place. If null, clear the place.\n * @param {jQuery} drop the place to put it.\n */\n DragDropToTextQuestion.prototype.sendDragToDrop = function(drag, drop) {\n // Send drag home if there is no place in drop.\n if (this.getPlace(drop) === null) {\n this.sendDragHome(drag);\n return;\n }\n\n // Is there already a drag in this drop? if so, evict it.\n var oldDrag = this.getCurrentDragInPlace(this.getPlace(drop));\n if (oldDrag.length !== 0) {\n var currentPlace = this.getClassnameNumericSuffix(oldDrag, 'inplace');\n // When infinite group and there is already a drag in a drop, reject the exact clone in the same drop.\n if (this.hasDropSameDrag(currentPlace, drop, oldDrag, drag)) {\n this.sendDragHome(drag);\n return;\n }\n var hiddenDrop = this.getDrop(oldDrag, currentPlace);\n hiddenDrop.addClass('active');\n oldDrag.addClass('beingdragged');\n oldDrag.offset(hiddenDrop.offset());\n this.sendDragHome(oldDrag);\n }\n\n if (drag.length === 0) {\n this.setInputValue(this.getPlace(drop), 0);\n if (drop.data('isfocus')) {\n drop.focus();\n }\n } else {\n // Prevent the drag item drop into two drop-zone.\n if (this.getClassnameNumericSuffix(drag, 'inplace')) {\n return;\n }\n\n this.setInputValue(this.getPlace(drop), this.getChoice(drag));\n drag.removeClass('unplaced')\n .addClass('placed inplace' + this.getPlace(drop));\n drag.attr('tabindex', 0);\n this.animateTo(drag, drop);\n }\n };\n\n /**\n * When infinite group and there is already a drag in a drop, reject the exact clone in the same drop.\n *\n * @param {int} currentPlace the position of the current drop.\n * @param {jQuery} drop the drop containing a drag.\n * @param {jQuery} oldDrag the drag already placed in drop.\n * @param {jQuery} drag the new drag which is exactly the same (clone) as oldDrag .\n * @returns {boolean}\n */\n DragDropToTextQuestion.prototype.hasDropSameDrag = function(currentPlace, drop, oldDrag, drag) {\n if (drag.hasClass('infinite')) {\n return drop.hasClass('place' + currentPlace) &&\n this.getGroup(drag) === this.getGroup(drop) &&\n this.getChoice(drag) === this.getChoice(oldDrag) &&\n this.getGroup(drag) === this.getGroup(oldDrag);\n }\n return false;\n };\n\n /**\n * Animate a drag back to its home.\n *\n * @param {jQuery} drag the item being moved.\n */\n DragDropToTextQuestion.prototype.sendDragHome = function(drag) {\n var currentPlace = this.getClassnameNumericSuffix(drag, 'inplace');\n if (currentPlace !== null) {\n drag.removeClass('inplace' + currentPlace);\n }\n drag.data('unplaced', true);\n\n this.animateTo(drag, this.getDragHome(this.getGroup(drag), this.getChoice(drag)));\n };\n\n /**\n * Handles keyboard events on drops.\n *\n * Drops are focusable. Once focused, right/down/space switches to the next choice, and\n * left/up switches to the previous. Escape clear.\n *\n * @param {KeyboardEvent} e\n */\n DragDropToTextQuestion.prototype.handleKeyPress = function(e) {\n var drop = $(e.target).closest('.drop');\n if (drop.length === 0) {\n var placedDrag = $(e.target);\n var currentPlace = this.getClassnameNumericSuffix(placedDrag, 'inplace');\n if (currentPlace !== null) {\n drop = this.getDrop(placedDrag, currentPlace);\n }\n }\n var currentDrag = this.getCurrentDragInPlace(this.getPlace(drop)),\n nextDrag = $();\n\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n nextDrag = this.getNextDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n nextDrag = this.getPreviousDrag(this.getGroup(drop), currentDrag);\n break;\n\n case keys.escape:\n break;\n\n default:\n questionManager.isKeyboardNavigation = false;\n return; // To avoid the preventDefault below.\n }\n\n if (nextDrag.length) {\n nextDrag.data('isfocus', true);\n nextDrag.addClass('beingdragged');\n var hiddenDrag = this.getDragClone(nextDrag);\n if (hiddenDrag.length) {\n if (nextDrag.hasClass('infinite')) {\n var noOfDrags = this.noOfDropsInGroup(this.getGroup(nextDrag));\n var cloneDrags = this.getInfiniteDragClones(nextDrag, false);\n if (cloneDrags.length < noOfDrags) {\n var cloneDrag = nextDrag.clone();\n cloneDrag.removeClass('beingdragged');\n cloneDrag.removeAttr('tabindex');\n hiddenDrag.after(cloneDrag);\n questionManager.addEventHandlersToDrag(cloneDrag);\n nextDrag.offset(cloneDrag.offset());\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n } else {\n hiddenDrag.addClass('active');\n nextDrag.offset(hiddenDrag.offset());\n }\n }\n } else {\n drop.data('isfocus', true);\n }\n\n e.preventDefault();\n this.sendDragToDrop(nextDrag, drop);\n };\n\n /**\n * Choose the next drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropToTextQuestion.prototype.getNextDrag = function(group, drag) {\n var choice,\n numChoices = this.noOfChoicesInGroup(group);\n\n if (drag.length === 0) {\n choice = 1; // Was empty, so we want to select the first choice.\n } else {\n choice = this.getChoice(drag) + 1;\n }\n\n var next = this.getUnplacedChoice(group, choice);\n while (next.length === 0 && choice < numChoices) {\n choice++;\n next = this.getUnplacedChoice(group, choice);\n }\n\n return next;\n };\n\n /**\n * Choose the previous drag in a group.\n *\n * @param {int} group which group.\n * @param {jQuery} drag current choice (empty jQuery if there isn't one).\n * @return {jQuery} the next drag in that group, or null if there wasn't one.\n */\n DragDropToTextQuestion.prototype.getPreviousDrag = function(group, drag) {\n var choice;\n\n if (drag.length === 0) {\n choice = this.noOfChoicesInGroup(group);\n } else {\n choice = this.getChoice(drag) - 1;\n }\n\n var previous = this.getUnplacedChoice(group, choice);\n while (previous.length === 0 && choice > 1) {\n choice--;\n previous = this.getUnplacedChoice(group, choice);\n }\n\n // Does this choice exist?\n return previous;\n };\n\n /**\n * Animate an object to the given destination.\n *\n * @param {jQuery} drag the element to be animated.\n * @param {jQuery} target element marking the place to move it to.\n */\n DragDropToTextQuestion.prototype.animateTo = function(drag, target) {\n var currentPos = drag.offset(),\n targetPos = target.offset(),\n thisQ = this;\n\n M.util.js_pending('qtype_ddwtos-animate-' + thisQ.containerId);\n // Animate works in terms of CSS position, whereas locating an object\n // on the page works best with jQuery offset() function. So, to get\n // the right target position, we work out the required change in\n // offset() and then add that to the current CSS position.\n drag.animate(\n {\n left: parseInt(drag.css('left')) + targetPos.left - currentPos.left,\n top: parseInt(drag.css('top')) + targetPos.top - currentPos.top\n },\n {\n duration: 'fast',\n done: function() {\n $('body').trigger('qtype_ddwtos-dragmoved', [drag, target, thisQ]);\n M.util.js_complete('qtype_ddwtos-animate-' + thisQ.containerId);\n }\n }\n );\n };\n\n /**\n * Detect if a point is inside a given DOM node.\n *\n * @param {Number} pageX the x position.\n * @param {Number} pageY the y position.\n * @param {jQuery} drop the node to check (typically a drop).\n * @return {boolean} whether the point is inside the node.\n */\n DragDropToTextQuestion.prototype.isPointInDrop = function(pageX, pageY, drop) {\n var position = drop.offset();\n return pageX >= position.left && pageX < position.left + drop.width()\n && pageY >= position.top && pageY < position.top + drop.height();\n };\n\n /**\n * Set the value of the hidden input for a place, to record what is currently there.\n *\n * @param {int} place which place to set the input value for.\n * @param {int} choice the value to set.\n */\n DragDropToTextQuestion.prototype.setInputValue = function(place, choice) {\n this.getRoot().find('input.placeinput.place' + place).val(choice);\n };\n\n /**\n * Get the outer div for this question.\n *\n * @returns {jQuery} containing that div.\n */\n DragDropToTextQuestion.prototype.getRoot = function() {\n return $(document.getElementById(this.containerId));\n };\n\n /**\n * Get drag home for a given choice.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} containing that div.\n */\n DragDropToTextQuestion.prototype.getDragHome = function(group, choice) {\n if (!this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice).is(':visible')) {\n return this.getRoot().find('.draggrouphomes' + group +\n ' span.draghome.infinite' +\n '.choice' + choice +\n '.group' + group);\n }\n return this.getRoot().find('.draghome.dragplaceholder.group' + group + '.choice' + choice);\n };\n\n /**\n * Get an unplaced choice for a particular group.\n *\n * @param {int} group the group.\n * @param {int} choice the choice number.\n * @returns {jQuery} jQuery wrapping the unplaced choice. If there isn't one, the jQuery will be empty.\n */\n DragDropToTextQuestion.prototype.getUnplacedChoice = function(group, choice) {\n return this.getRoot().find('.draghome.group' + group + '.choice' + choice + '.unplaced').slice(0, 1);\n };\n\n /**\n * Get the drag that is currently in a given place.\n *\n * @param {int} place the place number.\n * @return {jQuery} the current drag (or an empty jQuery if none).\n */\n DragDropToTextQuestion.prototype.getCurrentDragInPlace = function(place) {\n return this.getRoot().find('span.draghome.inplace' + place);\n };\n\n /**\n * Return the number of blanks in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of drops.\n */\n DragDropToTextQuestion.prototype.noOfDropsInGroup = function(group) {\n return this.getRoot().find('.drop.group' + group).length;\n };\n\n /**\n * Return the number of choices in a given group.\n *\n * @param {int} group the group number.\n * @returns {int} the number of choices.\n */\n DragDropToTextQuestion.prototype.noOfChoicesInGroup = function(group) {\n return this.getRoot().find('.draghome.group' + group).length;\n };\n\n /**\n * Return the number at the end of the CSS class name with the given prefix.\n *\n * @param {jQuery} node\n * @param {String} prefix name prefix\n * @returns {Number|null} the suffix if found, else null.\n */\n DragDropToTextQuestion.prototype.getClassnameNumericSuffix = function(node, prefix) {\n var classes = node.attr('class');\n if (classes !== undefined && classes !== '') {\n var classesArr = classes.split(' ');\n for (var index = 0; index < classesArr.length; index++) {\n var patt1 = new RegExp('^' + prefix + '([0-9])+$');\n if (patt1.test(classesArr[index])) {\n var patt2 = new RegExp('([0-9])+$');\n var match = patt2.exec(classesArr[index]);\n return Number(match[0]);\n }\n }\n }\n return null;\n };\n\n /**\n * Get the choice number of a drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {Number} the choice number.\n */\n DragDropToTextQuestion.prototype.getChoice = function(drag) {\n return this.getClassnameNumericSuffix(drag, 'choice');\n };\n\n /**\n * Given a DOM node that is significant to this question\n * (drag, drop, ...) get the group it belongs to.\n *\n * @param {jQuery} node a DOM node.\n * @returns {Number} the group it belongs to.\n */\n DragDropToTextQuestion.prototype.getGroup = function(node) {\n return this.getClassnameNumericSuffix(node, 'group');\n };\n\n /**\n * Get the place number of a drop, or its corresponding hidden input.\n *\n * @param {jQuery} node the DOM node.\n * @returns {Number} the place number.\n */\n DragDropToTextQuestion.prototype.getPlace = function(node) {\n return this.getClassnameNumericSuffix(node, 'place');\n };\n\n /**\n * Get drag clone for a given drag.\n *\n * @param {jQuery} drag the drag.\n * @returns {jQuery} the drag's clone.\n */\n DragDropToTextQuestion.prototype.getDragClone = function(drag) {\n return this.getRoot().find('.draggrouphomes' +\n this.getGroup(drag) +\n ' span.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.dragplaceholder');\n };\n\n /**\n * Get infinite drag clones for given drag.\n *\n * @param {jQuery} drag the drag.\n * @param {Boolean} inHome in the home area or not.\n * @returns {jQuery} the drag's clones.\n */\n DragDropToTextQuestion.prototype.getInfiniteDragClones = function(drag, inHome) {\n if (inHome) {\n return this.getRoot().find('.draggrouphomes' +\n this.getGroup(drag) +\n ' span.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n }\n return this.getRoot().find('span.draghome' +\n '.choice' + this.getChoice(drag) +\n '.group' + this.getGroup(drag) +\n '.infinite').not('.dragplaceholder');\n };\n\n /**\n * Get drop for a given drag and place.\n *\n * @param {jQuery} drag the drag.\n * @param {Integer} currentPlace the current place of drag.\n * @returns {jQuery} the drop's clone.\n */\n DragDropToTextQuestion.prototype.getDrop = function(drag, currentPlace) {\n return this.getRoot().find('.drop.group' + this.getGroup(drag) + '.place' + currentPlace);\n };\n\n /**\n * Singleton that tracks all the DragDropToTextQuestions on this page, and deals\n * with event dispatching.\n *\n * @type {Object}\n */\n var questionManager = {\n /**\n * {boolean} used to ensure the event handlers are only initialised once per page.\n */\n eventHandlersInitialised: false,\n\n /**\n * {Object} ensures that the drag event handlers are only initialised once per question,\n * indexed by containerId (id on the .que div).\n */\n dragEventHandlersInitialised: {},\n\n /**\n * {boolean} is keyboard navigation or not.\n */\n isKeyboardNavigation: false,\n\n /**\n * {DragDropToTextQuestion[]} all the questions on this page, indexed by containerId (id on the .que div).\n */\n questions: {},\n\n /**\n * Initialise questions.\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 */\n init: function(containerId, readOnly) {\n questionManager.questions[containerId] = new DragDropToTextQuestion(containerId, readOnly);\n if (!questionManager.eventHandlersInitialised) {\n questionManager.setupEventHandlers();\n questionManager.eventHandlersInitialised = true;\n }\n if (!questionManager.dragEventHandlersInitialised.hasOwnProperty(containerId)) {\n questionManager.dragEventHandlersInitialised[containerId] = true;\n // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.\n var questionContainer = document.getElementById(containerId);\n if (questionContainer.classList.contains('ddwtos') &&\n !questionContainer.classList.contains('qtype_ddwtos-readonly')) {\n // TODO: Convert all the jQuery selectors and events to native Javascript.\n questionManager.addEventHandlersToDrag($(questionContainer).find('span.draghome'));\n }\n }\n },\n\n /**\n * Set up the event handlers that make this question type work. (Done once per page.)\n */\n setupEventHandlers: function() {\n $('body')\n .on('keydown',\n '.que.ddwtos:not(.qtype_ddwtos-readonly) span.drop',\n questionManager.handleKeyPress)\n .on('keydown',\n '.que.ddwtos:not(.qtype_ddwtos-readonly) span.draghome.placed:not(.beingdragged)',\n questionManager.handleKeyPress)\n .on('qtype_ddwtos-dragmoved', questionManager.handleDragMoved);\n },\n\n /**\n * Binding the drag/touch event again for newly created element.\n *\n * @param {jQuery} element Element to bind the event\n */\n addEventHandlersToDrag: function(element) {\n // Unbind all the mousedown and touchstart events to prevent double binding.\n element.unbind('mousedown touchstart');\n element.on('mousedown touchstart', questionManager.handleDragStart);\n },\n\n /**\n * Handle mouse down / touch start on drags.\n * @param {Event} e the DOM event.\n */\n handleDragStart: function(e) {\n e.preventDefault();\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleDragStart(e);\n }\n },\n\n /**\n * Handle key down / press on drops.\n * @param {KeyboardEvent} e\n */\n handleKeyPress: function(e) {\n if (questionManager.isKeyboardNavigation) {\n return;\n }\n questionManager.isKeyboardNavigation = true;\n var question = questionManager.getQuestionForEvent(e);\n if (question) {\n question.handleKeyPress(e);\n }\n },\n\n /**\n * Given an event, work out which question it affects.\n *\n * @param {Event} e the event.\n * @returns {DragDropToTextQuestion|undefined} The question, or undefined.\n */\n getQuestionForEvent: function(e) {\n var containerId = $(e.currentTarget).closest('.que.ddwtos').attr('id');\n return questionManager.questions[containerId];\n },\n\n /**\n * Handle when drag moved.\n *\n * @param {Event} e the event.\n * @param {jQuery} drag the drag\n * @param {jQuery} target the target\n * @param {DragDropToTextQuestion} thisQ the question.\n */\n handleDragMoved: function(e, drag, target, thisQ) {\n drag.removeClass('beingdragged');\n drag.css('top', '').css('left', '');\n target.after(drag);\n target.removeClass('active');\n if (typeof drag.data('unplaced') !== 'undefined' && drag.data('unplaced') === true) {\n drag.removeClass('placed').addClass('unplaced');\n drag.removeAttr('tabindex');\n drag.removeData('unplaced');\n if (drag.hasClass('infinite') && thisQ.getInfiniteDragClones(drag, true).length > 1) {\n thisQ.getInfiniteDragClones(drag, true).first().remove();\n }\n }\n if (typeof drag.data('isfocus') !== 'undefined' && drag.data('isfocus') === true) {\n drag.focus();\n drag.removeData('isfocus');\n }\n if (typeof target.data('isfocus') !== 'undefined' && target.data('isfocus') === true) {\n target.removeData('isfocus');\n }\n if (questionManager.isKeyboardNavigation) {\n questionManager.isKeyboardNavigation = false;\n }\n if (thisQ.isQuestionInteracted()) {\n // The user has interacted with the draggable items. We need to mark the form as dirty.\n questionManager.handleFormDirty();\n // Save the new answered value.\n thisQ.questionAnswer = thisQ.getQuestionAnsweredValues();\n }\n },\n\n /**\n * Handle when the form is dirty.\n */\n handleFormDirty: function() {\n const responseForm = document.getElementById('responseform');\n FormChangeChecker.markFormAsDirty(responseForm);\n }\n };\n\n /**\n * @alias module:qtype_ddwtos/ddwtos\n */\n return {\n /**\n * Initialise one drag-drop into text 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 */\n init: questionManager.init\n };\n});\n"],"names":["define","$","dragDrop","keys","FormChangeChecker","filterEvent","DragDropToTextQuestion","containerId","readOnly","thisQ","this","questionAnswer","questionDragDropWidthHeight","getRoot","addClass","resizeAllDragsAndDrops","cloneDrags","positionDrags","document","addEventListener","eventTypes","filterContentRenderingComplete","elements","detail","nodes","forEach","element","changeAllDragsAndDropsToFilteredContent","prototype","find","each","i","node","resizeAllDragsAndDropsInGroup","getClassnameNumericSuffix","group","dragDropItems","maxWidth","maxHeight","drag","Math","max","ceil","offsetWidth","offsetHeight","setElementSize","filteredElement","currentFilteredItem","parentIsDD","parent","closest","hasClass","isDD","length","getGroup","choice","getChoice","listOfModifiedDragDrop","get","originalClass","attr","originalStyle","filteredDragDropClone","clone","before","push","remove","currentHeight","height","currentWidth","width","classList","add","css","index","draghome","placeHolder","removeClass","root","not","dragNode","currentPlace","removeAttr","inputNode","input","val","place","getPlace","drop","dropPosition","offset","data","top","left","unplacedDrag","getUnplacedChoice","hiddenDrag","getDragClone","noOfDrags","noOfDropsInGroup","getInfiniteDragClones","cloneDrag","after","questionManager","addEventHandlersToDrag","sendDragToDrop","getQuestionAnsweredValues","result","id","value","isQuestionInteracted","oldAnswer","newAnswer","isInteracted","JSON","stringify","Object","key","handleDragStart","e","target","prepare","start","setInputValue","hiddenDrop","getDrop","x","y","dragMove","dragEnd","pageX","pageY","dropNode","isPointInDrop","placed","dropZone","sendDragHome","oldDrag","getCurrentDragInPlace","hasDropSameDrag","focus","animateTo","getDragHome","handleKeyPress","placedDrag","currentDrag","nextDrag","keyCode","space","arrowRight","arrowDown","getNextDrag","arrowLeft","arrowUp","getPreviousDrag","escape","isKeyboardNavigation","preventDefault","numChoices","noOfChoicesInGroup","next","previous","currentPos","targetPos","M","util","js_pending","animate","parseInt","duration","done","trigger","js_complete","position","getElementById","is","slice","prefix","classes","undefined","classesArr","split","RegExp","test","match","exec","Number","inHome","eventHandlersInitialised","dragEventHandlersInitialised","questions","init","setupEventHandlers","hasOwnProperty","questionContainer","contains","on","handleDragMoved","unbind","question","getQuestionForEvent","currentTarget","removeData","first","handleFormDirty","responseForm","markFormAsDirty"],"mappings":"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"} \ No newline at end of file diff --git a/question/type/ddwtos/amd/src/ddwtos.js b/question/type/ddwtos/amd/src/ddwtos.js index d02bc003c25d9..941e992b9bd8d 100644 --- a/question/type/ddwtos/amd/src/ddwtos.js +++ b/question/type/ddwtos/amd/src/ddwtos.js @@ -41,12 +41,14 @@ define([ 'jquery', 'core/dragdrop', 'core/key_codes', - 'core_form/changechecker' + 'core_form/changechecker', + 'core_filters/events', ], function( $, dragDrop, keys, - FormChangeChecker + FormChangeChecker, + filterEvent ) { "use strict"; @@ -59,14 +61,22 @@ define([ * @constructor */ function DragDropToTextQuestion(containerId, readOnly) { + const thisQ = this; this.containerId = containerId; this.questionAnswer = {}; + this.questionDragDropWidthHeight = []; if (readOnly) { this.getRoot().addClass('qtype_ddwtos-readonly'); } this.resizeAllDragsAndDrops(); this.cloneDrags(); this.positionDrags(); + // Wait for all dynamic content loaded by filter to be completed. + document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => { + elements.detail.nodes.forEach((element) => { + thisQ.changeAllDragsAndDropsToFilteredContent(element); + }); + }); } /** @@ -87,12 +97,12 @@ define([ */ DragDropToTextQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) { var thisQ = this, - dragHomes = this.getRoot().find('.draggrouphomes' + group + ' span.draghome'), + dragDropItems = this.getRoot().find('span.group' + group), maxWidth = 0, maxHeight = 0; // Find the maximum size of any drag in this groups. - dragHomes.each(function(i, drag) { + dragDropItems.each(function(i, drag) { maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth)); maxHeight = Math.max(maxHeight, Math.ceil(0 + drag.offsetHeight)); }); @@ -100,16 +110,85 @@ define([ // The size we will want to set is a bit bigger than this. maxWidth += 8; maxHeight += 2; - + thisQ.questionDragDropWidthHeight[group] = {maxWidth: maxWidth, maxHeight: maxHeight}; // Set each drag home to that size. - dragHomes.each(function(i, drag) { + dragDropItems.each(function(i, drag) { thisQ.setElementSize(drag, maxWidth, maxHeight); }); + }; + + /** + * Change all the drags and drops related to the item that has been changed by filter to correct size and content. + * + * @param {object} filteredElement the element has been modified by filter. + */ + DragDropToTextQuestion.prototype.changeAllDragsAndDropsToFilteredContent = function(filteredElement) { + let currentFilteredItem = $(filteredElement); + const parentIsDD = currentFilteredItem.parent().closest('span').hasClass('placed') || + currentFilteredItem.parent().closest('span').hasClass('draghome'); + const isDD = currentFilteredItem.hasClass('placed') || currentFilteredItem.hasClass('draghome'); + // The filtered element or parent element should a drag or drop item. + if (!parentIsDD && !isDD) { + return; + } + if (parentIsDD) { + currentFilteredItem = currentFilteredItem.parent().closest('span'); + } + const thisQ = this; + if (thisQ.getRoot().find(currentFilteredItem).length <= 0) { + // If the DD item doesn't belong to this question + // In case we have multiple questions in the same page. + return; + } + const group = thisQ.getGroup(currentFilteredItem), + choice = thisQ.getChoice(currentFilteredItem); + let listOfModifiedDragDrop = []; + // Get the list of drag and drop item within the same group and choice. + this.getRoot().find('.group' + group + '.choice' + choice).each(function(i, node) { + // Same modified item, skip it. + if ($(node).get(0) === currentFilteredItem.get(0)) { + return; + } + const originalClass = $(node).attr('class'); + const originalStyle = $(node).attr('style'); + // We want to keep all the handler and event for filtered item, so using clone is the only choice. + const filteredDragDropClone = currentFilteredItem.clone(); + // Replace the class and style of the drag drop item we want to replace for the clone. + filteredDragDropClone.attr('class', originalClass); + filteredDragDropClone.attr('style', originalStyle); + // Insert into DOM. + $(node).before(filteredDragDropClone); + // Add the item has been replaced to a list so we can remove it later. + listOfModifiedDragDrop.push(node); + }); - // Set each drop to that size. - this.getRoot().find('span.drop.group' + group).each(function(i, drop) { - thisQ.setElementSize(drop, maxWidth, maxHeight); + listOfModifiedDragDrop.forEach(function(node) { + $(node).remove(); }); + // Save the current height and width. + const currentHeight = currentFilteredItem.height(); + const currentWidth = currentFilteredItem.width(); + // Set to auto so we can get the real height and width of the filtered item. + currentFilteredItem.height('auto'); + currentFilteredItem.width('auto'); + // We need to set display block so we can get height and width. + // Some browser can't get the offsetWidth/Height if they are an inline element like span tag. + if (!filteredElement.offsetWidth || !filteredElement.offsetHeight) { + filteredElement.classList.add('d-block'); + } + if (thisQ.questionDragDropWidthHeight[group].maxWidth < Math.ceil(filteredElement.offsetWidth) || + thisQ.questionDragDropWidthHeight[group].maxHeight < Math.ceil(0 + filteredElement.offsetHeight)) { + // Remove the d-block class before calculation. + filteredElement.classList.remove('d-block'); + // Now resize all the items in the same group if we have new maximum width or height. + thisQ.resizeAllDragsAndDropsInGroup(group); + } else { + // Return the original height and width in case the real height and width is not the maximum. + currentFilteredItem.height(currentHeight); + currentFilteredItem.width(currentWidth); + } + // Remove the d-block class after resize. + filteredElement.classList.remove('d-block'); }; /** diff --git a/question/type/ddwtos/renderer.php b/question/type/ddwtos/renderer.php index bf3d5b194f695..a98af708c7dd8 100644 --- a/question/type/ddwtos/renderer.php +++ b/question/type/ddwtos/renderer.php @@ -107,11 +107,9 @@ protected function embedded_element(question_attempt $qa, $place, protected function drag_boxes($qa, $group, $choices, question_display_options $options) { $boxes = ''; - foreach ($choices as $key => $choice) { - // Bug 8632: long text entry causes bug in drag and drop field in IE. - $content = str_replace('-', '‑', $choice->text); - $content = str_replace(' ', ' ', $content); + foreach ($choices as $key => $choice) { + $content = question_utils::format_question_fragment($choice->text, $this->page->context); $infinite = ''; if ($choice->infinite) { $infinite = ' infinite'; diff --git a/question/type/ddwtos/styles.css b/question/type/ddwtos/styles.css index 35fb15b0947fa..aa552cdaf505c 100644 --- a/question/type/ddwtos/styles.css +++ b/question/type/ddwtos/styles.css @@ -126,3 +126,7 @@ .que.ddwtos sub { bottom: -0.2em; } + +.que.ddwtos .MathJax_Display { + margin: 0; +} diff --git a/question/type/ddwtos/tests/behat/preview.feature b/question/type/ddwtos/tests/behat/preview.feature index bf44032cf46ca..77deea44ce98f 100644 --- a/question/type/ddwtos/tests/behat/preview.feature +++ b/question/type/ddwtos/tests/behat/preview.feature @@ -18,9 +18,10 @@ Feature: Preview a drag-drop into text question | contextlevel | reference | name | | Course | C1 | Test questions | And the following "questions" exist: - | questioncategory | qtype | name | template | - | Test questions | ddwtos | Drag to text | fox | - | Test questions | ddwtos | Drag to text infinite | infinite | + | questioncategory | qtype | name | template | + | Test questions | ddwtos | Drag to text | fox | + | Test questions | ddwtos | Drag to text infinite | infinite | + | Test questions | ddwtos | Drag to mathjax equation | mathjax | @javascript @_bug_phantomjs Scenario: Preview a question using the mouse. @@ -78,3 +79,12 @@ Feature: Preview a drag-drop into text question Then I should see "Option1" in the home area of drag and drop into text question And I should see "Option2" in the home area of drag and drop into text question And I should see "Option3" in the home area of drag and drop into text question + + @javascript + Scenario: Preview a drag-drop into text question with mathjax question. + Given the "mathjaxloader" filter is "on" + And the "mathjaxloader" filter applies to "content and headings" + And I am on the "Drag to mathjax equation" "core_question > preview" page logged in as teacher + And I press "Fill in correct responses" + When I press "Submit and finish" + Then ".filter_mathjaxloader_equation" "css_element" should exist in the ".draghome" "css_element" diff --git a/question/type/ddwtos/tests/helper.php b/question/type/ddwtos/tests/helper.php index d5e8b9f063a27..9517a9720c735 100644 --- a/question/type/ddwtos/tests/helper.php +++ b/question/type/ddwtos/tests/helper.php @@ -34,7 +34,7 @@ */ class qtype_ddwtos_test_helper extends question_test_helper { public function get_test_questions() { - return array('fox', 'maths', 'oddgroups', 'missingchoiceno', 'infinite'); + return ['fox', 'maths', 'oddgroups', 'missingchoiceno', 'infinite', 'mathjax']; } /** @@ -156,6 +156,30 @@ public function get_ddwtos_question_form_data_missingchoiceno() { return $fromform; } + /** + * Get data required to save a drag-drop into text question where the the answer contain equation + * + * + * @return stdClass data to create a ddwtos question. + */ + public function get_ddwtos_question_form_data_mathjax() { + $fromform = new stdClass(); + + $fromform->name = 'Drag-and-drop words into sentences question with equation'; + $fromform->questiontext = ['text' => 'Fill in the correct mathjax equation: y = 2, x =4 : [[1]]', 'format' => FORMAT_HTML]; + $fromform->defaultmark = 1.0; + $fromform->generalfeedback = ['text' => 'The right answer is: "y = x^2"', 'format' => FORMAT_HTML]; + $fromform->choices = [ + ['answer' => '$$ y = x^2 $$', 'choicegroup' => '1'], + ['answer' => '$$ y = x^5 $$', 'choicegroup' => '1'], + ]; + test_question_maker::set_standard_combined_feedback_form_data($fromform); + $fromform->shownumcorrect = 0; + $fromform->penalty = 0.3333333; + $fromform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY; + return $fromform; + } + /** * @return qtype_ddwtos_question */ From 8f2d505e38c62c4d36642171d1a6958317c4b28a Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Mon, 12 Feb 2024 22:21:26 +0000 Subject: [PATCH 006/119] MDL-80907 behat: be more precise in selecting table rows --- lib/tests/behat/behat_general.php | 174 ++++++++++++++++++++---------- 1 file changed, 117 insertions(+), 57 deletions(-) diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index b38bd60956df5..74651ceaf4418 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -1423,31 +1423,8 @@ public function row_column_of_table_should_contain($row, $column, $table, $value $rowliteral = behat_context_helper::escape($row); $valueliteral = behat_context_helper::escape($value); - $columnliteral = behat_context_helper::escape($column); - - if (preg_match('/^-?(\d+)-?$/', $column, $columnasnumber)) { - // Column indicated as a number, just use it as position of the column. - $columnpositionxpath = "/child::*[position() = {$columnasnumber[1]}]"; - } else { - // Header can be in thead or tbody (first row), following xpath should work. - $theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" . - $columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]"; - $tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" . - $columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]"; - // Check if column exists. - $columnheaderxpath = $tablexpath . "[" . $theadheaderxpath . " | " . $tbodyheaderxpath . "]"; - $columnheader = $this->getSession()->getDriver()->find($columnheaderxpath); - if (empty($columnheader)) { - $columnexceptionmsg = $column . '" in table "' . $table . '"'; - throw new ElementNotFoundException($this->getSession(), "\n$columnheaderxpath\n\n".'Column', null, $columnexceptionmsg); - } - // Following conditions were considered before finding column count. - // 1. Table header can be in thead/tr/th or tbody/tr/td[1]. - // 2. First column can have th (Gradebook -> user report), so having lenient sibling check. - $columnpositionxpath = "/child::*[position() = count(" . $tablexpath . "/" . $theadheaderxpath . - "/preceding-sibling::*) + 1]"; - } + $columnpositionxpath = $this->get_table_column_xpath($table, $column); // Check if value exists in specific row/column. // Get row xpath. @@ -1489,6 +1466,99 @@ public function row_column_of_table_should_not_contain($row, $column, $table, $v ); } + /** + * Get xpath for a row child that corresponds to the specified column header + * + * @param string $table table identifier that can be used with 'table' node selector (i.e. table title or CSS class) + * @param string $column either text in the column header or the column number, such as -1-, -2-, etc + * When matching the column header it has to be either exact match of the whole header or an exact + * match of a text inside a link in the header. + * For example, to match "First name / Last name" you need to specify either "First name" or "Last name" + * @return string + */ + protected function get_table_column_xpath(string $table, string $column): string { + $tablenode = $this->get_selected_node('table', $table); + $tablexpath = $tablenode->getXpath(); + $columnliteral = behat_context_helper::escape($column); + if (preg_match('/^-?(\d+)-?$/', $column, $columnasnumber)) { + // Column indicated as a number, just use it as position of the column. + $columnpositionxpath = "/child::*[position() = {$columnasnumber[1]}]"; + } else { + // Header can be in thead or tbody (first row), following xpath should work. + $theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)={$columnliteral} or a[normalize-space(text())=" . + $columnliteral . "] or div[normalize-space(text())={$columnliteral}])]"; + $tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)={$columnliteral} or a[normalize-space(text())=" . + $columnliteral . "] or div[normalize-space(text())={$columnliteral}])]"; + + // Check if column exists. + $columnheaderxpath = "{$tablexpath}[{$theadheaderxpath} | {$tbodyheaderxpath}]"; + $columnheader = $this->getSession()->getDriver()->find($columnheaderxpath); + if (empty($columnheader)) { + if (strpos($column, '/') !== false) { + // We are not able to match headers consisting of several links, such as "First name / Last name". + // Instead we can match "First name" or "Last name" or "-1-" (column number). + throw new Exception("Column matching locator \"$column\" not found. ". + "If the column header contains multiple links, specify only one of the link texts. ". + "Otherwise, use the column number as the locator"); + } + $columnexceptionmsg = $column . '" in table "' . $table . '"'; + throw new ElementNotFoundException($this->getSession(), "\n$columnheaderxpath\n\n".'Column', + null, $columnexceptionmsg); + } + // Following conditions were considered before finding column count. + // 1. Table header can be in thead/tr/th or tbody/tr/td[1]. + // 2. First column can have th (Gradebook -> user report), so having lenient sibling check. + $columnpositionxpath = "/child::*[position() = count({$tablexpath}/{$theadheaderxpath}" . + "/preceding-sibling::*) + 1]"; + } + return $columnpositionxpath; + } + + /** + * Find a table row where each of the specified columns matches and throw exception if not found + * + * @param string $table table locator + * @param array $cells key is the column locator (name or index such as '-1-') and value is the text contents of the table cell + */ + protected function ensure_table_row_exists(string $table, array $cells): void { + $tablenode = $this->get_selected_node('table', $table); + $tablexpath = $tablenode->getXpath(); + + $columnconditions = []; + foreach ($cells as $columnname => $value) { + $valueliteral = behat_context_helper::escape($value); + $columnpositionxpath = $this->get_table_column_xpath($table, $columnname); + $columnconditions[] = '.' . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]"; + } + $rowxpath = $tablexpath . "/tbody/tr[" . join(' and ', $columnconditions) . ']'; + + $rownode = $this->getSession()->getDriver()->find($rowxpath); + if (empty($rownode)) { + $rowlocator = array_map(fn($k) => "{$k} => {$cells[$k]}", array_keys($cells)); + throw new ElementNotFoundException($this->getSession(), "\n$rowxpath\n\n".'Table row', null, join(', ', $rowlocator)); + } + } + + /** + * Find a table row where each of the specified columns matches and throw exception if found + * + * @param string $table table locator + * @param array $cells key is the column locator (name or index such as '-1-') and value is the text contents of the table cell + */ + protected function ensure_table_row_does_not_exist(string $table, array $cells): void { + try { + $this->ensure_table_row_exists($table, $cells); + // Throw exception if found. + } catch (ElementNotFoundException $e) { + // Table row/column doesn't contain this value. Nothing to do. + return; + } + $rowlocator = array_map(fn($k) => "{$k} => {$cells[$k]}", array_keys($cells)); + throw new ExpectationException('Table row "' . join(', ', $rowlocator) . + '" is present in the table "' . $table . '"', $this->getSession() + ); + } + /** * Checks that the provided value exist in table. * @@ -1505,29 +1575,22 @@ public function row_column_of_table_should_not_contain($row, $column, $table, $v */ public function following_should_exist_in_the_table($table, TableNode $data) { $datahash = $data->getHash(); + if ($datahash && count($data->getRow(0)) != count($datahash[0])) { + // Check that the number of columns in the hash is the same as the number of the columns in the first row. + throw new coding_exception('Table contains duplicate column headers'); + } foreach ($datahash as $row) { - - // Row contains only a single column, just assert it's present in the table. - if (count($row) === 1) { - $this->execute('behat_general::assert_element_contains_text', [reset($row), $table, 'table']); - } else { - // Iterate over all columns. - $firstcell = null; - foreach ($row as $column => $value) { - if ($firstcell === null) { - $firstcell = $value; - } else { - $this->row_column_of_table_should_contain($firstcell, $column, $table, $value); - } - } - } + $this->ensure_table_row_exists($table, $row); } } /** * Checks that the provided values do not exist in a table. * + * If there are more than two columns, we check that NEITHER of the columns 2..n match + * in the row where the first column matches + * * @Then /^the following should not exist in the "(?P[^"]*)" table:$/ * @throws ExpectationException * @param string $table name of table @@ -1537,27 +1600,24 @@ public function following_should_exist_in_the_table($table, TableNode $data) { */ public function following_should_not_exist_in_the_table($table, TableNode $data) { $datahash = $data->getHash(); + if ($datahash && count($data->getRow(0)) != count($datahash[0])) { + // Check that the number of columns in the hash is the same as the number of the columns in the first row. + throw new coding_exception('Table contains duplicate column headers'); + } foreach ($datahash as $value) { - - // Row contains only a single column, just assert it's not present in the table. - if (count($value) === 1) { - $this->execute('behat_general::assert_element_not_contains_text', [reset($value), $table, 'table']); - } else { - // Iterate over all columns. - $row = array_shift($value); - foreach ($value as $column => $value) { - try { - $this->row_column_of_table_should_contain($row, $column, $table, $value); - // Throw exception if found. - } catch (ElementNotFoundException $e) { - // Table row/column doesn't contain this value. Nothing to do. - continue; - } - throw new ExpectationException('"' . $column . '" with value "' . $value . '" is present in "' . - $row . '" row for table "' . $table . '"', $this->getSession() - ); + if (count($value) > 2) { + // When there are more than two columns, what we really want to check is that for the rows + // where the first column matches, NEITHER of the other columns match. + $columns = array_keys($value); + for ($i = 1; $i < count($columns); $i++) { + $this->ensure_table_row_does_not_exist($table, [ + $columns[0] => $value[$columns[0]], + $columns[$i] => $value[$columns[$i]], + ]); } + } else { + $this->ensure_table_row_does_not_exist($table, $value); } } } From 49ab83c1a4c6a27009d23bb4e1637dc33fd63d62 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Mon, 12 Feb 2024 23:30:20 +0000 Subject: [PATCH 007/119] MDL-80907 various: fixes to incorrect column selectors in behat --- admin/tests/behat/browse_users.feature | 12 ++-- .../logstore_store_visibility_change.feature | 2 +- .../task/tests/behat/manage_tasks.feature | 4 +- comment/tests/behat/manage.feature | 2 +- enrol/cohort/tests/behat/enrolcohorts.feature | 4 +- .../grader/tests/behat/switch_views.feature | 12 ++-- .../tests/behat/tertiary_name_filter.feature | 14 ++-- .../tertiary_navigation_searching.feature | 9 +++ .../tests/behat/basic_functionality.feature | 10 +-- .../singleview/tests/behat/singleview.feature | 2 +- grade/tests/behat/grade_import.feature | 2 +- .../tests/behat/grade_letter_boundary.feature | 2 +- .../grade_letter_boundary_20160518.feature | 2 +- .../grade_regrade_do_not_override.feature | 8 +-- grade/tests/behat/grade_scales.feature | 4 +- .../behat/grade_single_item_scales.feature | 4 +- .../behat/toggle_grade_categories.feature | 72 ++++++++----------- group/tests/behat/private_groups.feature | 20 +++--- .../tests/behat/course_summary.feature | 18 ++--- .../tests/behat/summary_filter_groups.feature | 24 +++---- .../behat/summary_filter_no_groups.feature | 2 +- .../configlog/tests/behat/view_report.feature | 2 +- reportbuilder/tests/behat/audience.feature | 2 +- user/tests/behat/table_sorting.feature | 2 +- 24 files changed, 114 insertions(+), 121 deletions(-) diff --git a/admin/tests/behat/browse_users.feature b/admin/tests/behat/browse_users.feature index 0c6a8e309d7d8..b06f508de9dac 100644 --- a/admin/tests/behat/browse_users.feature +++ b/admin/tests/behat/browse_users.feature @@ -18,7 +18,7 @@ Feature: An administrator can browse user accounts When I navigate to "Users > Accounts > Browse list of users" in site administration # Name field always present, email field is default for showidentity. Then the following should exist in the "reportbuilder-table" table: - | First name / Last name | Email address | + | First name | Email address | | User One | one@example.com | | User Two | two@example.com | # Should not see other identity fields or non-default name fields. @@ -34,7 +34,7 @@ Feature: An administrator can browse user accounts | alternativefullnameformat | firstnamephonetic lastname | When I navigate to "Users > Accounts > Browse list of users" in site administration Then the following should exist in the "reportbuilder-table" table: - | First name - phonetic / Last name | Email address | + | First name - phonetic | Email address | | Yewzer One | one@example.com | | Yoozare Two | two@example.com | @@ -43,7 +43,7 @@ Feature: An administrator can browse user accounts | showuseridentity | department,profile_field_frog | When I navigate to "Users > Accounts > Browse list of users" in site administration Then the following should exist in the "reportbuilder-table" table: - | First name / Last name | Favourite frog | Department | + | First name | Favourite frog | Department | | User One | Kermit | Attack | | User Two | Tree | Defence | And I should not see "Email address" in the "table" "css_element" @@ -103,7 +103,7 @@ Feature: An administrator can browse user accounts | user3 | User | Three | three@example.com | Glass | And I navigate to "Users > Accounts > Browse list of users" in site administration Then the following should exist in the "reportbuilder-table" table: - | First name / Last name | Email address | + | First name | Email address | | User One | one@example.com | | User Two | two@example.com | | User Three | three@example.com | @@ -137,7 +137,7 @@ Feature: An administrator can browse user accounts | user2 | C1 | student | And I navigate to "Users > Accounts > Browse list of users" in site administration Then the following should exist in the "reportbuilder-table" table: - | First name / Last name | Email address | + | First name | Email address | | User One | one@example.com | | User Two | two@example.com | And I click on "Filters" "button" @@ -166,7 +166,7 @@ Feature: An administrator can browse user accounts | user1 | coursecreator | system | | And I navigate to "Users > Accounts > Browse list of users" in site administration Then the following should exist in the "reportbuilder-table" table: - | First name / Last name | Email address | + | First name | Email address | | User One | one@example.com | | User Two | two@example.com | | User Three | three@example.com | diff --git a/admin/tool/log/tests/behat/logstore_store_visibility_change.feature b/admin/tool/log/tests/behat/logstore_store_visibility_change.feature index 79e5ebfe53a4f..39641c02e3d72 100644 --- a/admin/tool/log/tests/behat/logstore_store_visibility_change.feature +++ b/admin/tool/log/tests/behat/logstore_store_visibility_change.feature @@ -14,6 +14,6 @@ Feature: In a report, admin can see logstore visibility changes And I click on "Enable" "icon" in the "External database log" "table_row" And I navigate to "Reports > Config changes" in site administration Then the following should exist in the "reportbuilder-table" table: - | User | Plugin | Setting | New value | Original value | + | First name | Plugin | Setting | New value | Original value | | Admin User | logstore_standard | tool_logstore_visibility | 0 | 1 | | Admin User | logstore_database | tool_logstore_visibility | 1 | 0 | diff --git a/admin/tool/task/tests/behat/manage_tasks.feature b/admin/tool/task/tests/behat/manage_tasks.feature index dd27d85f713e5..3fb58f25d8a0b 100644 --- a/admin/tool/task/tests/behat/manage_tasks.feature +++ b/admin/tool/task/tests/behat/manage_tasks.feature @@ -62,8 +62,8 @@ Feature: Manage scheduled tasks And I press "Save changes" Then I should see "Changes saved" And the following should not exist in the "admintable" table: - | Name | Component | Minute | Hour | Day | Day of week | Month | - | Log table cleanup | Standard log | */5 | 1 | 2 | 4 | 3 | + | Name | Minute | Hour | Day | Day of week | Month | + | Log table cleanup | */5 | 1 | 2 | 4 | 3 | And I should see "Log table cleanup" in the "tr.table-primary" "css_element" Scenario: Disabled plugin's tasks are labelled as disabled too diff --git a/comment/tests/behat/manage.feature b/comment/tests/behat/manage.feature index 8f59e9dd185b0..64b231f8cd2ef 100644 --- a/comment/tests/behat/manage.feature +++ b/comment/tests/behat/manage.feature @@ -17,7 +17,7 @@ Feature: Manage comments made by users Scenario: View and filter site comments When I navigate to "Reports > Comments" in site administration And the following should exist in the "reportbuilder-table" table: - | -0- | Content | Context URL | + | First name | Content | Context URL | | Admin User | Uno | Course: Course 1 | | Admin User | Dos | Course: Course 1 | | Admin User | Tres | Course: Course 1 | diff --git a/enrol/cohort/tests/behat/enrolcohorts.feature b/enrol/cohort/tests/behat/enrolcohorts.feature index 2571dc52cbebf..a59b8a48029c6 100644 --- a/enrol/cohort/tests/behat/enrolcohorts.feature +++ b/enrol/cohort/tests/behat/enrolcohorts.feature @@ -83,11 +83,11 @@ Feature: Cohort enrolment management And I navigate to course participants # Verifies students 1 and 4 are in the cohort and student 2 is not any more. And the following should exist in the "participants" table: - | First name / Last name | Email address | Roles | Groups | + | First name | Email address | Roles | Groups | | Sandra Cole | s1@example.com | Student | Alpha1 cohort | | Jane Doe | s4@example.com | Student | Alpha1 cohort | And the following should not exist in the "participants" table: - | First name / Last name | Email address | Roles | Groups | + | First name | Email address | Roles | Groups | | John Smith | s2@example.com | Student | Alpha1 cohort | @javascript diff --git a/grade/report/grader/tests/behat/switch_views.feature b/grade/report/grader/tests/behat/switch_views.feature index 8560e94ac0f2d..d493e43a746d5 100644 --- a/grade/report/grader/tests/behat/switch_views.feature +++ b/grade/report/grader/tests/behat/switch_views.feature @@ -48,7 +48,7 @@ Feature: We can change what we are viewing on the grader report And I should see "Manual grade" And I should see "Course total" And the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | -5- | -6- | + | -1- | -2- | -3- | -4- | -5- | -6- | | Student 1 | student1@example.com | 80 | 90 | 30 | 170 | And I click on grade item menu "Course 1" of type "course" on "grader" page And I choose "Show totals only" in the open action menu @@ -57,7 +57,7 @@ Feature: We can change what we are viewing on the grader report And I should not see "Manual grade" And I should see "Course total" And the following should exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | Student 1 | student1@example.com | 170 | And I click on grade item menu "Course 1" of type "course" on "grader" page And I click on "Show grades only" "link" @@ -66,7 +66,7 @@ Feature: We can change what we are viewing on the grader report And I should see "Manual grade" And I should not see "Course total" And the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | -5- | + | -1- | -2- | -3- | -4- | -5- | | Student 1 | student1@example.com | 80 | 90 | 30 | @javascript @skip_chrome_zerosize @@ -83,7 +83,7 @@ Feature: We can change what we are viewing on the grader report And I should see "Manual grade" And I should see "Course total" And the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | -5- | -6- | + | -1- | -2- | -3- | -4- | -5- | -6- | | Student 1 | student1@example.com | 80 | - | 30 | 105.71 | And I click on grade item menu "Course 1" of type "course" on "grader" page And I choose "Show totals only" in the open action menu @@ -92,7 +92,7 @@ Feature: We can change what we are viewing on the grader report And I should not see "Manual grade" And I should see "Course total" And the following should exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | Student 1 | student1@example.com | 105.71 | And I click on grade item menu "Course 1" of type "course" on "grader" page When I click on "Show grades only" "link" @@ -101,5 +101,5 @@ Feature: We can change what we are viewing on the grader report And I should see "Manual grade" And I should not see "Course total" And the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | -5- | + | -1- | -2- | -3- | -4- | -5- | | Student 1 | student1@example.com | 80 | - | 30 | diff --git a/grade/report/grader/tests/behat/tertiary_name_filter.feature b/grade/report/grader/tests/behat/tertiary_name_filter.feature index ae82f47d565b6..f962bf861eb3b 100644 --- a/grade/report/grader/tests/behat/tertiary_name_filter.feature +++ b/grade/report/grader/tests/behat/tertiary_name_filter.feature @@ -156,10 +156,10 @@ Feature: Within the grader report, test that we can open our generic filter drop When I press "Apply" And the field "Search users" matches value "Student 1" Then the following should exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | Student 1 | student1@example.com | - | And the following should not exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | Teacher 1 | teacher1@example.com | - | | Dummy User | student2@example.com | - | | User Example | student3@example.com | - | @@ -169,7 +169,7 @@ Feature: Within the grader report, test that we can open our generic filter drop And I select "M" in the "First name" "core_grades > initials bar" And I press "Apply" And the following should not exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | Student 1 | student1@example.com | - | | Teacher 1 | teacher1@example.com | - | | Dummy User | student2@example.com | - | @@ -181,12 +181,12 @@ Feature: Within the grader report, test that we can open our generic filter drop Given I set the field "Search users" to "User" And I click on "View all results (3)" "option_role" And the following should exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | User Example | student3@example.com | - | | User Test | student4@example.com | - | | Dummy User | student2@example.com | - | And the following should not exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | Student 1 | student1@example.com | - | | Teacher 1 | teacher1@example.com | - | | Turtle Manatee | student5@example.com | - | @@ -194,10 +194,10 @@ Feature: Within the grader report, test that we can open our generic filter drop And I select "E" in the "Last name" "core_grades > initials bar" And I press "Apply" Then the following should exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | User Example | student3@example.com | - | And the following should not exist in the "user-grades" table: - | -1- | -1- | -3- | + | -1- | -2- | -3- | | Student 1 | student1@example.com | - | | Teacher 1 | teacher1@example.com | - | | Dummy User | student2@example.com | - | diff --git a/grade/report/grader/tests/behat/tertiary_navigation_searching.feature b/grade/report/grader/tests/behat/tertiary_navigation_searching.feature index b9319b3f7dea9..3f787e7a89c2b 100644 --- a/grade/report/grader/tests/behat/tertiary_navigation_searching.feature +++ b/grade/report/grader/tests/behat/tertiary_navigation_searching.feature @@ -274,6 +274,8 @@ Feature: Within the grader report, test that we can search for users And I press the up key And I press the enter key And I wait to be redirected + # Sometimes with behat we get unattached nodes causing spurious failures. + And I wait "1" seconds And the following should exist in the "user-grades" table: | -1- | | Dummy User | @@ -284,6 +286,13 @@ Feature: Within the grader report, test that we can search for users | Teacher 1 | | Student 1 | | Turtle Manatee | + And I set the field "Search users" to "ABC" + And I wait until "Turtle Manatee" "option_role" exists + And I press the tab key + And the focused element is "Clear search input" "button" + And I press the enter key + And I wait until the page is ready + And I confirm "Turtle Manatee" in "user" search within the gradebook widget does not exist Scenario: Once a teacher searches, it'll apply the currently set filters and inform the teacher as such # Set up a basic filtering case. diff --git a/grade/report/history/tests/behat/basic_functionality.feature b/grade/report/history/tests/behat/basic_functionality.feature index 0889b4e176b43..8e2ca357014fa 100644 --- a/grade/report/history/tests/behat/basic_functionality.feature +++ b/grade/report/history/tests/behat/basic_functionality.feature @@ -51,7 +51,7 @@ Feature: A teacher checks the grade history report in a course And I navigate to "View > Grade history" in the course gradebook When I press "Submit" Then the following should exist in the "gradereport_history" table: - | First name/Last name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader | + | First name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader | | Student 1 | student1@example.com | apple | The greatest assignment ever | | 50.00 | Teacher 1 | | Student 1 | student1@example.com | apple | Rewarding assignment | | 60.00 | Teacher 1 | | Student 2 | student2@example.com | orange | The greatest assignment ever | | 50.00 | Teacher 1 | @@ -79,7 +79,7 @@ Feature: A teacher checks the grade history report in a course And I press "Finish selecting users" And I press "Submit" And the following should exist in the "gradereport_history" table: - | First name/Last name | Grade item | Original grade | Revised grade | Grader | + | First name | Grade item | Original grade | Revised grade | Grader | | Student 1 | The greatest assignment ever | | 50.00 | Teacher 1 | | Student 1 | Rewarding assignment | | 60.00 | Teacher 1 | | Student 1 | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 | @@ -96,7 +96,7 @@ Feature: A teacher checks the grade history report in a course And I set the field "Grade item" to "The greatest assignment ever" And I press "Submit" And the following should exist in the "gradereport_history" table: - | First name/Last name | Grade item | Original grade | Revised grade | Grader | + | First name | Grade item | Original grade | Revised grade | Grader | | Student 1 | The greatest assignment ever | | 50.00 | Teacher 1 | | Student 1 | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 | And the following should not exist in the "gradereport_history" table: @@ -106,7 +106,7 @@ Feature: A teacher checks the grade history report in a course And I set the field "Grader" to "Teacher 1" And I press "Submit" And the following should exist in the "gradereport_history" table: - | First name/Last name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader | + | First name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader | | Student 1 | student1@example.com | apple | The greatest assignment ever | | 50.00 | Teacher 1 | And the following should not exist in the "gradereport_history" table: | Student 1 | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 | @@ -114,5 +114,5 @@ Feature: A teacher checks the grade history report in a course And I click on "id_revisedonly" "checkbox" And I press "Submit" And the following should exist in the "gradereport_history" table: - | First name/Last name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader | + | First name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader | | Student 1 | student1@example.com | apple | The greatest assignment ever | | 50.00 | Teacher 1 | diff --git a/grade/report/singleview/tests/behat/singleview.feature b/grade/report/singleview/tests/behat/singleview.feature index 1cb9f29b24c01..5475a4204fb5c 100644 --- a/grade/report/singleview/tests/behat/singleview.feature +++ b/grade/report/singleview/tests/behat/singleview.feature @@ -93,7 +93,7 @@ Feature: We can use Single view And I press "Save" Then I should see "Grades were set for 1 items" And the following should exist in the "generaltable" table: - | First name (Alternate name) Last name | Grade | + | User full name | Grade | | Ann, Jill, Grainne, Beauchamp | Very good | And I am on the "Course 1" "grades > Single view > View" page logged in as "teacher2" And I click on "Users" "link" in the ".page-toggler" "css_element" diff --git a/grade/tests/behat/grade_import.feature b/grade/tests/behat/grade_import.feature index a5ea6f6feb3a7..f468d4cefbd70 100644 --- a/grade/tests/behat/grade_import.feature +++ b/grade/tests/behat/grade_import.feature @@ -62,7 +62,7 @@ Feature: An admin can import grades into gradebook using a CSV file And I should see "Grade import success" And I click on "Continue" "button" Then the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | + | -1- | -2- | -3- | -4- | | Student 1 | student1@example.com | 400.00 | 400.00 | | Student 2 | student2@example.com | 50.00 | 50.00 | | Student 3 | student3@example.com | 50.00 | 50.00 | diff --git a/grade/tests/behat/grade_letter_boundary.feature b/grade/tests/behat/grade_letter_boundary.feature index 2cf197229302a..7265ffbbe49c8 100644 --- a/grade/tests/behat/grade_letter_boundary.feature +++ b/grade/tests/behat/grade_letter_boundary.feature @@ -37,5 +37,5 @@ Feature: We can customise the letter boundary of a course. And I am on "Course 1" course homepage with editing mode off And I navigate to "View > Grader report" in the course gradebook Then the following should exist in the "user-grades" table: - | -1- | -1- |-3- | -4- | + | -1- | -2- |-3- | -4- | | Student 1 | student1@example.com | D | D | diff --git a/grade/tests/behat/grade_letter_boundary_20160518.feature b/grade/tests/behat/grade_letter_boundary_20160518.feature index 9c6636e983733..a984da8d62b4d 100644 --- a/grade/tests/behat/grade_letter_boundary_20160518.feature +++ b/grade/tests/behat/grade_letter_boundary_20160518.feature @@ -38,5 +38,5 @@ Feature: We can customise the letter boundary of a course in gradebook version 2 And I am on "Course 1" course homepage with editing mode off And I navigate to "View > Grader report" in the course gradebook Then the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | + | -1- | -2- | -3- | -4- | | Student 1 | student1@example.com | F | F | diff --git a/grade/tests/behat/grade_regrade_do_not_override.feature b/grade/tests/behat/grade_regrade_do_not_override.feature index d20e68ed24c96..0dd89994422b9 100644 --- a/grade/tests/behat/grade_regrade_do_not_override.feature +++ b/grade/tests/behat/grade_regrade_do_not_override.feature @@ -32,8 +32,8 @@ Feature: Regrading grades does not unnecessarily mark some as overriden And I press "Save changes" And I am on the "Course 1" "grades > Grader report > View" page And the following should exist in the "gradereport-grader-table" table: - | | | | - | First name / Last name | Assignment 1 | Course total | + | -1- | -3- | -4- | + | First name | Assignment 1 | Course total | | Student 1 | 80.00 | 80.00 | | Student 2 | 60.00 | 60.00 | And I turn editing mode on @@ -64,7 +64,7 @@ Feature: Regrading grades does not unnecessarily mark some as overriden When I am on the "Course 1" "grades > Grader report > View" page And I turn editing mode off Then the following should exist in the "gradereport-grader-table" table: - | | | | - | First name / Last name | Assignment 1 | Course total | + | -1- | -3- | -4- | + | First name | Assignment 1 | Course total | | Student 1 | 90.00 | 180.00 | | Student 2 | 70.00 | 160.00 | diff --git a/grade/tests/behat/grade_scales.feature b/grade/tests/behat/grade_scales.feature index 64d126751981b..fee7dd54f357a 100644 --- a/grade/tests/behat/grade_scales.feature +++ b/grade/tests/behat/grade_scales.feature @@ -73,7 +73,7 @@ Feature: View gradebook when scales are used Scenario: Test displaying scales in gradebook in aggregation method Natural When I turn editing mode off Then the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | -5- | + | -1- | -2- | -3- | -4- | -5- | | Student 1 | student1@example.com | A | 5.00 | 5.00 | | Student 2 | student2@example.com | B | 4.00 | 4.00 | | Student 3 | student3@example.com | C | 3.00 | 3.00 | @@ -116,7 +116,7 @@ Feature: View gradebook when scales are used | Minimum grade | 1 | And I turn editing mode off Then the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | -5- | + | -1- | -2- | -3- | -4- | -5- | | Student 1 | student1@example.com | A | 5.00 | | | Student 2 | student2@example.com | B | 4.00 | | | Student 3 | student3@example.com | C | 3.00 | | diff --git a/grade/tests/behat/grade_single_item_scales.feature b/grade/tests/behat/grade_single_item_scales.feature index 18889649ce487..16024ed456d80 100644 --- a/grade/tests/behat/grade_single_item_scales.feature +++ b/grade/tests/behat/grade_single_item_scales.feature @@ -58,7 +58,7 @@ Feature: View gradebook when single item scales are used Scenario: Test displaying single item scales in gradebook in aggregation method Natural When I turn editing mode off Then the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | -5- | + | -1- | -2- | -3- | -4- | -5- | | Student 1 | student1@example.com | Ace! | 1.00 | 1.00 | And the following should exist in the "user-grades" table: | -1- | -2- | -3- | -4- | @@ -92,7 +92,7 @@ Feature: View gradebook when single item scales are used | Category name | Sub category () | And I turn editing mode off Then the following should exist in the "user-grades" table: - | -1- | -1- | -3- | -4- | -5- | + | -1- | -2- | -3- | -4- | -5- | | Student 1 | student1@example.com | Ace! | | | | Student 2 | student2@example.com | - | - | - | And the following should exist in the "user-grades" table: diff --git a/grade/tests/behat/toggle_grade_categories.feature b/grade/tests/behat/toggle_grade_categories.feature index 3eed4f6f0f3df..0e2e47445f0f1 100644 --- a/grade/tests/behat/toggle_grade_categories.feature +++ b/grade/tests/behat/toggle_grade_categories.feature @@ -78,14 +78,12 @@ Feature: Teachers can toggle the visibility of the grade categories in the Grade | Course | And I should see "Course" in the "setup-grades" "table" And "Expand" "link" should exist in the "Course" "table_row" - And the following should not exist in the "setup-grades" table: - | Name | - | Test assignment one | - | Category 1 | - | Test assignment two | - | Manual grade | - | Category 1 total | - | Course total | + And I should not see "Test assignment one" in the "setup-grades" "table" + And I should not see "Category 1" in the "setup-grades" "table" + And I should not see "Test assignment two" in the "setup-grades" "table" + And I should not see "Manual grade" in the "setup-grades" "table" + And I should not see "Category 1 total" in the "setup-grades" "table" + And I should not see "Course total" in the "setup-grades" "table" # Expand the grade category 'Course'. 'Category 1' should be still collapsed. And I click on "Expand" "link" in the "Course" "table_row" And the following should exist in the "setup-grades" table: @@ -96,11 +94,9 @@ Feature: Teachers can toggle the visibility of the grade categories in the Grade | Course total | And "Collapse" "link" should exist in the "Course" "table_row" And "Expand" "link" should exist in the "Category 1" "table_row" - And the following should not exist in the "setup-grades" table: - | Name | - | Test assignment two | - | Manual grade | - | Category 1 total | + And I should not see "Test assignment two" in the "setup-grades" "table" + And I should not see "Manual grade" in the "setup-grades" "table" + And I should not see "Category 1 total" in the "setup-grades" "table" Scenario: A teacher can see the aggregated max grade for a grade category even when the category is collapsed Given the following should exist in the "setup-grades" table: @@ -150,11 +146,9 @@ Feature: Teachers can toggle the visibility of the grade categories in the Grade | Category 1 | And "Collapse" "link" should exist in the "Course" "table_row" And "Expand" "link" should exist in the "Category 1" "table_row" - And the following should not exist in the "setup-grades" table: - | Name | - | Test assignment two | - | Manual grade | - | Category 1 total | + And I should not see "Test assignment two" in the "setup-grades" "table" + And I should not see "Manual grade" in the "setup-grades" "table" + And I should not see "Category 1 total" in the "setup-grades" "table" # Expand the grade category 'Category 1'. And I click on "Expand" "link" in the "Category 1" "table_row" And the following should exist in the "setup-grades" table: @@ -172,12 +166,10 @@ Feature: Teachers can toggle the visibility of the grade categories in the Grade And I click on "Collapse" "link" in the "Course" "table_row" And I should see "Course" in the "setup-grades" "table" And "Expand" "link" should exist in the "Course" "table_row" - And the following should not exist in the "setup-grades" table: - | Name | - | Test assignment one | - | Category 1 | - | Test assignment two | - | Manual grade | + And I should not see "Test assignment one" in the "setup-grades" "table" + And I should not see "Category 1" in the "setup-grades" "table" + And I should not see "Test assignment two" in the "setup-grades" "table" + And I should not see "Manual grade" in the "setup-grades" "table" # Expand the grade category 'Course'. 'Category 1' should be still collapsed. And I click on "Expand" "link" in the "Course" "table_row" And the following should exist in the "setup-grades" table: @@ -187,11 +179,9 @@ Feature: Teachers can toggle the visibility of the grade categories in the Grade | Category 1 | And "Collapse" "link" should exist in the "Course" "table_row" And "Expand" "link" should exist in the "Category 1" "table_row" - And the following should not exist in the "setup-grades" table: - | Name | - | Test assignment two | - | Manual grade | - | Category 1 total | + And I should not see "Test assignment two" in the "setup-grades" "table" + And I should not see "Manual grade" in the "setup-grades" "table" + And I should not see "Category 1 total" in the "setup-grades" "table" Scenario: Previously collapsed categories are still shown as collapsed when a teacher navigates back to Gradebook setup # Collapse the grade category 'Category 1' and navigate to the course homepage. @@ -206,11 +196,9 @@ Feature: Teachers can toggle the visibility of the grade categories in the Grade | Course total | And "Collapse" "link" should exist in the "Course" "table_row" And "Expand" "link" should exist in the "Category 1" "table_row" - And the following should not exist in the "setup-grades" table: - | Name | - | Test assignment two | - | Manual grade | - | Category 1 total | + And I should not see "Test assignment two" in the "setup-grades" "table" + And I should not see "Manual grade" in the "setup-grades" "table" + And I should not see "Category 1 total" in the "setup-grades" "table" Scenario: Previously collapsed categories are still shown as collapsed when a teacher is moving grade items in Gradebook setup # Collapse the grade category 'Category 1'. @@ -224,11 +212,9 @@ Feature: Teachers can toggle the visibility of the grade categories in the Grade | Category 1 | And "Collapse" "link" should exist in the "Course" "table_row" And "Expand" "link" should exist in the "Category 1" "table_row" - And the following should not exist in the "setup-grades" table: - | Name | - | Test assignment two | - | Manual grade | - | Category 1 total | + And I should not see "Test assignment two" in the "setup-grades" "table" + And I should not see "Manual grade" in the "setup-grades" "table" + And I should not see "Category 1 total" in the "setup-grades" "table" Scenario: Grade categories are shown as collapsed only to the teacher that collapsed them # Collapse the grade category 'Category 1'. @@ -256,8 +242,6 @@ Feature: Teachers can toggle the visibility of the grade categories in the Grade | Course total | And "Collapse" "link" should exist in the "Course" "table_row" And "Expand" "link" should exist in the "Category 1" "table_row" - And the following should not exist in the "setup-grades" table: - | Name | - | Test assignment two | - | Manual grade | - | Category 1 total | + And I should not see "Test assignment two" in the "setup-grades" "table" + And I should not see "Manual grade" in the "setup-grades" "table" + And I should not see "Category 1 total" in the "setup-grades" "table" diff --git a/group/tests/behat/private_groups.feature b/group/tests/behat/private_groups.feature index 87f9db427f1ac..3d8b665523946 100644 --- a/group/tests/behat/private_groups.feature +++ b/group/tests/behat/private_groups.feature @@ -56,7 +56,7 @@ Feature: Private groups Scenario: Participants in "Visible" groups see their membership and other members: Given I am on the "C1" "enrolled users" page logged in as "student1" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | No groups | | Student 3 | No groups | @@ -69,7 +69,7 @@ Feature: Private groups Scenario: Participants in "Only visible to members" groups see their membership and other members, plus "Visible" Given I am on the "C1" "enrolled users" page logged in as "student2" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | Only visible to members/Non-Participation, Only visible to members/Participation | | Student 3 | No groups | @@ -82,7 +82,7 @@ Feature: Private groups Scenario: Participants in "Only see own membership" groups see their membership but not other members, plus "Visible" Given I am on the "C1" "enrolled users" page logged in as "student3" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | No groups | | Student 3 | Only see own membership | @@ -95,7 +95,7 @@ Feature: Private groups Scenario: Participants in "Not visible" groups do not see that group, do see "Visible" Given I am on the "C1" "enrolled users" page logged in as "student4" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | No groups | | Student 3 | No groups | @@ -108,7 +108,7 @@ Feature: Private groups Scenario: View participants list as a teacher: Given I am on the "C1" "enrolled users" page logged in as "teacher1" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | Only visible to members/Non-Participation, Only visible to members/Participation | | Student 3 | Only see own membership | @@ -125,10 +125,10 @@ Feature: Private groups And I set the field "Type or select..." to "Only see own membership" And I click on "Apply filters" "button" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 3 | Only see own membership | And the following should not exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 7 | No groups | @javascript @@ -138,7 +138,7 @@ Feature: Private groups And I set the field "Type or select..." to "No group" And I click on "Apply filters" "button" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 2 | No groups | | Student 4 | No groups | | Student 6 | No groups | @@ -153,7 +153,7 @@ Feature: Private groups And I set the field "Type or select..." to "Only see own membership" And I click on "Apply filters" "button" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 2 | No groups | | Student 4 | No groups | @@ -170,7 +170,7 @@ Feature: Private groups And I set the field "Type or select..." to "No group" And I click on "Apply filters" "button" Then the following should exist in the "participants" table: - | First name / Surname | Groups | + | First name | Groups | | Student 1 | Visible/Non-Participation, Visible/Participation | | Student 3 | Only see own membership | | Student 5 | Visible/Non-Participation, Visible/Participation | diff --git a/mod/forum/report/summary/tests/behat/course_summary.feature b/mod/forum/report/summary/tests/behat/course_summary.feature index 4357c493bcaed..c805c704a88aa 100644 --- a/mod/forum/report/summary/tests/behat/course_summary.feature +++ b/mod/forum/report/summary/tests/behat/course_summary.feature @@ -60,12 +60,12 @@ Feature: Course level forum summary report And I should see "Export posts" And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | - | First name / Last name | -3- | -4- | Earliest post | Most recent post | + | First name | -3- | -4- | Earliest post | Most recent post | | Student 1 | 1 | 1 | Thursday, 28 March 2019, 11:50 | Thursday, 6 June 2019, 6:40 | | Student 2 | 0 | 0 | - | - | | Teacher 1 | 1 | 2 | Wednesday, 27 March 2019, 12:10 | Wednesday, 10 July 2019, 9:30 | And the following should not exist in the "forumreport_summary_table" table: - | First name / Last name | + | First name | | Student 3 | And the "Forum selected" select box should contain "All forums in course" And the "Forum selected" select box should contain "forum1" @@ -76,13 +76,13 @@ Feature: Course level forum summary report And I should not see "Export posts" And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | - | First name / Last name | -3- | -4- | Earliest post | Most recent post | + | First name | -3- | -4- | Earliest post | Most recent post | | Student 1 | 2 | 3 | Thursday, 25 January 2018, 4:40 | Saturday, 25 January 2020, 11:50 | | Student 2 | 0 | 0 | - | - | | Teacher 1 | 4 | 3 | Sunday, 14 January 2018, 9:00 | Thursday, 26 December 2019, 9:30 | And the following should not exist in the "forumreport_summary_table" table: - | First name / Last name | - | Student 3 | + | First name | + | Student 3 | Scenario: Course forum summary report correctly formats forum activity names Given the "multilang" filter is "on" @@ -104,10 +104,10 @@ Feature: Course level forum summary report And I navigate to "Reports" in current page administration And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | - | First name / Last name | -2- | -3- | Earliest post | Most recent post | + | First name | -2- | -3- | Earliest post | Most recent post | | Student 1 | 0 | 1 | Thursday, 25 January 2018, 4:40 | Thursday, 25 January 2018, 4:40 | And the following should not exist in the "forumreport_summary_table" table: - | First name / Last name | + | First name | | Student 2 | | Student 3 | | Teacher 1 | @@ -119,10 +119,10 @@ Feature: Course level forum summary report Then I select "All forums in course" from the "Forum selected" singleselect And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | - | First name / Last name | -2- | -3- | Earliest post | Most recent post | + | First name | -2- | -3- | Earliest post | Most recent post | | Student 1 | 2 | 3 | Thursday, 25 January 2018, 4:40 | Saturday, 25 January 2020, 11:50 | And the following should not exist in the "forumreport_summary_table" table: - | First name / Last name | + | First name | | Student 2 | | Student 3 | | Teacher 1 | diff --git a/mod/forum/report/summary/tests/behat/summary_filter_groups.feature b/mod/forum/report/summary/tests/behat/summary_filter_groups.feature index 3c24a189f8b9c..b57692e7da410 100644 --- a/mod/forum/report/summary/tests/behat/summary_filter_groups.feature +++ b/mod/forum/report/summary/tests/behat/summary_filter_groups.feature @@ -67,7 +67,7 @@ Feature: Groups report filter is available if groups exist Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | @@ -103,7 +103,7 @@ Feature: Groups report filter is available if groups exist And "Groups (all)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | @@ -115,7 +115,7 @@ Feature: Groups report filter is available if groups exist Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | @@ -128,7 +128,7 @@ Feature: Groups report filter is available if groups exist And "Groups (3)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 1 | 2 | And I should not see "Student 2" @@ -137,7 +137,7 @@ Feature: Groups report filter is available if groups exist And "Groups (3)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 1 | 2 | And I should not see "Student 2" @@ -149,7 +149,7 @@ Feature: Groups report filter is available if groups exist Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 2 | | Teacher 1 | 3 | 1 | @@ -161,7 +161,7 @@ Feature: Groups report filter is available if groups exist And "Groups (2)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 1 | | Teacher 1 | 2 | 1 | @@ -172,7 +172,7 @@ Feature: Groups report filter is available if groups exist And I navigate to "Reports" in current page administration Then the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 2 | | Teacher 1 | 3 | 1 | @@ -183,7 +183,7 @@ Feature: Groups report filter is available if groups exist And "Groups (1)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 1 | | Teacher 1 | 1 | 0 | @@ -196,7 +196,7 @@ Feature: Groups report filter is available if groups exist Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | @@ -225,7 +225,7 @@ Feature: Groups report filter is available if groups exist And "Groups (2)" "button" should exist Then the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 2 | 3 | And I should not see "Student 2" @@ -234,7 +234,7 @@ Feature: Groups report filter is available if groups exist And "Groups (2)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | - | First name / Last name | -3- | -4- | + | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 2 | 3 | And I should not see "Student 2" diff --git a/mod/forum/report/summary/tests/behat/summary_filter_no_groups.feature b/mod/forum/report/summary/tests/behat/summary_filter_no_groups.feature index 87f4f237da78c..ee5963e132d4f 100644 --- a/mod/forum/report/summary/tests/behat/summary_filter_no_groups.feature +++ b/mod/forum/report/summary/tests/behat/summary_filter_no_groups.feature @@ -54,7 +54,7 @@ Feature: Groups report filter is not available if no groups exist Then "Groups" "button" should not exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | - | First name / Last name | -3- | + | First name | -3- | | Teacher 1 | 2 | | Student 1 | 1 | | Student 2 | 0 | diff --git a/report/configlog/tests/behat/view_report.feature b/report/configlog/tests/behat/view_report.feature index 0be890c5d9b76..b1a0650e6782b 100644 --- a/report/configlog/tests/behat/view_report.feature +++ b/report/configlog/tests/behat/view_report.feature @@ -17,7 +17,7 @@ Feature: In a report, admin can see configuration changes Scenario: Display configuration changes report When I navigate to "Reports > Config changes" in site administration Then the following should exist in the "reportbuilder-table" table: - | User | Plugin | Setting | New value | Original value | + | First name | Plugin | Setting | New value | Original value | | Admin User | quiz | initialnumfeedbacks | 5 | 2 | | Admin User | folder | maxsizetodownload | 2048 | 0 | | Admin User | core | defaultcity | Perth | | diff --git a/reportbuilder/tests/behat/audience.feature b/reportbuilder/tests/behat/audience.feature index 115f08db8913b..887d58bec36db 100644 --- a/reportbuilder/tests/behat/audience.feature +++ b/reportbuilder/tests/behat/audience.feature @@ -250,7 +250,7 @@ Feature: Configure access to reports based on intended audience And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" When I click on the "Access" dynamic tab Then the following should exist in the "reportbuilder-table" table: - | -0- | Email address | Fruit | + | -1- | Email address | Fruit | | User 1 | user1@example.com | Apple | | User 2 | user2@example.com | Banana | | User 3 | user3@example.com | Banana | diff --git a/user/tests/behat/table_sorting.feature b/user/tests/behat/table_sorting.feature index 02b76201d8c7c..1626c812627cc 100644 --- a/user/tests/behat/table_sorting.feature +++ b/user/tests/behat/table_sorting.feature @@ -20,7 +20,7 @@ Feature: Tables can be sorted by additional names Scenario: All user names are show and sortable in the administration user list. Given I navigate to "Users > Accounts > Browse list of users" in site administration Then the following should exist in the "reportbuilder-table" table: - | First name / Middle name / Alternate name / Last name | Email address | + | First name | Email address | | Admin User | moodle@example.com | | Annie Faith Anne Edison | student1@example.com | | George David Gman Bradley | student2@example.com | From 913c573a798f2e8567d66f00e54b153b506e9120 Mon Sep 17 00:00:00 2001 From: Sumaiya Javed Date: Wed, 24 Apr 2024 16:36:44 +1200 Subject: [PATCH 008/119] MDL-80797 core: Hooks to add or reorder items in secondary navigation. Add the ability for a third party plugin to add or reorder nodes in the course navigation menu. This will allow secondary navigation to be overridden by using Hooks API. new file: lib/classes/hook/navigation/secondary_extend.php modified: lib/classes/navigation/views/secondary.php --- .../hook/navigation/secondary_extend.php | 53 +++++++++++++++++++ lib/classes/navigation/views/secondary.php | 4 ++ 2 files changed, 57 insertions(+) create mode 100644 lib/classes/hook/navigation/secondary_extend.php diff --git a/lib/classes/hook/navigation/secondary_extend.php b/lib/classes/hook/navigation/secondary_extend.php new file mode 100644 index 0000000000000..eaa6d96348b18 --- /dev/null +++ b/lib/classes/hook/navigation/secondary_extend.php @@ -0,0 +1,53 @@ +. + +namespace core\hook\navigation; +use core\navigation\views\secondary; + +/** + * Allows plugins to insert nodes into site secondary navigation + * + * @package core + * @author Sumaiya Javed + * @copyright 2024 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\core\attribute\label('Allows plugins to insert nodes into site secondary navigation')] +#[\core\attribute\tags('navigation')] +final class secondary_extend { + + /** + * Creates new hook. + * + * @param secondary $secondaryview secondary navigation view + */ + public function __construct( + /** + * @var secondary $secondaryview secondary navigation view + */ + public readonly secondary $secondaryview, + ) { + } + + /** + * secondary navigation view + * + * @return secondary + */ + public function get_secondaryview(): secondary { + return $this->secondaryview; + } +} diff --git a/lib/classes/navigation/views/secondary.php b/lib/classes/navigation/views/secondary.php index d9461681165a4..431af85d37d19 100644 --- a/lib/classes/navigation/views/secondary.php +++ b/lib/classes/navigation/views/secondary.php @@ -432,6 +432,10 @@ protected function load_course_navigation(?navigation_node $rootnode = null): vo self::TYPE_COURSE, null, 'coursehome'), reset($nodekeys) ); } + + // Allow plugins to add nodes to the secondary navigation. + $hook = new \core\hook\navigation\secondary_extend($this); + \core\di::get(\core\hook\manager::class)->dispatch($hook); } /** From 4e9a8c6cd71128e63701194ebff4d97091aa264a Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 7 May 2024 11:35:58 +0200 Subject: [PATCH 009/119] MDL-81885 tool_generator: add admin settings generator The tool_generator creates a testing scenario that can execute all steps from behat_data_generators (with some limitations). However, it cannot set any admin settings, which limits the tool's ability to generate many real test scenarios. --- .../classes/local/testscenario/runner.php | 39 +++++++++- .../classes/local/testscenario/steprunner.php | 28 ++++--- .../tests/behat/testscenario.feature | 5 ++ .../fixtures/testscenario/scenario.feature | 4 +- .../tests/local/testscenario/runner_test.php | 2 +- .../local/testscenario/steprunner_test.php | 74 ++++++++++--------- 6 files changed, 99 insertions(+), 53 deletions(-) diff --git a/admin/tool/generator/classes/local/testscenario/runner.php b/admin/tool/generator/classes/local/testscenario/runner.php index ea9d04562594e..8122cf5cadc5f 100644 --- a/admin/tool/generator/classes/local/testscenario/runner.php +++ b/admin/tool/generator/classes/local/testscenario/runner.php @@ -16,12 +16,15 @@ namespace tool_generator\local\testscenario; +use behat_admin; use behat_data_generators; +use behat_base; use Behat\Gherkin\Parser; use Behat\Gherkin\Lexer; use Behat\Gherkin\Keywords\ArrayKeywords; use ReflectionClass; use ReflectionMethod; +use stdClass; /** * Class to process a scenario generator file. @@ -72,6 +75,7 @@ public function include_behat_libraries() { require_once($CFG->libdir . '/behat/classes/behat_command.php'); require_once($CFG->libdir . '/behat/behat_base.php'); require_once("{$CFG->libdir}/tests/behat/behat_data_generators.php"); + require_once("{$CFG->dirroot}/admin/tests/behat/behat_admin.php"); return true; } @@ -81,6 +85,15 @@ public function include_behat_libraries() { private function load_generator() { $this->generator = new behat_data_generators(); $this->validsteps = $this->scan_generator($this->generator); + + // Set config values is not inside the general behat generators. + $extra = $this->scan_method( + new ReflectionMethod(behat_admin::class, 'the_following_config_values_are_set_as_admin'), + new behat_admin(), + ); + if ($extra) { + $this->validsteps[$extra->given] = $extra; + } } /** @@ -93,14 +106,32 @@ private function scan_generator(behat_data_generators $generator): array { $class = new ReflectionClass($generator); $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { - $given = $this->get_method_given($method); - if ($given) { - $result[$given] = $method->getName(); + $scan = $this->scan_method($method, $generator); + if ($scan) { + $result[$scan->given] = $scan; } } return $result; } + /** + * Scan a method to get the given expression tag. + * @param ReflectionMethod $method the method to scan. + * @param behat_base $behatclass the behat class instance to use. + * @return stdClass|null the method data (given, name, class). + */ + private function scan_method(ReflectionMethod $method, behat_base $behatclass): ?stdClass { + $given = $this->get_method_given($method); + if (!$given) { + return null; + } + return (object)[ + 'given' => $given, + 'name' => $method->getName(), + 'generator' => $behatclass, + ]; + } + /** * Get the given expression tag of a method. * @@ -146,7 +177,7 @@ public function parse_feature(string $content): parsedfeature { $result->add_scenario($scenario->getNodeType(), $scenario->getTitle()); $steps = $scenario->getSteps(); foreach ($steps as $step) { - $result->add_step(new steprunner($this->generator, $this->validsteps, $step)); + $result->add_step(new steprunner(null, $this->validsteps, $step)); } } } diff --git a/admin/tool/generator/classes/local/testscenario/steprunner.php b/admin/tool/generator/classes/local/testscenario/steprunner.php index e315640aad5ac..17b7834c276a6 100644 --- a/admin/tool/generator/classes/local/testscenario/steprunner.php +++ b/admin/tool/generator/classes/local/testscenario/steprunner.php @@ -16,7 +16,7 @@ namespace tool_generator\local\testscenario; -use behat_data_generators; +use behat_base; use Behat\Gherkin\Node\StepNode; /** @@ -27,8 +27,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class steprunner { - /** @var behat_data_generators the behat data generator instance. */ - private behat_data_generators $generator; + /** @var behat_base|null the behat step class instance. */ + private ?behat_base $generator = null; /** @var array the valid steps indexed by given expression tag. */ private array $validsteps; @@ -53,12 +53,14 @@ class steprunner { /** * Constructor. - * @param behat_data_generators $generator the behat data generator instance. + * @param behat_base|null $unused This does nothing, do not use it. * @param array $validsteps the valid steps indexed by given expression tag. * @param StepNode $stepnode the step node to process. */ - public function __construct(behat_data_generators $generator, array $validsteps, StepNode $stepnode) { - $this->generator = $generator; + public function __construct($unused, array $validsteps, StepNode $stepnode) { + if ($unused !== null) { + debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER); + } $this->validsteps = $validsteps; $this->stepnode = $stepnode; $this->init(); @@ -73,12 +75,13 @@ public function __construct(behat_data_generators $generator, array $validsteps, private function init() { $matches = []; $linetext = $this->stepnode->getText(); - foreach ($this->validsteps as $pattern => $method) { - if (!$this->match_given($pattern, $linetext, $matches)) { + foreach ($this->validsteps as $method) { + if (!$this->match_given($method->given, $linetext, $matches)) { continue; } - $this->method = $method; - $this->params = $this->build_method_params($method, $matches); + $this->method = $method->name; + $this->params = $this->build_method_params($method->name, $matches, $method->generator); + $this->generator = $method->generator; $this->isvalid = true; return; } @@ -89,10 +92,11 @@ private function init() { * Build the method parameters. * @param string $methodname the method name. * @param array $matches the matches. + * @param behat_base $generator the method class. * @return array the method parameters. */ - private function build_method_params($methodname, $matches) { - $method = new \ReflectionMethod($this->generator, $methodname); + private function build_method_params(string $methodname, array $matches, behat_base $generator) { + $method = new \ReflectionMethod($generator, $methodname); $params = []; foreach ($method->getParameters() as $param) { $paramname = $param->getName(); diff --git a/admin/tool/generator/tests/behat/testscenario.feature b/admin/tool/generator/tests/behat/testscenario.feature index 41f641df82b86..1a03a8bf065f5 100644 --- a/admin/tool/generator/tests/behat/testscenario.feature +++ b/admin/tool/generator/tests/behat/testscenario.feature @@ -21,6 +21,11 @@ Feature: Create testing scenarios using generators And I should see "Student Test3" And I should see "Student Test4" And I should see "Student Test5" + And I set the field "Participants tertiary navigation" to "Enrolment methods" + And I click on "Edit" "link" in the "Manual enrolments" "table_row" + And the field "Send course welcome message" matches value "No" + And I navigate to "Plugins > Enrolments > Manual enrolments" in site administration + And the field "Send course welcome message" matches value "No" @javascript Scenario: Prevent creating a testing scenario with no steps to execute diff --git a/admin/tool/generator/tests/fixtures/testscenario/scenario.feature b/admin/tool/generator/tests/fixtures/testscenario/scenario.feature index d228631ca0f2b..8993270dc694a 100644 --- a/admin/tool/generator/tests/fixtures/testscenario/scenario.feature +++ b/admin/tool/generator/tests/fixtures/testscenario/scenario.feature @@ -1,6 +1,8 @@ Feature: Prepare scenario for testing Scenario: Create course content - Given the following "course" exists: + Given the following config values are set as admin: + | sendcoursewelcomemessage | 0 | enrol_manual | + And the following "course" exists: | fullname | Course test | | shortname | C1 | | category | 0 | diff --git a/admin/tool/generator/tests/local/testscenario/runner_test.php b/admin/tool/generator/tests/local/testscenario/runner_test.php index 835abeb61b1b3..42e2471cae81a 100644 --- a/admin/tool/generator/tests/local/testscenario/runner_test.php +++ b/admin/tool/generator/tests/local/testscenario/runner_test.php @@ -46,7 +46,7 @@ public function test_parse_and_execute_feature(): void { $feature = $runner->parse_feature($contents); $this->assertEquals(2, count($feature->get_scenarios())); - $this->assertEquals(6, count($feature->get_all_steps())); + $this->assertEquals(7, count($feature->get_all_steps())); $this->assertTrue($feature->is_valid()); $result = $runner->execute($feature); diff --git a/admin/tool/generator/tests/local/testscenario/steprunner_test.php b/admin/tool/generator/tests/local/testscenario/steprunner_test.php index 08d20c567a158..9aeb6a889669d 100644 --- a/admin/tool/generator/tests/local/testscenario/steprunner_test.php +++ b/admin/tool/generator/tests/local/testscenario/steprunner_test.php @@ -57,6 +57,31 @@ private function get_step(string $step): StepNode { return $steps[0]; } + /** + * Get the list of valid behat steps for the tests. + * @return array the valid steps details. + */ + private function get_valid_steps(): array { + $generator = new behat_data_generators(); + return [ + '/^the following "(?P(?:[^"]|\\")*)" exist:$/' => (object) [ + 'name' => 'the_following_entities_exist', + 'given' => '/^the following "(?P(?:[^"]|\\")*)" exist:$/', + 'generator' => $generator, + ], + ':count :entitytype exist with the following data:' => (object) [ + 'name' => 'the_following_repeated_entities_exist', + 'given' => ':count :entitytype exist with the following data:', + 'generator' => $generator, + ], + 'the following :entitytype exists:' => (object) [ + 'name' => 'the_following_entity_exists', + 'given' => 'the following :entitytype exists:', + 'generator' => $generator, + ], + ]; + } + /** * Test for parse_feature. * @covers ::is_valid @@ -65,15 +90,9 @@ private function get_step(string $step): StepNode { * @dataProvider execute_steps_provider */ public function test_is_valid(string $step, bool $expected): void { - $generator = new behat_data_generators(); - $validsteps = [ - '/^the following "(?P(?:[^"]|\\")*)" exist:$/' => 'the_following_entities_exist', - ':count :entitytype exist with the following data:' => 'the_following_repeated_entities_exist', - 'the following :entitytype exists:' => 'the_following_entity_exists', - ]; - + $validsteps = $this->get_valid_steps(); $step = $this->get_step($step); - $steprunner = new steprunner($generator, $validsteps, $step); + $steprunner = new steprunner(null, $validsteps, $step); $this->assertEquals($expected, $steprunner->is_valid()); } @@ -91,15 +110,10 @@ public function test_execute(string $step, bool $expected): void { $this->resetAfterTest(); - $generator = new behat_data_generators(); - $validsteps = [ - '/^the following "(?P(?:[^"]|\\")*)" exist:$/' => 'the_following_entities_exist', - ':count :entitytype exist with the following data:' => 'the_following_repeated_entities_exist', - 'the following :entitytype exists:' => 'the_following_entity_exists', - ]; + $validsteps = $this->get_valid_steps(); $step = $this->get_step($step); - $steprunner = new steprunner($generator, $validsteps, $step); + $steprunner = new steprunner(null, $validsteps, $step); $this->assertFalse($steprunner->is_executed()); @@ -162,18 +176,13 @@ public function test_execute_duplicated(): void { $this->resetAfterTest(); - $generator = new behat_data_generators(); - $validsteps = [ - '/^the following "(?P(?:[^"]|\\")*)" exist:$/' => 'the_following_entities_exist', - ':count :entitytype exist with the following data:' => 'the_following_repeated_entities_exist', - 'the following :entitytype exists:' => 'the_following_entity_exists', - ]; + $validsteps = $this->get_valid_steps(); $step = $this->get_step('Given the following "course" exists: | fullname | Course test | | shortname | C1 | | category | 0 |'); - $steprunner = new steprunner($generator, $validsteps, $step); + $steprunner = new steprunner(null, $validsteps, $step); $this->assertFalse($steprunner->is_executed()); @@ -190,7 +199,7 @@ public function test_execute_duplicated(): void { ); // Execute the same course creation. - $steprunner = new steprunner($generator, $validsteps, $step); + $steprunner = new steprunner(null, $validsteps, $step); $this->assertFalse($steprunner->is_executed()); $result = $steprunner->execute(); $this->assertFalse($result); @@ -204,19 +213,14 @@ public function test_execute_duplicated(): void { * @covers ::get_arguments_string */ public function test_get_step_content(): void { - $generator = new behat_data_generators(); - $validsteps = [ - '/^the following "(?P(?:[^"]|\\")*)" exist:$/' => 'the_following_entities_exist', - ':count :entitytype exist with the following data:' => 'the_following_repeated_entities_exist', - 'the following :entitytype exists:' => 'the_following_entity_exists', - ]; - $step = $this->get_step('Given the following "course" exists: - | fullname | Course test | - | shortname | C1 | - | category | 0 | - | numsections | 3 |'); - $steprunner = new steprunner($generator, $validsteps, $step); + | fullname | Course test | + | shortname | C1 | + | category | 0 | + | numsections | 3 |'); + + $validsteps = $this->get_valid_steps(); + $steprunner = new steprunner(null, $validsteps, $step); $this->assertEquals( 'the following "course" exists:', From 306d733f5f8c1d780bb2951170d1784b55851f1e Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 25 Apr 2024 19:48:57 +0100 Subject: [PATCH 010/119] MDL-81657 libraries: upgrade to version 1.3.73 of Minify. --- lib/minify/matthiasmullie-minify/src/CSS.php | 161 ++++++++++++++---- lib/minify/matthiasmullie-minify/src/JS.php | 3 - .../matthiasmullie-minify/src/Minify.php | 20 ++- lib/thirdpartylibs.xml | 2 +- 4 files changed, 140 insertions(+), 46 deletions(-) diff --git a/lib/minify/matthiasmullie-minify/src/CSS.php b/lib/minify/matthiasmullie-minify/src/CSS.php index 09cedeee3a064..4f56320c47459 100644 --- a/lib/minify/matthiasmullie-minify/src/CSS.php +++ b/lib/minify/matthiasmullie-minify/src/CSS.php @@ -106,8 +106,8 @@ protected function moveImportsToTop($content) /** * Combine CSS from import statements. * - * Import statements will be loaded and their content merged into the original - * file, to save HTTP requests. + * \@import's will be loaded and their content merged into the original file, + * to save HTTP requests. * * @param string $source The file to combine imports for * @param string $content The CSS content to combine imports for @@ -316,7 +316,9 @@ public function execute($path = null, $parents = array()) $css = $this->replace($css); $css = $this->stripWhitespace($css); - $css = $this->shortenColors($css); + $css = $this->convertLegacyColors($css); + $css = $this->cleanupModernColors($css); + $css = $this->shortenHEXColors($css); $css = $this->shortenZeroes($css); $css = $this->shortenFontWeights($css); $css = $this->stripEmptyTags($css); @@ -480,64 +482,153 @@ protected function move(ConverterInterface $converter, $content) } /** - * Shorthand hex color codes. - * #FF0000 -> #F00. + * Shorthand HEX color codes. + * #FF0000FF -> #f00 -> red + * #FF00FF00 -> transparent. * - * @param string $content The CSS content to shorten the hex color codes for + * @param string $content The CSS content to shorten the HEX color codes for * * @return string */ - protected function shortenColors($content) + protected function shortenHexColors($content) { - $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); + // shorten repeating patterns within HEX .. + $content = preg_replace('/(?<=[: ])#([0-9a-f])\\1([0-9a-f])\\2([0-9a-f])\\3(?:([0-9a-f])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); - // remove alpha channel if it's pointless... - $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content); - $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content); + // remove alpha channel if it's pointless .. + $content = preg_replace('/(?<=[: ])#([0-9a-f]{6})ff(?=[; }])/i', '#$1', $content); + $content = preg_replace('/(?<=[: ])#([0-9a-f]{3})f(?=[; }])/i', '#$1', $content); + + // replace `transparent` with shortcut .. + $content = preg_replace('/(?<=[: ])#[0-9a-f]{6}00(?=[; }])/i', '#fff0', $content); $colors = array( + // make these more readable + '#00f' => 'blue', + '#dc143c' => 'crimson', + '#0ff' => 'cyan', + '#8b0000' => 'darkred', + '#696969' => 'dimgray', + '#ff69b4' => 'hotpink', + '#0f0' => 'lime', + '#fdf5e6' => 'oldlace', + '#87ceeb' => 'skyblue', + '#d8bfd8' => 'thistle', // we can shorten some even more by replacing them with their color name - '#F0FFFF' => 'azure', - '#F5F5DC' => 'beige', - '#A52A2A' => 'brown', - '#FF7F50' => 'coral', - '#FFD700' => 'gold', + '#f0ffff' => 'azure', + '#f5f5dc' => 'beige', + '#ffe4c4' => 'bisque', + '#a52a2a' => 'brown', + '#ff7f50' => 'coral', + '#ffd700' => 'gold', '#808080' => 'gray', '#008000' => 'green', - '#4B0082' => 'indigo', - '#FFFFF0' => 'ivory', - '#F0E68C' => 'khaki', - '#FAF0E6' => 'linen', + '#4b0082' => 'indigo', + '#fffff0' => 'ivory', + '#f0e68c' => 'khaki', + '#faf0e6' => 'linen', '#800000' => 'maroon', '#000080' => 'navy', '#808000' => 'olive', - '#CD853F' => 'peru', - '#FFC0CB' => 'pink', - '#DDA0DD' => 'plum', + '#ffa500' => 'orange', + '#da70d6' => 'orchid', + '#cd853f' => 'peru', + '#ffc0cb' => 'pink', + '#dda0dd' => 'plum', '#800080' => 'purple', - '#F00' => 'red', - '#FA8072' => 'salmon', - '#A0522D' => 'sienna', - '#C0C0C0' => 'silver', - '#FFFAFA' => 'snow', - '#D2B48C' => 'tan', - '#FF6347' => 'tomato', - '#EE82EE' => 'violet', - '#F5DEB3' => 'wheat', + '#f00' => 'red', + '#fa8072' => 'salmon', + '#a0522d' => 'sienna', + '#c0c0c0' => 'silver', + '#fffafa' => 'snow', + '#d2b48c' => 'tan', + '#008080' => 'teal', + '#ff6347' => 'tomato', + '#ee82ee' => 'violet', + '#f5deb3' => 'wheat', // or the other way around - 'WHITE' => '#fff', - 'BLACK' => '#000', + 'black' => '#000', + 'fuchsia' => '#f0f', + 'magenta' => '#f0f', + 'white' => '#fff', + 'yellow' => '#ff0', + // and also `transparent` + 'transparent' => '#fff0', ); return preg_replace_callback( '/(?<=[: ])(' . implode('|', array_keys($colors)) . ')(?=[; }])/i', function ($match) use ($colors) { - return $colors[strtoupper($match[0])]; + return $colors[strtolower($match[0])]; + }, + $content + ); + } + + /** + * Convert RGB|HSL color codes. + * rgb(255,0,0,.5) -> rgb(255 0 0 / .5). + * rgb(255,0,0) -> #f00. + * + * @param string $content The CSS content to shorten the RGB color codes for + * + * @return string + */ + protected function convertLegacyColors($content) + { + /* + https://drafts.csswg.org/css-color/#color-syntax-legacy + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl + */ + + // convert legacy color syntax + $content = preg_replace('/(rgb)a?\(\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0,1]?(?:\.[0-9]*)?)\s*\)/i', '$1($2 $3 $4 / $5)', $content); + $content = preg_replace('/(rgb)a?\(\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*,\s*([0-9]{1,3}%?)\s*\)/i', '$1($2 $3 $4)', $content); + $content = preg_replace('/(hsl)a?\(\s*([0-9]+(?:deg|grad|rad|turn)?)\s*,\s*([0-9]{1,3}%)\s*,\s*([0-9]{1,3}%)\s*,\s*([0,1]?(?:\.[0-9]*)?)\s*\)/i', '$1($2 $3 $4 / $5)', $content); + $content = preg_replace('/(hsl)a?\(\s*([0-9]+(?:deg|grad|rad|turn)?)\s*,\s*([0-9]{1,3}%)\s*,\s*([0-9]{1,3}%)\s*\)/i', '$1($2 $3 $4)', $content); + + // convert `rgb` to `hex` + $dec = '([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])'; + return preg_replace_callback( + "/rgb\($dec $dec $dec\)/i", + function ($match) { + return sprintf('#%02x%02x%02x', $match[1], $match[2], $match[3]); }, $content ); } + /** + * Cleanup RGB|HSL|HWB|LCH|LAB + * rgb(255 0 0 / 1) -> rgb(255 0 0). + * rgb(255 0 0 / 0) -> transparent. + * + * @param string $content The CSS content to cleanup HSL|HWB|LCH|LAB + * + * @return string + */ + protected function cleanupModernColors($content) + { + /* + https://drafts.csswg.org/css-color/#color-syntax-modern + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lab + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch + https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab + */ + $tag = '(rgb|hsl|hwb|(?:(?:ok)?(?:lch|lab)))'; + + // remove alpha channel if it's pointless .. + $content = preg_replace('/' . $tag . '\(\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+\/\s+1(?:(?:\.\d?)*|00%)?\s*\)/i', '$1($2 $3 $4)', $content); + + // replace `transparent` with shortcut .. + $content = preg_replace('/' . $tag . '\(\s*[^\s]+\s+[^\s]+\s+[^\s]+\s+\/\s+0(?:[\.0%]*)?\s*\)/i', '#fff0', $content); + + return $content; + } + /** * Shorten CSS font weights. * diff --git a/lib/minify/matthiasmullie-minify/src/JS.php b/lib/minify/matthiasmullie-minify/src/JS.php index d592b08bb33f5..9cf43a2e742a3 100644 --- a/lib/minify/matthiasmullie-minify/src/JS.php +++ b/lib/minify/matthiasmullie-minify/src/JS.php @@ -122,9 +122,6 @@ class JS extends Minify */ protected $operatorsAfter = array(); - /** - * {@inheritdoc} - */ public function __construct() { call_user_func_array(array('\\MatthiasMullie\Minify\\Minify', '__construct'), func_get_args()); diff --git a/lib/minify/matthiasmullie-minify/src/Minify.php b/lib/minify/matthiasmullie-minify/src/Minify.php index f77478bebb71f..29e6eb8c00af5 100644 --- a/lib/minify/matthiasmullie-minify/src/Minify.php +++ b/lib/minify/matthiasmullie-minify/src/Minify.php @@ -270,7 +270,7 @@ protected function stripMultilineComments() $minifier = $this; $callback = function ($match) use ($minifier) { $count = count($minifier->extracted); - $placeholder = '/*'.$count.'*/'; + $placeholder = '/*' . $count . '*/'; $minifier->extracted[$placeholder] = $match[0]; return $placeholder; @@ -494,14 +494,20 @@ protected function canImportFile($path) $parsed = parse_url($path); if ( // file is elsewhere - isset($parsed['host']) || + isset($parsed['host']) // file responds to queries (may change, or need to bypass cache) - isset($parsed['query']) + || isset($parsed['query']) ) { return false; } - return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); + try { + return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); + } + // catch openbasedir exceptions which are not caught by @ on is_file() + catch (\Exception $e) { + return false; + } } /** @@ -534,9 +540,9 @@ protected function openFileForWriting($path) protected function writeToFile($handler, $content, $path = '') { if ( - !is_resource($handler) || - ($result = @fwrite($handler, $content)) === false || - ($result < strlen($content)) + !is_resource($handler) + || ($result = @fwrite($handler, $content)) === false + || ($result < strlen($content)) ) { throw new IOException('The file "' . $path . '" could not be written to. Check your disk space and file permissions.'); } diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index d59a4ddc9e0ec..42014efcfcc92 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -82,7 +82,7 @@ minify/matthiasmullie-minify MatthiasMullie\Minify CSS & JavaScript minifier, in PHP - 1.3.71 + 1.3.73 MIT https://github.com/matthiasmullie/minify From 7c3b8c0a4019bdb7a7bb59f11e303040aa9de9ef Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 23 May 2024 19:04:33 +0100 Subject: [PATCH 011/119] MDL-82068 reportbuilder: re-factor SQL JOIN collection to trait. Replace identical implementation of the same in various classes with usage of the new trait. Implement constructor property promotion in modified classes at the same time, to help clean up/clarify class properties. --- reportbuilder/classes/local/entities/base.php | 40 +-------- .../classes/local/helpers/custom_fields.php | 62 +++---------- .../classes/local/helpers/join_trait.php | 65 ++++++++++++++ .../local/helpers/user_profile_fields.php | 76 ++++------------ reportbuilder/classes/local/report/column.php | 84 +++++------------- reportbuilder/classes/local/report/filter.php | 86 ++++--------------- .../tests/local/entities/base_test.php | 46 +--------- .../local/helpers/custom_fields_test.php | 51 ++++------- .../tests/local/helpers/join_trait_test.php | 86 +++++++++++++++++++ .../helpers/user_profile_fields_test.php | 41 ++++----- .../tests/local/report/column_test.php | 34 +------- .../tests/local/report/filter_test.php | 28 +----- 12 files changed, 266 insertions(+), 433 deletions(-) create mode 100644 reportbuilder/classes/local/helpers/join_trait.php create mode 100644 reportbuilder/tests/local/helpers/join_trait_test.php diff --git a/reportbuilder/classes/local/entities/base.php b/reportbuilder/classes/local/entities/base.php index a24ddaaee27a4..5ba3b1c76edb6 100644 --- a/reportbuilder/classes/local/entities/base.php +++ b/reportbuilder/classes/local/entities/base.php @@ -19,7 +19,7 @@ namespace core_reportbuilder\local\entities; use coding_exception; -use core_reportbuilder\local\helpers\database; +use core_reportbuilder\local\helpers\{database, join_trait}; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use lang_string; @@ -33,6 +33,8 @@ */ abstract class base { + use join_trait; + /** @var string $entityname Internal reference to name of entity */ private $entityname = null; @@ -45,9 +47,6 @@ abstract class base { /** @var array $tablejoinaliases Database tables that have already been joined to the report and their aliases */ private $tablejoinaliases = []; - /** @var string[] $joins List of SQL joins for the entity */ - private $joins = []; - /** @var column[] $columns List of columns for the entity */ private $columns = []; @@ -253,39 +252,6 @@ final public function has_table_join_alias(string $tablename): bool { return array_key_exists($tablename, $this->tablejoinaliases); } - /** - * Add join clause required for this entity to join to existing tables/entities - * - * @param string $join - * @return self - */ - final public function add_join(string $join): self { - $this->joins[trim($join)] = trim($join); - return $this; - } - - /** - * Add multiple join clauses required for this entity {@see add_join} - * - * @param string[] $joins - * @return self - */ - final public function add_joins(array $joins): self { - foreach ($joins as $join) { - $this->add_join($join); - } - return $this; - } - - /** - * Return entity joins - * - * @return string[] - */ - final public function get_joins(): array { - return array_values($this->joins); - } - /** * Helper method for returning joins necessary for retrieving tags related to the current entity * diff --git a/reportbuilder/classes/local/helpers/custom_fields.php b/reportbuilder/classes/local/helpers/custom_fields.php index 5ce3e6a83d867..b7ffe1e33c0a5 100644 --- a/reportbuilder/classes/local/helpers/custom_fields.php +++ b/reportbuilder/classes/local/helpers/custom_fields.php @@ -40,66 +40,32 @@ */ class custom_fields { - /** @var string $entityname Name of the entity */ - private $entityname; + use join_trait; /** @var handler $handler The handler for the customfields */ - private $handler; - - /** @var int $tablefieldalias The table alias and the field name (table.field) that matches the customfield instanceid. */ - private $tablefieldalias; - - /** @var array additional joins */ - private $joins = []; + private handler $handler; /** - * Class customfields constructor. + * Constructor * - * @param string $tablefieldalias table alias and the field name (table.field) that matches the customfield instanceid. - * @param string $entityname name of the entity in the report where we add custom fields. + * @param string $tablefieldalias The table/field alias to match the instance ID when adding columns and filters. + * @param string $entityname The entity name used when adding columns and filters. * @param string $component component name of full frankenstyle plugin name. * @param string $area name of the area (each component/plugin may define handlers for multiple areas). * @param int $itemid item id if the area uses them (usually not used). */ - public function __construct(string $tablefieldalias, string $entityname, string $component, string $area, int $itemid = 0) { - $this->tablefieldalias = $tablefieldalias; - $this->entityname = $entityname; + public function __construct( + /** @var string The table/field alias to match the instance ID when adding columns and filters */ + private readonly string $tablefieldalias, + /** @var string The entity name used when adding columns and filters */ + private readonly string $entityname, + string $component, + string $area, + int $itemid = 0, + ) { $this->handler = handler::get_handler($component, $area, $itemid); } - /** - * Additional join that is needed. - * - * @param string $join - * @return self - */ - public function add_join(string $join): self { - $this->joins[trim($join)] = trim($join); - return $this; - } - - /** - * Additional joins that are needed. - * - * @param array $joins - * @return self - */ - public function add_joins(array $joins): self { - foreach ($joins as $join) { - $this->add_join($join); - } - return $this; - } - - /** - * Return joins - * - * @return string[] - */ - private function get_joins(): array { - return array_values($this->joins); - } - /** * Get table alias for given custom field * diff --git a/reportbuilder/classes/local/helpers/join_trait.php b/reportbuilder/classes/local/helpers/join_trait.php new file mode 100644 index 0000000000000..05b8a06be379d --- /dev/null +++ b/reportbuilder/classes/local/helpers/join_trait.php @@ -0,0 +1,65 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\helpers; + +/** + * Trait for classes that expect to store SQL table joins + * + * @package core_reportbuilder + * @copyright 2024 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait join_trait { + + /** @var string[] SQL table joins */ + private array $joins = []; + + /** + * Add single SQL table join + * + * @param string $join + * @return static + */ + final public function add_join(string $join): static { + $this->joins[trim($join)] = trim($join); + return $this; + } + + /** + * Add multiple SQL table joins + * + * @param string[] $joins + * @return static + */ + final public function add_joins(array $joins): static { + foreach ($joins as $join) { + $this->add_join($join); + } + return $this; + } + + /** + * Return SQL table joins + * + * @return string[] + */ + final public function get_joins(): array { + return array_values($this->joins); + } +} diff --git a/reportbuilder/classes/local/helpers/user_profile_fields.php b/reportbuilder/classes/local/helpers/user_profile_fields.php index 36cb0276b5c5a..bb2addfc5c91f 100644 --- a/reportbuilder/classes/local/helpers/user_profile_fields.php +++ b/reportbuilder/classes/local/helpers/user_profile_fields.php @@ -44,72 +44,28 @@ */ class user_profile_fields { - /** @var array user profile fields */ - private $userprofilefields; + use join_trait; - /** @var string $entityname Name of the entity */ - private $entityname; - - /** @var int $usertablefieldalias The user table/field alias */ - private $usertablefieldalias; - - /** @var array additional joins */ - private $joins = []; + /** @var profile_field_base[] User profile fields */ + private array $userprofilefields; /** - * Class userprofilefields constructor. + * Constructor * - * @param string $usertablefieldalias The user table/field alias used when adding columns and filters. + * @param string $usertablefieldalias The table/field alias to match the user ID when adding columns and filters. * @param string $entityname The entity name used when adding columns and filters. */ - public function __construct(string $usertablefieldalias, string $entityname) { - $this->usertablefieldalias = $usertablefieldalias; - $this->entityname = $entityname; - $this->userprofilefields = $this->get_user_profile_fields(); - } - - /** - * Retrieves the list of available/visible user profile fields - * - * @return profile_field_base[] - */ - private function get_user_profile_fields(): array { - return array_filter(profile_get_user_fields_with_data(0), static function(profile_field_base $profilefield): bool { - return $profilefield->is_visible(); - }); - } - - /** - * Additional join that is needed. - * - * @param string $join - * @return self - */ - public function add_join(string $join): self { - $this->joins[trim($join)] = trim($join); - return $this; - } - - /** - * Additional joins that are needed. - * - * @param array $joins - * @return self - */ - public function add_joins(array $joins): self { - foreach ($joins as $join) { - $this->add_join($join); - } - return $this; - } - - /** - * Return joins - * - * @return string[] - */ - private function get_joins(): array { - return array_values($this->joins); + public function __construct( + /** @var string The table/field alias to match the user ID when adding columns and filters */ + private readonly string $usertablefieldalias, + /** @var string The entity name used when adding columns and filters */ + private readonly string $entityname, + ) { + // Retrieve the list of available/visible user profile fields. + $this->userprofilefields = array_filter( + profile_get_user_fields_with_data(0), + fn(profile_field_base $field) => $field->is_visible(), + ); } /** diff --git a/reportbuilder/classes/local/report/column.php b/reportbuilder/classes/local/report/column.php index dcd4b9bd94353..fa7e4e84f8ed7 100644 --- a/reportbuilder/classes/local/report/column.php +++ b/reportbuilder/classes/local/report/column.php @@ -20,8 +20,7 @@ use coding_exception; use lang_string; -use core_reportbuilder\local\helpers\aggregation; -use core_reportbuilder\local\helpers\database; +use core_reportbuilder\local\helpers\{aggregation, database, join_trait}; use core_reportbuilder\local\aggregation\base; use core_reportbuilder\local\models\column as column_model; @@ -34,6 +33,8 @@ */ final class column { + use join_trait; + /** @var int Column type is integer */ public const TYPE_INTEGER = 1; @@ -55,24 +56,12 @@ final class column { /** @var int $index Column index within a report */ private $index; - /** @var string $columnname Internal reference to name of column */ - private $columnname; - - /** @var lang_string $columntitle Used as a title for the column in reports */ - private $columntitle; - /** @var bool $hascustomcolumntitle Used to store if the column has been given a custom title */ private $hascustomcolumntitle = false; - /** @var string $entityname Name of the entity this column belongs to */ - private $entityname; - /** @var int $type Column data type (one of the TYPE_* class constants) */ private $type = self::TYPE_TEXT; - /** @var string[] $joins List of SQL joins for this column */ - private $joins = []; - /** @var array $fields */ private $fields = []; @@ -101,16 +90,16 @@ final class column { private $attributes = []; /** @var bool $available Used to know if column is available to the current user or not */ - protected $available = true; + private $available = true; /** @var bool $deprecated */ - protected $deprecated = false; + private $deprecated = false; /** @var string $deprecatedmessage */ - protected $deprecatedmessage; + private $deprecatedmessage; /** @var column_model $persistent */ - protected $persistent; + private $persistent; /** * Column constructor @@ -129,10 +118,15 @@ final class column { * this value should be the result of calling {@see get_entity_name}, however if creating columns inside reports directly * it should be the name of the entity as passed to {@see \core_reportbuilder\local\report\base::annotate_entity} */ - public function __construct(string $name, ?lang_string $title, string $entityname) { - $this->columnname = $name; - $this->columntitle = $title; - $this->entityname = $entityname; + public function __construct( + /** @var string Internal name of the column */ + private string $name, + /** @var lang_string|null Title of the column used in reports */ + private ?lang_string $title, + /** @var string Name of the entity this column belongs to */ + private readonly string $entityname, + ) { + } /** @@ -142,7 +136,7 @@ public function __construct(string $name, ?lang_string $title, string $entitynam * @return self */ public function set_name(string $name): self { - $this->columnname = $name; + $this->name = $name; return $this; } @@ -152,7 +146,7 @@ public function set_name(string $name): self { * @return mixed */ public function get_name(): string { - return $this->columnname; + return $this->name; } /** @@ -162,7 +156,7 @@ public function get_name(): string { * @return self */ public function set_title(?lang_string $title): self { - $this->columntitle = $title; + $this->title = $title; $this->hascustomcolumntitle = true; return $this; } @@ -173,7 +167,7 @@ public function set_title(?lang_string $title): self { * @return string */ public function get_title(): string { - return $this->columntitle ? (string) $this->columntitle : ''; + return $this->title ? (string) $this->title : ''; } /** @@ -251,44 +245,6 @@ public function get_type(): int { return $this->type; } - /** - * Add join clause required for this column to join to existing tables/entities - * - * This is necessary in the case where {@see add_field} is selecting data from a table that isn't otherwise queried - * - * @param string $join - * @return self - */ - public function add_join(string $join): self { - $this->joins[trim($join)] = trim($join); - return $this; - } - - /** - * Add multiple join clauses required for this column, passing each to {@see add_join} - * - * Typically when defining columns in entities, you should pass {@see \core_reportbuilder\local\report\base::get_joins} to - * this method, so that all entity joins are included in the report when your column is added to it - * - * @param string[] $joins - * @return self - */ - public function add_joins(array $joins): self { - foreach ($joins as $join) { - $this->add_join($join); - } - return $this; - } - - /** - * Return column joins - * - * @return string[] - */ - public function get_joins(): array { - return array_values($this->joins); - } - /** * Adds a field to be queried from the database that is necessary for this column * diff --git a/reportbuilder/classes/local/report/filter.php b/reportbuilder/classes/local/report/filter.php index 5a1ac9797d34d..4e11f87870631 100644 --- a/reportbuilder/classes/local/report/filter.php +++ b/reportbuilder/classes/local/report/filter.php @@ -21,7 +21,7 @@ use lang_string; use moodle_exception; use core_reportbuilder\local\filters\base; -use core_reportbuilder\local\helpers\database; +use core_reportbuilder\local\helpers\{database, join_trait}; use core_reportbuilder\local\models\filter as filter_model; /** @@ -33,17 +33,7 @@ */ final class filter { - /** @var string $filterclass */ - private $filterclass; - - /** @var string $name */ - private $name; - - /** @var lang_string $header */ - private $header; - - /** @var string $entity */ - private $entityname; + use join_trait; /** @var string $fieldsql */ private $fieldsql = ''; @@ -51,26 +41,23 @@ final class filter { /** @var array $fieldparams */ private $fieldparams = []; - /** @var string[] $joins */ - protected $joins = []; - /** @var bool $available */ - protected $available = true; + private $available = true; /** @var bool $deprecated */ - protected $deprecated = false; + private $deprecated = false; /** @var string $deprecatedmessage */ - protected $deprecatedmessage; + private $deprecatedmessage; /** @var mixed $options */ - protected $options; + private $options; /** @var array $limitoperators */ - protected $limitoperators = []; + private $limitoperators = []; /** @var filter_model $persistent */ - protected $persistent; + private $persistent; /** * Filter constructor @@ -86,22 +73,21 @@ final class filter { * @throws moodle_exception For invalid filter class */ public function __construct( - string $filterclass, - string $name, - lang_string $header, - string $entityname, + /** @var string Filter type class to use, must extend {@see base} filter class */ + private readonly string $filterclass, + /** @var string Internal name of the filter */ + private readonly string $name, + /** @var lang_string Title of the filter used in reports */ + private lang_string $header, + /** @var string Name of the entity this filter belongs to */ + private readonly string $entityname, string $fieldsql = '', - array $fieldparams = [] + array $fieldparams = [], ) { if (!class_exists($filterclass) || !is_subclass_of($filterclass, base::class)) { throw new moodle_exception('filterinvalid', 'reportbuilder', '', null, $filterclass); } - $this->filterclass = $filterclass; - $this->name = $name; - $this->header = $header; - $this->entityname = $entityname; - if ($fieldsql !== '') { $this->set_field_sql($fieldsql, $fieldparams); } @@ -163,44 +149,6 @@ public function get_unique_identifier(): string { return $this->get_entity_name() . ':' . $this->get_name(); } - /** - * Return joins - * - * @return string[] - */ - public function get_joins(): array { - return array_values($this->joins); - } - - /** - * Add join clause required for this filter to join to existing tables/entities - * - * This is necessary in the case where {@see set_field_sql} is selecting data from a table that isn't otherwise queried - * - * @param string $join - * @return self - */ - public function add_join(string $join): self { - $this->joins[trim($join)] = trim($join); - return $this; - } - - /** - * Add multiple join clauses required for this filter, passing each to {@see add_join} - * - * Typically when defining filters in entities, you should pass {@see \core_reportbuilder\local\report\base::get_joins} to - * this method, so that all entity joins are included in the report when your filter is used in it - * - * @param string[] $joins - * @return self - */ - public function add_joins(array $joins): self { - foreach ($joins as $join) { - $this->add_join($join); - } - return $this; - } - /** * Get SQL expression for the field * diff --git a/reportbuilder/tests/local/entities/base_test.php b/reportbuilder/tests/local/entities/base_test.php index 58330dbeb7216..8306df93fa825 100644 --- a/reportbuilder/tests/local/entities/base_test.php +++ b/reportbuilder/tests/local/entities/base_test.php @@ -43,7 +43,7 @@ * @copyright 2021 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class base_test extends advanced_testcase { +final class base_test extends advanced_testcase { /** * Test entity table alias @@ -192,50 +192,6 @@ public function test_set_entity_title(): void { $this->assertEquals($newtitle, $entity->get_entity_title()); } - /** - * Test adding single join - */ - public function test_add_join(): void { - $entity = new base_test_entity(); - - $tablejoin = "JOIN {course} c2 ON c2.id = c1.id"; - $entity->add_join($tablejoin); - - $this->assertEquals([$tablejoin], $entity->get_joins()); - } - - /** - * Test adding multiple joins - */ - public function test_add_joins(): void { - $entity = new base_test_entity(); - - $tablejoins = [ - "JOIN {course} c2 ON c2.id = c1.id", - "JOIN {course} c3 ON c3.id = c1.id", - ]; - $entity->add_joins($tablejoins); - - $this->assertEquals($tablejoins, $entity->get_joins()); - } - - /** - * Test adding duplicate joins - */ - public function test_add_duplicate_joins(): void { - $entity = new base_test_entity(); - - $tablejoins = [ - "JOIN {course} c2 ON c2.id = c1.id", - "JOIN {course} c3 ON c3.id = c1.id", - ]; - $entity - ->add_joins($tablejoins) - ->add_joins($tablejoins); - - $this->assertEquals($tablejoins, $entity->get_joins()); - } - /** * Test getting column */ diff --git a/reportbuilder/tests/local/helpers/custom_fields_test.php b/reportbuilder/tests/local/helpers/custom_fields_test.php index 35abb77cf5f97..759857da88b50 100644 --- a/reportbuilder/tests/local/helpers/custom_fields_test.php +++ b/reportbuilder/tests/local/helpers/custom_fields_test.php @@ -43,7 +43,7 @@ * @copyright 2021 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class custom_fields_test extends core_reportbuilder_testcase { +final class custom_fields_test extends core_reportbuilder_testcase { /** * Generate custom fields, one of each type @@ -117,49 +117,34 @@ public function test_get_columns(): void { } /** - * Test for add_join + * Test that joins added to the custom fields helper are present in its columns/filters */ public function test_add_join(): void { $this->resetAfterTest(); $customfields = $this->generate_customfields(); - // By default, we always join on the customfield data table. - $columns = $customfields->get_columns(); - $joins = $columns[0]->get_joins(); + // We always join on the customfield data table. + $columnjoins = $customfields->get_columns()[0]->get_joins(); + $this->assertCount(1, $columnjoins); + $this->assertStringStartsWith('LEFT JOIN {customfield_data}', $columnjoins[0]); - $this->assertCount(1, $joins); - $this->assertStringStartsWith('LEFT JOIN {customfield_data}', $joins[0]); + $filterjoins = $customfields->get_filters()[0]->get_joins(); + $this->assertCount(1, $filterjoins); + $this->assertStringStartsWith('LEFT JOIN {customfield_data}', $filterjoins[0]); // Add additional join. $customfields->add_join('JOIN {test} t ON t.id = id'); - $columns = $customfields->get_columns(); - $joins = $columns[0]->get_joins(); - - $this->assertCount(2, $joins); - $this->assertEquals('JOIN {test} t ON t.id = id', $joins[0]); - $this->assertStringStartsWith('LEFT JOIN {customfield_data}', $joins[1]); - } - - /** - * Test for add_joins - */ - public function test_add_joins(): void { - $this->resetAfterTest(); - - $customfields = $this->generate_customfields(); - - // Add additional joins. - $customfields->add_joins(['JOIN {test} t ON t.id = id', 'JOIN {test2} t2 ON t2.id = id']); - - $columns = $customfields->get_columns(); - $joins = $columns[0]->get_joins(); + $columnjoins = $customfields->get_columns()[0]->get_joins(); + $this->assertCount(2, $columnjoins); + $this->assertEquals('JOIN {test} t ON t.id = id', $columnjoins[0]); + $this->assertStringStartsWith('LEFT JOIN {customfield_data}', $columnjoins[1]); - $this->assertCount(3, $joins); - $this->assertEquals('JOIN {test} t ON t.id = id', $joins[0]); - $this->assertEquals('JOIN {test2} t2 ON t2.id = id', $joins[1]); - $this->assertStringStartsWith('LEFT JOIN {customfield_data}', $joins[2]); + $filterjoins = $customfields->get_filters()[0]->get_joins(); + $this->assertCount(2, $filterjoins); + $this->assertEquals('JOIN {test} t ON t.id = id', $filterjoins[0]); + $this->assertStringStartsWith('LEFT JOIN {customfield_data}', $filterjoins[1]); } /** @@ -254,7 +239,7 @@ public function test_custom_report_content_column_defaults(): void { * * @return array[] */ - public function custom_report_filter_provider(): array { + public static function custom_report_filter_provider(): array { return [ 'Filter by text custom field' => ['course:customfield_text', [ 'course:customfield_text_operator' => text::IS_EQUAL_TO, diff --git a/reportbuilder/tests/local/helpers/join_trait_test.php b/reportbuilder/tests/local/helpers/join_trait_test.php new file mode 100644 index 0000000000000..5e9dbe765adc2 --- /dev/null +++ b/reportbuilder/tests/local/helpers/join_trait_test.php @@ -0,0 +1,86 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\helpers; + +use advanced_testcase; + +/** + * Unit tests for the join trait + * + * @package core_reportbuilder + * @covers \core_reportbuilder\local\helpers\join_trait + * @copyright 2024 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class join_trait_test extends advanced_testcase { + + /** + * Test adding single join + */ + public function test_add_join(): void { + + /** @var join_trait $trait */ + $trait = $this->getObjectForTrait(join_trait::class); + $trait->add_join('JOIN {test} t ON t.id = a.id'); + + $this->assertEquals(['JOIN {test} t ON t.id = a.id'], $trait->get_joins()); + } + + /** + * Test adding single join multiple times + */ + public function test_add_join_multiple(): void { + + /** @var join_trait $trait */ + $trait = $this->getObjectForTrait(join_trait::class); + + // Add multiple joins, two of which are duplicates. + $trait->add_join('JOIN {test} t1 ON t1.id = a.id') + ->add_join('JOIN {test} t2 ON t2.id = b.id') + ->add_join('JOIN {test} t1 ON t1.id = a.id'); + + // The duplicated join is normalised away. + $this->assertEquals([ + 'JOIN {test} t1 ON t1.id = a.id', + 'JOIN {test} t2 ON t2.id = b.id', + ], $trait->get_joins()); + } + + /** + * Test adding multiple joins + */ + public function test_add_joins(): void { + + /** @var join_trait $trait */ + $trait = $this->getObjectForTrait(join_trait::class); + + // Add multiple joins, two of which are duplicates. + $trait->add_joins([ + 'JOIN {test} t1 ON t1.id = a.id', + 'JOIN {test} t2 ON t2.id = b.id', + 'JOIN {test} t1 ON t1.id = a.id', + ]); + + // The duplicated join is normalised away. + $this->assertEquals([ + 'JOIN {test} t1 ON t1.id = a.id', + 'JOIN {test} t2 ON t2.id = b.id', + ], $trait->get_joins()); + } +} diff --git a/reportbuilder/tests/local/helpers/user_profile_fields_test.php b/reportbuilder/tests/local/helpers/user_profile_fields_test.php index c8c35f9e571e6..321a460366679 100644 --- a/reportbuilder/tests/local/helpers/user_profile_fields_test.php +++ b/reportbuilder/tests/local/helpers/user_profile_fields_test.php @@ -42,7 +42,7 @@ * @copyright 2021 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class user_profile_fields_test extends core_reportbuilder_testcase { +final class user_profile_fields_test extends core_reportbuilder_testcase { /** * Generate custom profile fields, one of each type @@ -132,33 +132,34 @@ public function test_get_columns(): void { } /** - * Test for add_join + * Test that joins added to the profile fields helper are present in its columns/filters */ public function test_add_join(): void { $this->resetAfterTest(); $userprofilefields = $this->generate_userprofilefields(); - $columns = $userprofilefields->get_columns(); - $this->assertCount(1, ($columns[0])->get_joins()); - $userprofilefields->add_join('JOIN {test} t ON t.id = id'); - $columns = $userprofilefields->get_columns(); - $this->assertCount(2, ($columns[0])->get_joins()); - } + // We always join on the user info data table. + $columnjoins = $userprofilefields->get_columns()[0]->get_joins(); + $this->assertCount(1, $columnjoins); + $this->assertStringStartsWith('LEFT JOIN {user_info_data}', $columnjoins[0]); - /** - * Test for add_joins - */ - public function test_add_joins(): void { - $this->resetAfterTest(); + $filterjoins = $userprofilefields->get_filters()[0]->get_joins(); + $this->assertCount(1, $filterjoins); + $this->assertStringStartsWith('LEFT JOIN {user_info_data}', $filterjoins[0]); - $userprofilefields = $this->generate_userprofilefields(); - $columns = $userprofilefields->get_columns(); - $this->assertCount(1, ($columns[0])->get_joins()); + // Add additional join. + $userprofilefields->add_join('JOIN {test} t ON t.id = id'); - $userprofilefields->add_joins(['JOIN {test} t ON t.id = id', 'JOIN {test2} t2 ON t2.id = id']); - $columns = $userprofilefields->get_columns(); - $this->assertCount(3, ($columns[0])->get_joins()); + $columnjoins = $userprofilefields->get_columns()[0]->get_joins(); + $this->assertCount(2, $columnjoins); + $this->assertEquals('JOIN {test} t ON t.id = id', $columnjoins[0]); + $this->assertStringStartsWith('LEFT JOIN {user_info_data}', $columnjoins[1]); + + $filterjoins = $userprofilefields->get_filters()[0]->get_joins(); + $this->assertCount(2, $filterjoins); + $this->assertEquals('JOIN {test} t ON t.id = id', $filterjoins[0]); + $this->assertStringStartsWith('LEFT JOIN {user_info_data}', $filterjoins[1]); } /** @@ -262,7 +263,7 @@ public function test_custom_report_content(): void { * * @return array[] */ - public function custom_report_filter_provider(): array { + public static function custom_report_filter_provider(): array { return [ 'Filter by checkbox profile field' => ['user:profilefield_checkbox', [ 'user:profilefield_checkbox_operator' => boolean_select::CHECKED, diff --git a/reportbuilder/tests/local/report/column_test.php b/reportbuilder/tests/local/report/column_test.php index 3b29dba4dc160..8ad01c67e2098 100644 --- a/reportbuilder/tests/local/report/column_test.php +++ b/reportbuilder/tests/local/report/column_test.php @@ -32,7 +32,7 @@ * @copyright 2020 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class column_test extends advanced_testcase { +final class column_test extends advanced_testcase { /** * Test column name getter/setter @@ -112,38 +112,12 @@ public function test_type_invalid(): void { $column->set_type(-1); } - /** - * Test adding single join - */ - public function test_add_join(): void { - $column = $this->create_column('test'); - $this->assertEquals([], $column->get_joins()); - - $column->add_join('JOIN {user} u ON u.id = table.userid'); - $this->assertEquals(['JOIN {user} u ON u.id = table.userid'], $column->get_joins()); - } - - /** - * Test adding multiple joins - */ - public function test_add_joins(): void { - $tablejoins = [ - "JOIN {course} c2 ON c2.id = c1.id", - "JOIN {course} c3 ON c3.id = c1.id", - ]; - - $column = $this->create_column('test') - ->add_joins($tablejoins); - - $this->assertEquals($tablejoins, $column->get_joins()); - } - /** * Data provider for {@see test_add_field} * * @return array */ - public function add_field_provider(): array { + public static function add_field_provider(): array { return [ ['foo', '', ['foo AS c1_foo']], ['foo', 'bar', ['foo AS c1_bar']], @@ -231,7 +205,7 @@ public function test_add_field_complex_without_alias(): void { * * @return array */ - public function add_fields_provider(): array { + public static function add_fields_provider(): array { return [ ['t.foo', ['t.foo AS c1_foo']], ['t.foo bar', ['t.foo AS c1_bar']], @@ -327,7 +301,7 @@ public function test_get_groupby_sql(): void { * * @return array[] */ - public function column_type_provider(): array { + public static function column_type_provider(): array { return [ [column::TYPE_INTEGER, 42], [column::TYPE_TEXT, 'Hello'], diff --git a/reportbuilder/tests/local/report/filter_test.php b/reportbuilder/tests/local/report/filter_test.php index 299d599a11a92..4d3910e64ee97 100644 --- a/reportbuilder/tests/local/report/filter_test.php +++ b/reportbuilder/tests/local/report/filter_test.php @@ -31,7 +31,7 @@ * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class filter_test extends advanced_testcase { +final class filter_test extends advanced_testcase { /** * Test getting filter class @@ -120,32 +120,6 @@ public function test_get_field_sql_and_params(): void { $this->assertEquals(['username_1' => 'test', 'idnumber_1' => 'bar'], $params); } - /** - * Test adding single join - */ - public function test_add_join(): void { - $filter = $this->create_filter('username', 'u.username'); - $this->assertEquals([], $filter->get_joins()); - - $filter->add_join('JOIN {user} u ON u.id = table.userid'); - $this->assertEquals(['JOIN {user} u ON u.id = table.userid'], $filter->get_joins()); - } - - /** - * Test adding multiple joins - */ - public function test_add_joins(): void { - $tablejoins = [ - "JOIN {course} c2 ON c2.id = c1.id", - "JOIN {course} c3 ON c3.id = c1.id", - ]; - - $filter = $this->create_filter('username', 'u.username') - ->add_joins($tablejoins); - - $this->assertEquals($tablejoins, $filter->get_joins()); - } - /** * Test is available */ From d5316aeac6c3a5571ee31bcaebb530a934fddb43 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 7 May 2024 21:59:38 +0100 Subject: [PATCH 012/119] MDL-80344 calendar: correct no subscriptions language string use. Ensure expected parameters are present; avoid string concatenation. --- calendar/renderer.php | 8 ++++---- lang/en/calendar.php | 7 +++++-- lang/en/deprecated.txt | 2 ++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/calendar/renderer.php b/calendar/renderer.php index 5134fa007fa5c..3d184b83e213b 100644 --- a/calendar/renderer.php +++ b/calendar/renderer.php @@ -298,9 +298,8 @@ public function render_subscriptions_header(): string { */ public function render_no_calendar_subscriptions(): string { $output = html_writer::start_div('mt-5'); - $importlink = html_writer::link((new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()))->out(), - get_string('importcalendarexternal', 'calendar')); - $output .= get_string('nocalendarsubscriptions', 'calendar', $importlink); + $importlink = (new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()))->out(); + $output .= get_string('nocalendarsubscriptionsimportexternal', 'core_calendar', $importlink); $output .= html_writer::end_div(); return $output; @@ -327,7 +326,8 @@ public function subscription_details($unused, $subscriptions, $unused2 = '') { $table->id = 'subscription_details_table'; if (empty($subscriptions)) { - $cell = new html_table_cell(get_string('nocalendarsubscriptions', 'calendar')); + $importlink = (new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()))->out(); + $cell = new html_table_cell(get_string('nocalendarsubscriptionsimportexternal', 'core_calendar', $importlink)); $cell->colspan = 5; $table->data[] = new html_table_row(array($cell)); } diff --git a/lang/en/calendar.php b/lang/en/calendar.php index f922d5d46d90d..694efe548bf09 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -168,7 +168,6 @@ $string['hideeventtype'] = 'Hide {$a} events'; $string['showeventtype'] = 'Show {$a} events'; $string['hourly'] = 'Hourly'; -$string['importcalendarexternal'] = 'Import an external calendar?'; $string['importcalendar'] = 'Import calendar'; $string['importcalendarheading'] = 'Import calendar...'; $string['importcalendarfrom'] = 'Import from'; @@ -193,9 +192,9 @@ $string['never'] = 'Never'; $string['newevent'] = 'New event'; $string['newmonthannouncement'] = 'Calendar is now set to {$a}.'; +$string['nocalendarsubscriptionsimportexternal'] = 'No calendar subscriptions yet. Import an external calendar'; $string['notitle'] = 'no title'; $string['noupcomingevents'] = 'There are no upcoming events'; -$string['nocalendarsubscriptions'] = 'No calendar subscriptions yet. Do you want to {$a}'; $string['oneevent'] = '1 event'; $string['pollinterval'] = 'Update interval'; $string['pollinterval_help'] = 'How often you would like the calendar to update with new events.'; @@ -287,3 +286,7 @@ // Deprecated since Moodle 4.4. $string['coursecalendar'] = '{$a} calendar'; + +// Deprecated since Moodle 4.5. +$string['importcalendarexternal'] = 'Import an external calendar?'; +$string['nocalendarsubscriptions'] = 'No calendar subscriptions yet. Do you want to {$a}'; diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index 31c9828e288d2..b934616128dbf 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -113,4 +113,6 @@ coursesearch_help,core blocknotexist,core_debug modulenotexist,core_debug coursecalendar,core_calendar +importcalendarexternal,core_calendar +nocalendarsubscriptions,core_calendar datechanged,core From fc8dfda112bb670414be37f1daa4080286758057 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 11 Jun 2024 16:15:02 +0200 Subject: [PATCH 013/119] MDL-82167 core: fix reactive debug panel --- lib/amd/build/local/reactive/debugpanel.min.js | 2 +- lib/amd/build/local/reactive/debugpanel.min.js.map | 2 +- lib/amd/src/local/reactive/debugpanel.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/amd/build/local/reactive/debugpanel.min.js b/lib/amd/build/local/reactive/debugpanel.min.js index 82de966570bc1..d6f23b882a6b2 100644 --- a/lib/amd/build/local/reactive/debugpanel.min.js +++ b/lib/amd/build/local/reactive/debugpanel.min.js @@ -9,6 +9,6 @@ define("core/local/reactive/debugpanel",["exports","core/reactive","core/log","c * @module core/local/reactive/debugpanel * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initsubpanel=_exports.init=void 0,_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.init=(target,selectors)=>{const element=document.getElementById(target);void 0!==_reactive.debug?new GlobalDebugPanel({element:element,reactive:_reactive.debug,selectors:selectors}):element.remove()};_exports.initsubpanel=(target,selectors)=>{const element=document.getElementById(target);void 0!==_reactive.debug?new DebugInstanceSubpanel({element:element,reactive:_reactive.debug,selectors:selectors}):element.remove()};class GlobalDebugPanel extends _reactive.BaseComponent{create(){this.name="GlobalDebugPanel",this.selectors={LOADERS:"[data-for='loaders']",SUBPANEL:"[data-for='subpanel']",NOINSTANCES:"[data-for='noinstances']",LOG:"[data-for='log']"},this.classes={HIDE:"d-none"},this.subPanels=new Set}stateReady(state){this._updateReactivesPanels({state:state}),this.getElement(this.selectors.SUBPANEL).innerHTML=""}getWatchers(){return[{watch:"reactives:created",handler:this._updateReactivesPanels}]}_updateReactivesPanels(_ref){var _this$getElement,_this$getElement$clas;let{state:state}=_ref;null===(_this$getElement=this.getElement(this.selectors.NOINSTANCES))||void 0===_this$getElement||null===(_this$getElement$clas=_this$getElement.classList)||void 0===_this$getElement$clas||_this$getElement$clas.toggle(this.classes.HIDE,state.reactives.size>0),state.reactives.forEach((instance=>{this._createLoader(instance)}))}_createLoader(instance){if(this.subPanels.has(instance.id))return;this.subPanels.add(instance.id);const loaders=this.getElement(this.selectors.LOADERS),btn=document.createElement("button");btn.innerHTML=instance.id,btn.dataset.id=instance.id,loaders.appendChild(btn),this.addEventListener(btn,"click",(()=>this._openPanel(btn,instance)))}async _openPanel(btn,instance){try{const target=this.getElement(this.selectors.SUBPANEL),data={...instance};await this.renderComponent(target,"core/local/reactive/debuginstancepanel",data)}catch(error){throw _log.default.error("Cannot load reactive debug subpanel"),error}}}class DebugInstanceSubpanel extends _reactive.BaseComponent{create(){this.name="DebugInstanceSubpanel",this.selectors={NAME:"[data-for='name']",CLOSE:"[data-for='close']",READMODE:"[data-for='readmode']",HIGHLIGHT:"[data-for='highlight']",LOG:"[data-for='log']",STATE:"[data-for='state']",CLEAN:"[data-for='clean']",PIN:"[data-for='pin']",SAVE:"[data-for='save']",INVALID:"[data-for='invalid']"},this.id=this.element.dataset.id,this.controller=M.reactive[this.id],this.draggable=!1,this.relativeDrag=!0,this.strings={savewarning:""}}stateReady(){var _this$getElement$inne,_this$getElement2;this.dragdrop=new _reactive.DragDrop(this),this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this.remove),this.controller.highlight&&this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT)),this.addEventListener(this.getElement(this.selectors.HIGHLIGHT),"click",(()=>{this.controller.highlight=!this.controller.highlight,this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT))})),this.addEventListener(this.getElement(this.selectors.READMODE),"click",this._toggleEditMode),this.addEventListener(this.getElement(this.selectors.CLEAN),"click",this._cleanAreas),this.addEventListener(this.getElement(this.selectors.PIN),"click",this._togglePin),this.getElement(this.selectors.SAVE).disabled=!0,this.addEventListener(this.getElement(this.selectors.STATE),"keyup",(0,_utils.debounce)(this._checkJSON,500)),this.addEventListener(this.getElement(this.selectors.SAVE),"click",this._saveState),this.strings.savewarning=null!==(_this$getElement$inne=null===(_this$getElement2=this.getElement(this.selectors.INVALID))||void 0===_this$getElement2?void 0:_this$getElement2.innerHTML)&&void 0!==_this$getElement$inne?_this$getElement$inne:"",this._refreshState()}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}getWatchers(){return[{watch:"reactives[".concat(this.id,"].lastChanges:updated"),handler:this._refreshLog},{watch:"reactives[".concat(this.id,"].modified:updated"),handler:this._refreshState},{watch:"reactives[".concat(this.id,"].readOnly:updated"),handler:this._refreshReadOnly}]}_refreshLog(_ref2){var _element$lastChanges;let{element:element}=_ref2;const logContent=(null!==(_element$lastChanges=null==element?void 0:element.lastChanges)&&void 0!==_element$lastChanges?_element$lastChanges:[]).join("\n"),target=this.getElement(this.selectors.LOG);target.value+="\n\n= Transaction =\n ".concat(logContent),target.scrollTop=target.scrollHeight}_cleanAreas(){this.getElement(this.selectors.LOG).value="",this._refreshState()}_refreshState(){this.getElement(this.selectors.STATE).value=JSON.stringify(this.controller.state,null,4)}_refreshReadOnly(){const target=this.getElement(this.selectors.READMODE);void 0===target.dataset.readonly&&(target.dataset.readonly=target.innerHTML),this.controller.readOnly?target.innerHTML=target.dataset.readonly:target.innerHTML=target.dataset.alt}_toggleEditMode(){this.controller.readOnly=!this.controller.readOnly}_checkJSON(){const invalid=this.getElement(this.selectors.INVALID),save=this.getElement(this.selectors.SAVE),edited=this.getElement(this.selectors.STATE).value,currentStateData=this.controller.stateData;if(edited==JSON.stringify(this.controller.state,null,4))return invalid.style.color="",invalid.innerHTML="",void(save.disabled=!0);try{const newState=JSON.parse(edited),result=this._generateStateUpdates(currentStateData,newState);return invalid.style.color="",invalid.innerHTML=this.strings.savewarning,save.disabled=!1,result}catch(error){var _error$message;return invalid.style.color="red",invalid.innerHTML=null!==(_error$message=error.message)&&void 0!==_error$message?_error$message:"Invalid JSON sctructure",void(save.disabled=!0)}}_saveState(){const updates=this._checkJSON();updates&&this.controller.processUpdates(updates)}_generateStateUpdates(currentStateData,newStateData){const updates=[],ids={};for(const[key,newValue]of Object.entries(newStateData))Array.isArray(newValue)?(ids[key]={},newValue.forEach((element=>{if(void 0===element.id)throw Error("Array ".concat(key," element without id attribute"));updates.push({name:key,action:"override",fields:element});const index=String(element.id).valueOf();ids[key][index]=!0}))):updates.push({name:key,action:"override",fields:newValue});for(const[key,oldValue]of Object.entries(currentStateData)){let deleteField=!1;if(void 0===newStateData[key]&&(deleteField=!0),Array.isArray(oldValue)){if(!deleteField&&void 0===ids[key])throw Error("Array ".concat(key," cannot change to object."));oldValue.forEach((element=>{const index=String(element.id).valueOf();let deleteEntry=deleteField;deleteEntry||void 0!==ids[key][index]||(deleteEntry=!0),deleteEntry&&updates.push({name:key,action:"delete",fields:element})}))}else{if(!deleteField&&void 0!==ids[key])throw Error("Object ".concat(key," cannot change to array."));deleteField&&updates.push({name:key,action:"delete",fields:oldValue})}}return updates}getDraggableData(){return this.draggable}dragEnd(dropdata,event){this.element.style.top="".concat(event.newFixedTop,"px"),this.element.style.left="".concat(event.newFixedLeft,"px")}_togglePin(){this.draggable=!this.draggable,this.dragdrop.setDraggable(this.draggable),this.draggable?this._unpin():this._pin()}_unpin(){const pageCenterY=window.innerHeight/2,pageCenterX=window.innerWidth/2,style={position:"fixed",resize:"both",overflow:"auto",height:"400px",width:"400px",top:"".concat(pageCenterY-200,"px"),left:"".concat(pageCenterX-200,"px")};Object.assign(this.element.style,style),this.getElement(this.selectors.STATE).style.height="50px",this.getElement(this.selectors.LOG).style.height="50px",this._toggleButtonText(this.getElement(this.selectors.PIN))}_pin(){["position","resize","overflow","top","left","height","width"].forEach((prop=>this.element.style.removeProperty(prop))),this._toggleButtonText(this.getElement(this.selectors.PIN))}_toggleButtonText(element){[element.innerHTML,element.dataset.alt]=[element.dataset.alt,element.innerHTML]}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initsubpanel=_exports.init=void 0,_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.init=(target,selectors)=>{const element=document.getElementById(target);void 0!==_reactive.debug?new GlobalDebugPanel({element:element,reactive:_reactive.debug,selectors:selectors}):element.remove()};_exports.initsubpanel=(target,selectors)=>{const element=document.getElementById(target);void 0!==_reactive.debug?new DebugInstanceSubpanel({element:element,reactive:_reactive.debug,selectors:selectors}):element.remove()};class GlobalDebugPanel extends _reactive.BaseComponent{create(){this.name="GlobalDebugPanel",this.selectors={LOADERS:"[data-for='loaders']",SUBPANEL:"[data-for='subpanel']",NOINSTANCES:"[data-for='noinstances']",LOG:"[data-for='log']"},this.classes={HIDE:"d-none"},this.subPanels=new Set}stateReady(state){this._updateReactivesPanels({state:state}),this.getElement(this.selectors.SUBPANEL).innerHTML=""}getWatchers(){return[{watch:"reactives:created",handler:this._updateReactivesPanels}]}_updateReactivesPanels(_ref){var _this$getElement,_this$getElement$clas;let{state:state}=_ref;null===(_this$getElement=this.getElement(this.selectors.NOINSTANCES))||void 0===_this$getElement||null===(_this$getElement$clas=_this$getElement.classList)||void 0===_this$getElement$clas||_this$getElement$clas.toggle(this.classes.HIDE,state.reactives.size>0),state.reactives.forEach((instance=>{this._createLoader(instance)}))}_createLoader(instance){if(this.subPanels.has(instance.id))return;this.subPanels.add(instance.id);const loaders=this.getElement(this.selectors.LOADERS),btn=document.createElement("button");btn.innerHTML=instance.id,btn.dataset.id=instance.id,loaders.appendChild(btn),this.addEventListener(btn,"click",(()=>this._openPanel(btn,instance)))}async _openPanel(btn,instance){try{const target=this.getElement(this.selectors.SUBPANEL),data={...instance};await this.renderComponent(target,"core/local/reactive/debuginstancepanel",data)}catch(error){throw _log.default.error("Cannot load reactive debug subpanel"),error}}}class DebugInstanceSubpanel extends _reactive.BaseComponent{create(){this.name="DebugInstanceSubpanel",this.selectors={NAME:"[data-for='name']",CLOSE:"[data-for='close']",READMODE:"[data-for='readmode']",HIGHLIGHT:"[data-for='highlight']",LOG:"[data-for='log']",STATE:"[data-for='state']",CLEAN:"[data-for='clean']",PIN:"[data-for='pin']",SAVE:"[data-for='save']",INVALID:"[data-for='invalid']"},this.id=this.element.dataset.id,this.controller=M.reactive[this.id],this.draggable=!1,this.relativeDrag=!0,this.strings={savewarning:""}}stateReady(){var _this$getElement$inne,_this$getElement2;this.dragdrop=new _reactive.DragDrop(this),this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this.remove),this.controller.highlight&&this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT)),this.addEventListener(this.getElement(this.selectors.HIGHLIGHT),"click",(()=>{this.controller.highlight=!this.controller.highlight,this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT))})),this.addEventListener(this.getElement(this.selectors.READMODE),"click",this._toggleEditMode),this.addEventListener(this.getElement(this.selectors.CLEAN),"click",this._cleanAreas),this.addEventListener(this.getElement(this.selectors.PIN),"click",this._togglePin),this.getElement(this.selectors.SAVE).disabled=!0,this.addEventListener(this.getElement(this.selectors.STATE),"keyup",(0,_utils.debounce)(this._checkJSON.bind(this),500)),this.addEventListener(this.getElement(this.selectors.SAVE),"click",this._saveState),this.strings.savewarning=null!==(_this$getElement$inne=null===(_this$getElement2=this.getElement(this.selectors.INVALID))||void 0===_this$getElement2?void 0:_this$getElement2.innerHTML)&&void 0!==_this$getElement$inne?_this$getElement$inne:"",this._refreshState()}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}getWatchers(){return[{watch:"reactives[".concat(this.id,"].lastChanges:updated"),handler:this._refreshLog},{watch:"reactives[".concat(this.id,"].modified:updated"),handler:this._refreshState},{watch:"reactives[".concat(this.id,"].readOnly:updated"),handler:this._refreshReadOnly}]}_refreshLog(_ref2){var _element$lastChanges;let{element:element}=_ref2;const logContent=(null!==(_element$lastChanges=null==element?void 0:element.lastChanges)&&void 0!==_element$lastChanges?_element$lastChanges:[]).join("\n"),target=this.getElement(this.selectors.LOG);target.value+="\n\n= Transaction =\n ".concat(logContent),target.scrollTop=target.scrollHeight}_cleanAreas(){this.getElement(this.selectors.LOG).value="",this._refreshState()}_refreshState(){this.getElement(this.selectors.STATE).value=JSON.stringify(this.controller.state,null,4)}_refreshReadOnly(){const target=this.getElement(this.selectors.READMODE);void 0===target.dataset.readonly&&(target.dataset.readonly=target.innerHTML),this.controller.readOnly?target.innerHTML=target.dataset.readonly:target.innerHTML=target.dataset.alt}_toggleEditMode(){this.controller.readOnly=!this.controller.readOnly}_checkJSON(){const invalid=this.getElement(this.selectors.INVALID),save=this.getElement(this.selectors.SAVE),edited=this.getElement(this.selectors.STATE).value,currentStateData=this.controller.stateData;if(edited==JSON.stringify(this.controller.state,null,4))return invalid.style.color="",invalid.innerHTML="",void(save.disabled=!0);try{const newState=JSON.parse(edited),result=this._generateStateUpdates(currentStateData,newState);return invalid.style.color="",invalid.innerHTML=this.strings.savewarning,save.disabled=!1,result}catch(error){var _error$message;return invalid.style.color="red",invalid.innerHTML=null!==(_error$message=error.message)&&void 0!==_error$message?_error$message:"Invalid JSON sctructure",void(save.disabled=!0)}}_saveState(){const updates=this._checkJSON();updates&&this.controller.processUpdates(updates)}_generateStateUpdates(currentStateData,newStateData){const updates=[],ids={};for(const[key,newValue]of Object.entries(newStateData))Array.isArray(newValue)?(ids[key]={},newValue.forEach((element=>{if(void 0===element.id)throw Error("Array ".concat(key," element without id attribute"));updates.push({name:key,action:"override",fields:element});const index=String(element.id).valueOf();ids[key][index]=!0}))):updates.push({name:key,action:"override",fields:newValue});for(const[key,oldValue]of Object.entries(currentStateData)){let deleteField=!1;if(void 0===newStateData[key]&&(deleteField=!0),Array.isArray(oldValue)){if(!deleteField&&void 0===ids[key])throw Error("Array ".concat(key," cannot change to object."));oldValue.forEach((element=>{const index=String(element.id).valueOf();let deleteEntry=deleteField;deleteEntry||void 0!==ids[key][index]||(deleteEntry=!0),deleteEntry&&updates.push({name:key,action:"delete",fields:element})}))}else{if(!deleteField&&void 0!==ids[key])throw Error("Object ".concat(key," cannot change to array."));deleteField&&updates.push({name:key,action:"delete",fields:oldValue})}}return updates}getDraggableData(){return this.draggable}dragEnd(dropdata,event){this.element.style.top="".concat(event.newFixedTop,"px"),this.element.style.left="".concat(event.newFixedLeft,"px")}_togglePin(){this.draggable=!this.draggable,this.dragdrop.setDraggable(this.draggable),this.draggable?this._unpin():this._pin()}_unpin(){const pageCenterY=window.innerHeight/2,pageCenterX=window.innerWidth/2,style={position:"fixed",resize:"both",overflow:"auto",height:"400px",width:"400px",top:"".concat(pageCenterY-200,"px"),left:"".concat(pageCenterX-200,"px")};Object.assign(this.element.style,style),this.getElement(this.selectors.STATE).style.height="50px",this.getElement(this.selectors.LOG).style.height="50px",this._toggleButtonText(this.getElement(this.selectors.PIN))}_pin(){["position","resize","overflow","top","left","height","width"].forEach((prop=>this.element.style.removeProperty(prop))),this._toggleButtonText(this.getElement(this.selectors.PIN))}_toggleButtonText(element){[element.innerHTML,element.dataset.alt]=[element.dataset.alt,element.innerHTML]}}})); //# sourceMappingURL=debugpanel.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/reactive/debugpanel.min.js.map b/lib/amd/build/local/reactive/debugpanel.min.js.map index b69f3f6544023..b6841c9f46a63 100644 --- a/lib/amd/build/local/reactive/debugpanel.min.js.map +++ b/lib/amd/build/local/reactive/debugpanel.min.js.map @@ -1 +1 @@ -{"version":3,"file":"debugpanel.min.js","sources":["../../../src/local/reactive/debugpanel.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 * Reactive module debug panel.\n *\n * This module contains all the UI components for the reactive debug tools.\n * Those tools are only available if the debug is enables and could be used\n * from the footer.\n *\n * @module core/local/reactive/debugpanel\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop, debug} from 'core/reactive';\nimport log from 'core/log';\nimport {debounce} from 'core/utils';\n\n/**\n * Init the main reactive panel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const init = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new GlobalDebugPanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Init an instance reactive subpanel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const initsubpanel = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new DebugInstanceSubpanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass GlobalDebugPanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'GlobalDebugPanel';\n // Default query selectors.\n this.selectors = {\n LOADERS: `[data-for='loaders']`,\n SUBPANEL: `[data-for='subpanel']`,\n NOINSTANCES: `[data-for='noinstances']`,\n LOG: `[data-for='log']`,\n };\n this.classes = {\n HIDE: `d-none`,\n };\n // The list of loaded debuggers.\n this.subPanels = new Set();\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n stateReady(state) {\n this._updateReactivesPanels({state});\n // Remove loading wheel.\n this.getElement(this.selectors.SUBPANEL).innerHTML = '';\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `reactives:created`, handler: this._updateReactivesPanels},\n ];\n }\n\n /**\n * Update the list of reactive instances.\n * @param {Object} args\n * @param {Object} args.state the current state\n */\n _updateReactivesPanels({state}) {\n this.getElement(this.selectors.NOINSTANCES)?.classList?.toggle(\n this.classes.HIDE,\n state.reactives.size > 0\n );\n // Generate loading buttons.\n state.reactives.forEach(\n instance => {\n this._createLoader(instance);\n }\n );\n }\n\n /**\n * Create a debug panel button for a specific reactive instance.\n *\n * @param {object} instance hte instance data\n */\n _createLoader(instance) {\n if (this.subPanels.has(instance.id)) {\n return;\n }\n this.subPanels.add(instance.id);\n const loaders = this.getElement(this.selectors.LOADERS);\n const btn = document.createElement(\"button\");\n btn.innerHTML = instance.id;\n btn.dataset.id = instance.id;\n loaders.appendChild(btn);\n // Add click event.\n this.addEventListener(btn, 'click', () => this._openPanel(btn, instance));\n }\n\n /**\n * Open a debug panel.\n *\n * @param {Element} btn the button element\n * @param {object} instance the instance data\n */\n async _openPanel(btn, instance) {\n try {\n const target = this.getElement(this.selectors.SUBPANEL);\n const data = {...instance};\n await this.renderComponent(target, 'core/local/reactive/debuginstancepanel', data);\n } catch (error) {\n log.error('Cannot load reactive debug subpanel');\n throw error;\n }\n }\n}\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass DebugInstanceSubpanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'DebugInstanceSubpanel';\n // Default query selectors.\n this.selectors = {\n NAME: `[data-for='name']`,\n CLOSE: `[data-for='close']`,\n READMODE: `[data-for='readmode']`,\n HIGHLIGHT: `[data-for='highlight']`,\n LOG: `[data-for='log']`,\n STATE: `[data-for='state']`,\n CLEAN: `[data-for='clean']`,\n PIN: `[data-for='pin']`,\n SAVE: `[data-for='save']`,\n INVALID: `[data-for='invalid']`,\n };\n this.id = this.element.dataset.id;\n this.controller = M.reactive[this.id];\n\n // The component is created always pinned.\n this.draggable = false;\n // We want the element to be dragged like modal.\n this.relativeDrag = true;\n // Save warning (will be loaded when state is ready.\n this.strings = {\n savewarning: '',\n };\n }\n\n /**\n * Initial state ready method.\n *\n */\n stateReady() {\n // Enable drag and drop.\n this.dragdrop = new DragDrop(this);\n\n // Close button.\n this.addEventListener(\n this.getElement(this.selectors.CLOSE),\n 'click',\n this.remove\n );\n // Highlight button.\n if (this.controller.highlight) {\n this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));\n }\n this.addEventListener(\n this.getElement(this.selectors.HIGHLIGHT),\n 'click',\n () => {\n this.controller.highlight = !this.controller.highlight;\n this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));\n }\n );\n // Edit mode button.\n this.addEventListener(\n this.getElement(this.selectors.READMODE),\n 'click',\n this._toggleEditMode\n );\n // Clean log and state.\n this.addEventListener(\n this.getElement(this.selectors.CLEAN),\n 'click',\n this._cleanAreas\n );\n // Unpin panel butotn.\n this.addEventListener(\n this.getElement(this.selectors.PIN),\n 'click',\n this._togglePin\n );\n // Save button, state format error message and state textarea.\n this.getElement(this.selectors.SAVE).disabled = true;\n\n this.addEventListener(\n this.getElement(this.selectors.STATE),\n 'keyup',\n debounce(this._checkJSON, 500)\n );\n\n this.addEventListener(\n this.getElement(this.selectors.SAVE),\n 'click',\n this._saveState\n );\n // Save the default save warning message.\n this.strings.savewarning = this.getElement(this.selectors.INVALID)?.innerHTML ?? '';\n // Add current state.\n this._refreshState();\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `reactives[${this.id}].lastChanges:updated`, handler: this._refreshLog},\n {watch: `reactives[${this.id}].modified:updated`, handler: this._refreshState},\n {watch: `reactives[${this.id}].readOnly:updated`, handler: this._refreshReadOnly},\n ];\n }\n\n /**\n * Wtacher method to refresh the log panel.\n *\n * @param {object} args\n * @param {HTMLElement} args.element\n */\n _refreshLog({element}) {\n const list = element?.lastChanges ?? [];\n\n const logContent = list.join(\"\\n\");\n // Append last log.\n const target = this.getElement(this.selectors.LOG);\n target.value += `\\n\\n= Transaction =\\n ${logContent}`;\n target.scrollTop = target.scrollHeight;\n }\n\n /**\n * Listener method to clean the log area.\n */\n _cleanAreas() {\n let target = this.getElement(this.selectors.LOG);\n target.value = '';\n\n this._refreshState();\n }\n\n /**\n * Watcher to refresh the state information.\n */\n _refreshState() {\n const target = this.getElement(this.selectors.STATE);\n target.value = JSON.stringify(this.controller.state, null, 4);\n }\n\n /**\n * Watcher to update the read only information.\n */\n _refreshReadOnly() {\n // Toggle the read mode button.\n const target = this.getElement(this.selectors.READMODE);\n if (target.dataset.readonly === undefined) {\n target.dataset.readonly = target.innerHTML;\n }\n if (this.controller.readOnly) {\n target.innerHTML = target.dataset.readonly;\n } else {\n target.innerHTML = target.dataset.alt;\n }\n }\n\n /**\n * Listener to toggle the edit mode of the component.\n */\n _toggleEditMode() {\n this.controller.readOnly = !this.controller.readOnly;\n }\n\n /**\n * Check that the edited state JSON is valid.\n *\n * Not all valid JSON are suitable for transforming the state. For example,\n * the first level attributes cannot change the type.\n *\n * @return {undefined|array} Array of state updates.\n */\n _checkJSON() {\n const invalid = this.getElement(this.selectors.INVALID);\n const save = this.getElement(this.selectors.SAVE);\n\n const edited = this.getElement(this.selectors.STATE).value;\n\n const currentStateData = this.controller.stateData;\n\n // Check if the json is tha same as state.\n if (edited == JSON.stringify(this.controller.state, null, 4)) {\n invalid.style.color = '';\n invalid.innerHTML = '';\n save.disabled = true;\n return undefined;\n }\n\n // Check if the json format is valid.\n try {\n const newState = JSON.parse(edited);\n // Check the first level did not change types.\n const result = this._generateStateUpdates(currentStateData, newState);\n // Enable save button.\n invalid.style.color = '';\n invalid.innerHTML = this.strings.savewarning;\n save.disabled = false;\n return result;\n } catch (error) {\n invalid.style.color = 'red';\n invalid.innerHTML = error.message ?? 'Invalid JSON sctructure';\n save.disabled = true;\n return undefined;\n }\n }\n\n /**\n * Listener to save the current edited state into the real state.\n */\n _saveState() {\n const updates = this._checkJSON();\n if (!updates) {\n return;\n }\n // Sent the updates to the state manager.\n this.controller.processUpdates(updates);\n }\n\n /**\n * Check that the edited state JSON is valid.\n *\n * Not all valid JSON are suitable for transforming the state. For example,\n * the first level attributes cannot change the type. This method do a two\n * steps comparison between the current state data and the new state data.\n *\n * A reactive state cannot be overridden like any other variable. To keep\n * the watchers updated is necessary to transform the current state into\n * the new one. As a result, this method generates all the necessary state\n * updates to convert the state into the new state.\n *\n * @param {object} currentStateData\n * @param {object} newStateData\n * @return {array} Array of state updates.\n * @throws {Error} is the structure is not compatible\n */\n _generateStateUpdates(currentStateData, newStateData) {\n\n const updates = [];\n\n const ids = {};\n\n // Step 1: Add all overrides newStateData.\n for (const [key, newValue] of Object.entries(newStateData)) {\n // Check is it is new.\n if (Array.isArray(newValue)) {\n ids[key] = {};\n newValue.forEach(element => {\n if (element.id === undefined) {\n throw Error(`Array ${key} element without id attribute`);\n }\n updates.push({\n name: key,\n action: 'override',\n fields: element,\n });\n const index = String(element.id).valueOf();\n ids[key][index] = true;\n });\n } else {\n updates.push({\n name: key,\n action: 'override',\n fields: newValue,\n });\n }\n }\n // Step 2: delete unnecesary data from currentStateData.\n for (const [key, oldValue] of Object.entries(currentStateData)) {\n let deleteField = false;\n // Check if the attribute is still there.\n if (newStateData[key] === undefined) {\n deleteField = true;\n }\n if (Array.isArray(oldValue)) {\n if (!deleteField && ids[key] === undefined) {\n throw Error(`Array ${key} cannot change to object.`);\n }\n oldValue.forEach(element => {\n const index = String(element.id).valueOf();\n let deleteEntry = deleteField;\n // Check if the id is there.\n if (!deleteEntry && ids[key][index] === undefined) {\n deleteEntry = true;\n }\n if (deleteEntry) {\n updates.push({\n name: key,\n action: 'delete',\n fields: element,\n });\n }\n });\n } else {\n if (!deleteField && ids[key] !== undefined) {\n throw Error(`Object ${key} cannot change to array.`);\n }\n if (deleteField) {\n updates.push({\n name: key,\n action: 'delete',\n fields: oldValue,\n });\n }\n }\n }\n // Delete all elements without action.\n return updates;\n }\n\n // Drag and drop methods.\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n getDraggableData() {\n return this.draggable;\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n * @param {Event} event the dropdata\n */\n dragEnd(dropdata, event) {\n this.element.style.top = `${event.newFixedTop}px`;\n this.element.style.left = `${event.newFixedLeft}px`;\n }\n\n /**\n * Pin and unpin the panel.\n */\n _togglePin() {\n this.draggable = !this.draggable;\n this.dragdrop.setDraggable(this.draggable);\n if (this.draggable) {\n this._unpin();\n } else {\n this._pin();\n }\n }\n\n /**\n * Unpin the panel form the footer.\n */\n _unpin() {\n // Find the initial spot.\n const pageCenterY = window.innerHeight / 2;\n const pageCenterX = window.innerWidth / 2;\n // Put the element in the middle of the screen\n const style = {\n position: 'fixed',\n resize: 'both',\n overflow: 'auto',\n height: '400px',\n width: '400px',\n top: `${pageCenterY - 200}px`,\n left: `${pageCenterX - 200}px`,\n };\n Object.assign(this.element.style, style);\n // Small also the text areas.\n this.getElement(this.selectors.STATE).style.height = '50px';\n this.getElement(this.selectors.LOG).style.height = '50px';\n\n this._toggleButtonText(this.getElement(this.selectors.PIN));\n }\n\n /**\n * Pin the panel into the footer.\n */\n _pin() {\n const props = [\n 'position',\n 'resize',\n 'overflow',\n 'top',\n 'left',\n 'height',\n 'width',\n ];\n props.forEach(\n prop => this.element.style.removeProperty(prop)\n );\n this._toggleButtonText(this.getElement(this.selectors.PIN));\n }\n\n /**\n * Toogle the button text with the data-alt value.\n *\n * @param {Element} element the button element\n */\n _toggleButtonText(element) {\n [element.innerHTML, element.dataset.alt] = [element.dataset.alt, element.innerHTML];\n }\n\n}\n"],"names":["target","selectors","element","document","getElementById","undefined","debug","GlobalDebugPanel","reactive","remove","DebugInstanceSubpanel","BaseComponent","create","name","LOADERS","SUBPANEL","NOINSTANCES","LOG","classes","HIDE","subPanels","Set","stateReady","state","_updateReactivesPanels","getElement","this","innerHTML","getWatchers","watch","handler","classList","toggle","reactives","size","forEach","instance","_createLoader","has","id","add","loaders","btn","createElement","dataset","appendChild","addEventListener","_openPanel","data","renderComponent","error","NAME","CLOSE","READMODE","HIGHLIGHT","STATE","CLEAN","PIN","SAVE","INVALID","controller","M","draggable","relativeDrag","strings","savewarning","dragdrop","DragDrop","highlight","_toggleButtonText","_toggleEditMode","_cleanAreas","_togglePin","disabled","_checkJSON","_saveState","_this$getElement2","_refreshState","destroy","unregister","_refreshLog","_refreshReadOnly","logContent","lastChanges","join","value","scrollTop","scrollHeight","JSON","stringify","readonly","readOnly","alt","invalid","save","edited","currentStateData","stateData","style","color","newState","parse","result","_generateStateUpdates","message","updates","processUpdates","newStateData","ids","key","newValue","Object","entries","Array","isArray","Error","push","action","fields","index","String","valueOf","oldValue","deleteField","deleteEntry","getDraggableData","dragEnd","dropdata","event","top","newFixedTop","left","newFixedLeft","setDraggable","_unpin","_pin","pageCenterY","window","innerHeight","pageCenterX","innerWidth","position","resize","overflow","height","width","assign","prop","removeProperty"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"debugpanel.min.js","sources":["../../../src/local/reactive/debugpanel.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 * Reactive module debug panel.\n *\n * This module contains all the UI components for the reactive debug tools.\n * Those tools are only available if the debug is enables and could be used\n * from the footer.\n *\n * @module core/local/reactive/debugpanel\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop, debug} from 'core/reactive';\nimport log from 'core/log';\nimport {debounce} from 'core/utils';\n\n/**\n * Init the main reactive panel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const init = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new GlobalDebugPanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Init an instance reactive subpanel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const initsubpanel = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new DebugInstanceSubpanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass GlobalDebugPanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'GlobalDebugPanel';\n // Default query selectors.\n this.selectors = {\n LOADERS: `[data-for='loaders']`,\n SUBPANEL: `[data-for='subpanel']`,\n NOINSTANCES: `[data-for='noinstances']`,\n LOG: `[data-for='log']`,\n };\n this.classes = {\n HIDE: `d-none`,\n };\n // The list of loaded debuggers.\n this.subPanels = new Set();\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n stateReady(state) {\n this._updateReactivesPanels({state});\n // Remove loading wheel.\n this.getElement(this.selectors.SUBPANEL).innerHTML = '';\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `reactives:created`, handler: this._updateReactivesPanels},\n ];\n }\n\n /**\n * Update the list of reactive instances.\n * @param {Object} args\n * @param {Object} args.state the current state\n */\n _updateReactivesPanels({state}) {\n this.getElement(this.selectors.NOINSTANCES)?.classList?.toggle(\n this.classes.HIDE,\n state.reactives.size > 0\n );\n // Generate loading buttons.\n state.reactives.forEach(\n instance => {\n this._createLoader(instance);\n }\n );\n }\n\n /**\n * Create a debug panel button for a specific reactive instance.\n *\n * @param {object} instance hte instance data\n */\n _createLoader(instance) {\n if (this.subPanels.has(instance.id)) {\n return;\n }\n this.subPanels.add(instance.id);\n const loaders = this.getElement(this.selectors.LOADERS);\n const btn = document.createElement(\"button\");\n btn.innerHTML = instance.id;\n btn.dataset.id = instance.id;\n loaders.appendChild(btn);\n // Add click event.\n this.addEventListener(btn, 'click', () => this._openPanel(btn, instance));\n }\n\n /**\n * Open a debug panel.\n *\n * @param {Element} btn the button element\n * @param {object} instance the instance data\n */\n async _openPanel(btn, instance) {\n try {\n const target = this.getElement(this.selectors.SUBPANEL);\n const data = {...instance};\n await this.renderComponent(target, 'core/local/reactive/debuginstancepanel', data);\n } catch (error) {\n log.error('Cannot load reactive debug subpanel');\n throw error;\n }\n }\n}\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass DebugInstanceSubpanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'DebugInstanceSubpanel';\n // Default query selectors.\n this.selectors = {\n NAME: `[data-for='name']`,\n CLOSE: `[data-for='close']`,\n READMODE: `[data-for='readmode']`,\n HIGHLIGHT: `[data-for='highlight']`,\n LOG: `[data-for='log']`,\n STATE: `[data-for='state']`,\n CLEAN: `[data-for='clean']`,\n PIN: `[data-for='pin']`,\n SAVE: `[data-for='save']`,\n INVALID: `[data-for='invalid']`,\n };\n this.id = this.element.dataset.id;\n this.controller = M.reactive[this.id];\n\n // The component is created always pinned.\n this.draggable = false;\n // We want the element to be dragged like modal.\n this.relativeDrag = true;\n // Save warning (will be loaded when state is ready.\n this.strings = {\n savewarning: '',\n };\n }\n\n /**\n * Initial state ready method.\n *\n */\n stateReady() {\n // Enable drag and drop.\n this.dragdrop = new DragDrop(this);\n\n // Close button.\n this.addEventListener(\n this.getElement(this.selectors.CLOSE),\n 'click',\n this.remove\n );\n // Highlight button.\n if (this.controller.highlight) {\n this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));\n }\n this.addEventListener(\n this.getElement(this.selectors.HIGHLIGHT),\n 'click',\n () => {\n this.controller.highlight = !this.controller.highlight;\n this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));\n }\n );\n // Edit mode button.\n this.addEventListener(\n this.getElement(this.selectors.READMODE),\n 'click',\n this._toggleEditMode\n );\n // Clean log and state.\n this.addEventListener(\n this.getElement(this.selectors.CLEAN),\n 'click',\n this._cleanAreas\n );\n // Unpin panel butotn.\n this.addEventListener(\n this.getElement(this.selectors.PIN),\n 'click',\n this._togglePin\n );\n // Save button, state format error message and state textarea.\n this.getElement(this.selectors.SAVE).disabled = true;\n\n this.addEventListener(\n this.getElement(this.selectors.STATE),\n 'keyup',\n debounce(this._checkJSON.bind(this), 500)\n );\n\n this.addEventListener(\n this.getElement(this.selectors.SAVE),\n 'click',\n this._saveState\n );\n // Save the default save warning message.\n this.strings.savewarning = this.getElement(this.selectors.INVALID)?.innerHTML ?? '';\n // Add current state.\n this._refreshState();\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `reactives[${this.id}].lastChanges:updated`, handler: this._refreshLog},\n {watch: `reactives[${this.id}].modified:updated`, handler: this._refreshState},\n {watch: `reactives[${this.id}].readOnly:updated`, handler: this._refreshReadOnly},\n ];\n }\n\n /**\n * Wtacher method to refresh the log panel.\n *\n * @param {object} args\n * @param {HTMLElement} args.element\n */\n _refreshLog({element}) {\n const list = element?.lastChanges ?? [];\n\n const logContent = list.join(\"\\n\");\n // Append last log.\n const target = this.getElement(this.selectors.LOG);\n target.value += `\\n\\n= Transaction =\\n ${logContent}`;\n target.scrollTop = target.scrollHeight;\n }\n\n /**\n * Listener method to clean the log area.\n */\n _cleanAreas() {\n let target = this.getElement(this.selectors.LOG);\n target.value = '';\n\n this._refreshState();\n }\n\n /**\n * Watcher to refresh the state information.\n */\n _refreshState() {\n const target = this.getElement(this.selectors.STATE);\n target.value = JSON.stringify(this.controller.state, null, 4);\n }\n\n /**\n * Watcher to update the read only information.\n */\n _refreshReadOnly() {\n // Toggle the read mode button.\n const target = this.getElement(this.selectors.READMODE);\n if (target.dataset.readonly === undefined) {\n target.dataset.readonly = target.innerHTML;\n }\n if (this.controller.readOnly) {\n target.innerHTML = target.dataset.readonly;\n } else {\n target.innerHTML = target.dataset.alt;\n }\n }\n\n /**\n * Listener to toggle the edit mode of the component.\n */\n _toggleEditMode() {\n this.controller.readOnly = !this.controller.readOnly;\n }\n\n /**\n * Check that the edited state JSON is valid.\n *\n * Not all valid JSON are suitable for transforming the state. For example,\n * the first level attributes cannot change the type.\n *\n * @return {undefined|array} Array of state updates.\n */\n _checkJSON() {\n const invalid = this.getElement(this.selectors.INVALID);\n const save = this.getElement(this.selectors.SAVE);\n\n const edited = this.getElement(this.selectors.STATE).value;\n\n const currentStateData = this.controller.stateData;\n\n // Check if the json is tha same as state.\n if (edited == JSON.stringify(this.controller.state, null, 4)) {\n invalid.style.color = '';\n invalid.innerHTML = '';\n save.disabled = true;\n return undefined;\n }\n\n // Check if the json format is valid.\n try {\n const newState = JSON.parse(edited);\n // Check the first level did not change types.\n const result = this._generateStateUpdates(currentStateData, newState);\n // Enable save button.\n invalid.style.color = '';\n invalid.innerHTML = this.strings.savewarning;\n save.disabled = false;\n return result;\n } catch (error) {\n invalid.style.color = 'red';\n invalid.innerHTML = error.message ?? 'Invalid JSON sctructure';\n save.disabled = true;\n return undefined;\n }\n }\n\n /**\n * Listener to save the current edited state into the real state.\n */\n _saveState() {\n const updates = this._checkJSON();\n if (!updates) {\n return;\n }\n // Sent the updates to the state manager.\n this.controller.processUpdates(updates);\n }\n\n /**\n * Check that the edited state JSON is valid.\n *\n * Not all valid JSON are suitable for transforming the state. For example,\n * the first level attributes cannot change the type. This method do a two\n * steps comparison between the current state data and the new state data.\n *\n * A reactive state cannot be overridden like any other variable. To keep\n * the watchers updated is necessary to transform the current state into\n * the new one. As a result, this method generates all the necessary state\n * updates to convert the state into the new state.\n *\n * @param {object} currentStateData\n * @param {object} newStateData\n * @return {array} Array of state updates.\n * @throws {Error} is the structure is not compatible\n */\n _generateStateUpdates(currentStateData, newStateData) {\n\n const updates = [];\n\n const ids = {};\n\n // Step 1: Add all overrides newStateData.\n for (const [key, newValue] of Object.entries(newStateData)) {\n // Check is it is new.\n if (Array.isArray(newValue)) {\n ids[key] = {};\n newValue.forEach(element => {\n if (element.id === undefined) {\n throw Error(`Array ${key} element without id attribute`);\n }\n updates.push({\n name: key,\n action: 'override',\n fields: element,\n });\n const index = String(element.id).valueOf();\n ids[key][index] = true;\n });\n } else {\n updates.push({\n name: key,\n action: 'override',\n fields: newValue,\n });\n }\n }\n // Step 2: delete unnecesary data from currentStateData.\n for (const [key, oldValue] of Object.entries(currentStateData)) {\n let deleteField = false;\n // Check if the attribute is still there.\n if (newStateData[key] === undefined) {\n deleteField = true;\n }\n if (Array.isArray(oldValue)) {\n if (!deleteField && ids[key] === undefined) {\n throw Error(`Array ${key} cannot change to object.`);\n }\n oldValue.forEach(element => {\n const index = String(element.id).valueOf();\n let deleteEntry = deleteField;\n // Check if the id is there.\n if (!deleteEntry && ids[key][index] === undefined) {\n deleteEntry = true;\n }\n if (deleteEntry) {\n updates.push({\n name: key,\n action: 'delete',\n fields: element,\n });\n }\n });\n } else {\n if (!deleteField && ids[key] !== undefined) {\n throw Error(`Object ${key} cannot change to array.`);\n }\n if (deleteField) {\n updates.push({\n name: key,\n action: 'delete',\n fields: oldValue,\n });\n }\n }\n }\n // Delete all elements without action.\n return updates;\n }\n\n // Drag and drop methods.\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n getDraggableData() {\n return this.draggable;\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n * @param {Event} event the dropdata\n */\n dragEnd(dropdata, event) {\n this.element.style.top = `${event.newFixedTop}px`;\n this.element.style.left = `${event.newFixedLeft}px`;\n }\n\n /**\n * Pin and unpin the panel.\n */\n _togglePin() {\n this.draggable = !this.draggable;\n this.dragdrop.setDraggable(this.draggable);\n if (this.draggable) {\n this._unpin();\n } else {\n this._pin();\n }\n }\n\n /**\n * Unpin the panel form the footer.\n */\n _unpin() {\n // Find the initial spot.\n const pageCenterY = window.innerHeight / 2;\n const pageCenterX = window.innerWidth / 2;\n // Put the element in the middle of the screen\n const style = {\n position: 'fixed',\n resize: 'both',\n overflow: 'auto',\n height: '400px',\n width: '400px',\n top: `${pageCenterY - 200}px`,\n left: `${pageCenterX - 200}px`,\n };\n Object.assign(this.element.style, style);\n // Small also the text areas.\n this.getElement(this.selectors.STATE).style.height = '50px';\n this.getElement(this.selectors.LOG).style.height = '50px';\n\n this._toggleButtonText(this.getElement(this.selectors.PIN));\n }\n\n /**\n * Pin the panel into the footer.\n */\n _pin() {\n const props = [\n 'position',\n 'resize',\n 'overflow',\n 'top',\n 'left',\n 'height',\n 'width',\n ];\n props.forEach(\n prop => this.element.style.removeProperty(prop)\n );\n this._toggleButtonText(this.getElement(this.selectors.PIN));\n }\n\n /**\n * Toogle the button text with the data-alt value.\n *\n * @param {Element} element the button element\n */\n _toggleButtonText(element) {\n [element.innerHTML, element.dataset.alt] = [element.dataset.alt, element.innerHTML];\n }\n\n}\n"],"names":["target","selectors","element","document","getElementById","undefined","debug","GlobalDebugPanel","reactive","remove","DebugInstanceSubpanel","BaseComponent","create","name","LOADERS","SUBPANEL","NOINSTANCES","LOG","classes","HIDE","subPanels","Set","stateReady","state","_updateReactivesPanels","getElement","this","innerHTML","getWatchers","watch","handler","classList","toggle","reactives","size","forEach","instance","_createLoader","has","id","add","loaders","btn","createElement","dataset","appendChild","addEventListener","_openPanel","data","renderComponent","error","NAME","CLOSE","READMODE","HIGHLIGHT","STATE","CLEAN","PIN","SAVE","INVALID","controller","M","draggable","relativeDrag","strings","savewarning","dragdrop","DragDrop","highlight","_toggleButtonText","_toggleEditMode","_cleanAreas","_togglePin","disabled","_checkJSON","bind","_saveState","_this$getElement2","_refreshState","destroy","unregister","_refreshLog","_refreshReadOnly","logContent","lastChanges","join","value","scrollTop","scrollHeight","JSON","stringify","readonly","readOnly","alt","invalid","save","edited","currentStateData","stateData","style","color","newState","parse","result","_generateStateUpdates","message","updates","processUpdates","newStateData","ids","key","newValue","Object","entries","Array","isArray","Error","push","action","fields","index","String","valueOf","oldValue","deleteField","deleteEntry","getDraggableData","dragEnd","dropdata","event","top","newFixedTop","left","newFixedLeft","setDraggable","_unpin","_pin","pageCenterY","window","innerHeight","pageCenterX","innerWidth","position","resize","overflow","height","width","assign","prop","removeProperty"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/src/local/reactive/debugpanel.js b/lib/amd/src/local/reactive/debugpanel.js index 738ad6bbad385..f9c3c58909ee3 100644 --- a/lib/amd/src/local/reactive/debugpanel.js +++ b/lib/amd/src/local/reactive/debugpanel.js @@ -266,7 +266,7 @@ class DebugInstanceSubpanel extends BaseComponent { this.addEventListener( this.getElement(this.selectors.STATE), 'keyup', - debounce(this._checkJSON, 500) + debounce(this._checkJSON.bind(this), 500) ); this.addEventListener( From 904cf075e7f0b0b457f071a08fa34db034d9f72b Mon Sep 17 00:00:00 2001 From: "Shamiso.Jaravaza" <33659194+ssj365@users.noreply.github.com> Date: Tue, 16 Jan 2024 10:16:37 -0700 Subject: [PATCH 014/119] MDL-80625 mod_bigbluebuttonbn: Fix param name --- mod/bigbluebuttonbn/classes/meeting.php | 2 +- mod/bigbluebuttonbn/tests/generator/lib.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mod/bigbluebuttonbn/classes/meeting.php b/mod/bigbluebuttonbn/classes/meeting.php index 25b9a34e25856..095a09d8a20c4 100644 --- a/mod/bigbluebuttonbn/classes/meeting.php +++ b/mod/bigbluebuttonbn/classes/meeting.php @@ -462,7 +462,7 @@ protected function create_meeting_metadata() { ); } if ((boolean) config::get('recordingready_enabled')) { - $metadata['bn-recording-ready-url'] = $this->instance->get_record_ready_url()->out(false); + $metadata['bbb-recording-ready-url'] = $this->instance->get_record_ready_url()->out(false); } if ((boolean) config::get('meetingevents_enabled')) { $metadata['analytics-callback-url'] = $this->instance->get_meeting_event_notification_url()->out(false); diff --git a/mod/bigbluebuttonbn/tests/generator/lib.php b/mod/bigbluebuttonbn/tests/generator/lib.php index c6970e6909d21..81e46caa11a89 100644 --- a/mod/bigbluebuttonbn/tests/generator/lib.php +++ b/mod/bigbluebuttonbn/tests/generator/lib.php @@ -246,7 +246,7 @@ protected function create_mockserver_recording(instance $instance, stdClass $rec 'sequence' => 1, 'meta' => [ 'bn-presenter-name' => $data['presentername'] ?? 'Fake presenter', - 'bn-recording-ready-url' => new moodle_url('/mod/bigbluebuttonbn/bbb_broker.php', [ + 'bbb-recording-ready-url' => new moodle_url('/mod/bigbluebuttonbn/bbb_broker.php', [ 'action' => 'recording_ready', 'bigbluebuttonbn' => $instance->get_instance_id() ]), @@ -352,7 +352,7 @@ public function create_meeting(array $data): stdClass { ], ]); if ((boolean) config::get('recordingready_enabled')) { - $roomconfig['meta']['bn-recording-ready-url'] = $instance->get_record_ready_url()->out(false); + $roomconfig['meta']['bbb-recording-ready-url'] = $instance->get_record_ready_url()->out(false); } if ((boolean) config::get('meetingevents_enabled')) { $roomconfig['meta']['analytics-callback-url'] = $instance->get_meeting_event_notification_url()->out(false); From 802876e9a8161a83737192d34c0fe7e0bbf4ecb9 Mon Sep 17 00:00:00 2001 From: "Shamiso.Jaravaza" <33659194+ssj365@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:54:50 -0700 Subject: [PATCH 015/119] MDL-80061 mod_bigbluebuttonbn: Change filter field * Change dismissed recording time filter from timecreated to timemodified --- mod/bigbluebuttonbn/classes/recording.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/bigbluebuttonbn/classes/recording.php b/mod/bigbluebuttonbn/classes/recording.php index 34bcb7556c0c9..d0565a824267c 100644 --- a/mod/bigbluebuttonbn/classes/recording.php +++ b/mod/bigbluebuttonbn/classes/recording.php @@ -788,7 +788,7 @@ public static function sync_pending_recordings_from_server(bool $dismissedonly = if ($dismissedonly) { mtrace("=> Looking for any recording that has been 'dismissed' in the past " . self::RECORDING_TIME_LIMIT_DAYS . " days."); - $select = 'status = :status_dismissed AND timecreated > :withindays'; + $select = 'status = :status_dismissed AND timemodified > :withindays'; $params['status_dismissed'] = self::RECORDING_STATUS_DISMISSED; } else { mtrace("=> Looking for any recording awaiting processing from the past " . self::RECORDING_TIME_LIMIT_DAYS . " days."); From 1945ad1d36ffa131c167495fea023d1117b6de55 Mon Sep 17 00:00:00 2001 From: Anupama Sarjoshi Date: Fri, 14 Jun 2024 11:04:03 +0100 Subject: [PATCH 016/119] MDL-79796 quiz: Preview icon should be visible in question bank pop-up --- mod/quiz/styles.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index 64d2683b0d098..780bbe267c40e 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -815,7 +815,7 @@ table.quizreviewsummary td.cell { } #page-mod-quiz-edit .question-bank-table { - width: auto; + width: 100%; } #page-mod-quiz-edit .question-bank-table .header { @@ -889,6 +889,7 @@ table.quizreviewsummary td.cell { #page-mod-quiz-edit .question-bank-table .questionnametext { padding: 0 0.2em; + width: 100% !important; /* stylelint-disable-line declaration-no-important */ } #page-mod-quiz-edit .question-bank-table .questiontext { @@ -915,6 +916,12 @@ table.quizreviewsummary td.cell { #page-mod-quiz-edit table.question-bank-table th { overflow: hidden; white-space: nowrap; + width: 1.2em !important; /* stylelint-disable-line declaration-no-important */ +} + +#page-mod-quiz-edit table.question-bank-table td.questionnametext > div { + overflow: hidden; + text-overflow: ellipsis; } .mod_quiz_qbank_dialogue { From 6105d96744dda7ce3f7a7aae6cb7df85fcd3bcbb Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 14 Jun 2024 12:37:52 +0100 Subject: [PATCH 017/119] MDL-82200 inplace editable: fix background of the help text --- theme/boost/scss/moodle/course.scss | 1 + theme/boost/style/moodle.css | 1 + theme/classic/style/moodle.css | 1 + 3 files changed, 3 insertions(+) diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index 8451484ecc37e..c49311a4acd67 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -535,6 +535,7 @@ span.editinstructions { text-decoration: none; z-index: 9999; border: $alert-border-width solid transparent; + width: fit-content; @include alert-variant( theme-color-level('info', $alert-bg-level), diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 36dfa1fe71cc2..9862d27afd082 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -28459,6 +28459,7 @@ span.editinstructions { text-decoration: none; z-index: 9999; border: 0 solid transparent; + width: fit-content; color: #00434e; background-color: #cce6ea; border-color: #b8dce2; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 41a60b439069b..a85f7f3e83263 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -28459,6 +28459,7 @@ span.editinstructions { text-decoration: none; z-index: 9999; border: 0 solid transparent; + width: fit-content; color: #00434e; background-color: #cce6ea; border-color: #b8dce2; From 06b779d31d2045567b7920078ef3397fdb199b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Thu, 30 May 2024 12:38:27 +0200 Subject: [PATCH 018/119] MDL-81818 theme_boost: Refactor ".no-gutters" usages for BS5 - Add .g-0 to the Boostratp 5 bridge SCSS file - Replace .no-gutters occurrences with .g-0 --- .upgradenotes/MDL-81818-2024053109201607.yml | 5 +++++ .../content/bootstrap/components/card.md | 4 ++-- .../content/bootstrap/utilities/stretched-link.md | 2 +- .../hugo/site/layouts/_default/docs.html | 2 +- .../hugo/site/layouts/shortcodes/moodleicons.html | 2 +- blocks/myoverview/templates/main.mustache | 2 +- blocks/timeline/templates/main.mustache | 2 +- message/templates/message_index.mustache | 2 +- mod/quiz/templates/list_of_attempts.mustache | 2 +- theme/boost/scss/moodle.scss | 3 +++ theme/boost/scss/moodle/bs5-bridge.scss | 7 +++++++ theme/boost/style/moodle.css | 10 +++++++--- theme/classic/style/moodle.css | 10 +++++++--- 13 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 .upgradenotes/MDL-81818-2024053109201607.yml create mode 100644 theme/boost/scss/moodle/bs5-bridge.scss diff --git a/.upgradenotes/MDL-81818-2024053109201607.yml b/.upgradenotes/MDL-81818-2024053109201607.yml new file mode 100644 index 0000000000000..9fedb9068cf2f --- /dev/null +++ b/.upgradenotes/MDL-81818-2024053109201607.yml @@ -0,0 +1,5 @@ +issueNumber: MDL-81818 +notes: + theme_boost: + - message: 'Bootstrap .no-gutters class is no longer used, use .g-0 instead.' + type: changed diff --git a/admin/tool/componentlibrary/content/bootstrap/components/card.md b/admin/tool/componentlibrary/content/bootstrap/components/card.md index ea784dc99461f..18645248cfcd5 100644 --- a/admin/tool/componentlibrary/content/bootstrap/components/card.md +++ b/admin/tool/componentlibrary/content/bootstrap/components/card.md @@ -392,11 +392,11 @@ Note that content should not be larger than the height of the image. If content ## Horizontal -Using a combination of grid and utility classes, cards can be made horizontal in a mobile-friendly and responsive way. In the example below, we remove the grid gutters with `.no-gutters` and use `.col-md-*` classes to make the card horizontal at the `md` breakpoint. Further adjustments may be needed depending on your card content. +Using a combination of grid and utility classes, cards can be made horizontal in a mobile-friendly and responsive way. In the example below, we remove the grid gutters with `.g-0` and use `.col-md-*` classes to make the card horizontal at the `md` breakpoint. Further adjustments may be needed depending on your card content. {{< example >}}
-
+
{{< placeholder width="100%" height="250" class="" text="Image" >}}
diff --git a/admin/tool/componentlibrary/content/bootstrap/utilities/stretched-link.md b/admin/tool/componentlibrary/content/bootstrap/utilities/stretched-link.md index db714cd7687ec..436318101cb20 100644 --- a/admin/tool/componentlibrary/content/bootstrap/utilities/stretched-link.md +++ b/admin/tool/componentlibrary/content/bootstrap/utilities/stretched-link.md @@ -38,7 +38,7 @@ Media objects do not have `position: relative` by default, so we need to add the Columns are `position: relative` by default, so clickable columns only require the `.stretched-link` class on a link. However, stretching a link over an entire `.row` requires `.position-static` on the column and `.position-relative` on the row. {{< example >}} -
+
{{< placeholder width="100%" height="200" class="w-100" text=" " title="Generic placeholder image" >}}
diff --git a/admin/tool/componentlibrary/hugo/site/layouts/_default/docs.html b/admin/tool/componentlibrary/hugo/site/layouts/_default/docs.html index 93c3b7c993897..27a96ef96b8d2 100644 --- a/admin/tool/componentlibrary/hugo/site/layouts/_default/docs.html +++ b/admin/tool/componentlibrary/hugo/site/layouts/_default/docs.html @@ -1,7 +1,7 @@ {{ partial "skippy" . }}
-
+