MDL-5311 qtype_multichoice: add clear my choice option
authorSimey Lameze <simey@moodle.com>
Thu, 4 Apr 2019 02:43:14 +0000 (10:43 +0800)
committerSimey Lameze <simey@moodle.com>
Mon, 29 Apr 2019 06:04:51 +0000 (14:04 +0800)
question/type/multichoice/amd/build/clearchoice.min.js [new file with mode: 0644]
question/type/multichoice/amd/src/clearchoice.js [new file with mode: 0644]
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/question.php
question/type/multichoice/renderer.php

diff --git a/question/type/multichoice/amd/build/clearchoice.min.js b/question/type/multichoice/amd/build/clearchoice.min.js
new file mode 100644 (file)
index 0000000..dc8c9fb
Binary files /dev/null and b/question/type/multichoice/amd/build/clearchoice.min.js differ
diff --git a/question/type/multichoice/amd/src/clearchoice.js b/question/type/multichoice/amd/src/clearchoice.js
new file mode 100644 (file)
index 0000000..59a03e6
--- /dev/null
@@ -0,0 +1,105 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Manages 'Clear my choice' functionality actions.
+ *
+ * @module     qtype_multichoice/clearchoice
+ * @copyright  2019 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      3.7
+ */
+define(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
+
+    var SELECTORS = {
+        CHOICE_ELEMENT: '.answer input',
+        CLEAR_CHOICE_ELEMENT: 'div[class="qtype_multichoice_clearchoice"]'
+    };
+
+    /**
+     * Mark clear choice radio as checked.
+     *
+     * @param {Object} clearChoiceContainer The clear choice option container.
+     */
+    var checkClearChoiceRadio = function(clearChoiceContainer) {
+        clearChoiceContainer.find('input[type="radio"]').prop('checked', true);
+    };
+
+    /**
+     * Get the clear choice div container.
+     *
+     * @param {Object} root The question root element.
+     * @param {string} fieldPrefix The question outer div prefix.
+     * @returns {Object} The clear choice div container.
+     */
+    var getClearChoiceElement = function(root, fieldPrefix) {
+        return root.find('div[id="' + fieldPrefix + '"]');
+    };
+
+    /**
+     * Hide clear choice option.
+     *
+     * @param {Object} clearChoiceContainer The clear choice option container.
+     */
+    var hideClearChoiceOption = function(clearChoiceContainer) {
+        clearChoiceContainer.addClass('hidden');
+    };
+
+    /**
+     * Shows clear choice option.
+     *
+     * @param {Object} clearChoiceContainer The clear choice option container.
+     */
+    var showClearChoiceOption = function(clearChoiceContainer) {
+        clearChoiceContainer.removeClass('hidden');
+    };
+
+    /**
+     * Register event listeners for the clear choice module.
+     *
+     * @param {Object} root The question outer div prefix.
+     * @param {string} fieldPrefix The "Clear choice" div prefix.
+     */
+    var registerEventListeners = function(root, fieldPrefix) {
+        var clearChoiceContainer = getClearChoiceElement(root, fieldPrefix);
+
+        root.on(CustomEvents.events.activate, SELECTORS.CLEAR_CHOICE_ELEMENT, function() {
+                // Mark the clear choice radio element as checked.
+                checkClearChoiceRadio(clearChoiceContainer);
+                // Now that the hidden radio has been checked, hide the clear choice option.
+                hideClearChoiceOption(clearChoiceContainer);
+        });
+
+        root.on(CustomEvents.events.activate, SELECTORS.CHOICE_ELEMENT, function() {
+            // If the event has been triggered by any other choice, show the clear choice option.
+            showClearChoiceOption(clearChoiceContainer);
+        });
+    };
+
+    /**
+     * Initialise clear choice module.
+
+     * @param {string} root The question outer div prefix.
+     * @param {string} fieldPrefix The "Clear choice" div prefix.
+     */
+    var init = function(root, fieldPrefix) {
+        root = $('#' + root);
+        registerEventListeners(root, fieldPrefix);
+    };
+
+    return {
+        init: init
+    };
+});
index 9e4ebb8..2473d43 100644 (file)
@@ -37,6 +37,7 @@ $string['answersingleno'] = 'Multiple answers allowed';
 $string['answersingleyes'] = 'One answer only';
 $string['choiceno'] = 'Choice {$a}';
 $string['choices'] = 'Available choices';
+$string['clearchoice'] = 'Clear my choice';
 $string['clozeaid'] = 'Enter missing word';
 $string['correctansweris'] = 'The correct answer is: {$a}';
 $string['correctanswersare'] = 'The correct answers are: {$a}';
index 09b9493..fcf63c5 100644 (file)
@@ -176,8 +176,7 @@ class qtype_multichoice_single_question extends qtype_multichoice_base {
     }
 
     public function summarise_response(array $response) {
-        if (!array_key_exists('answer', $response) ||
-                !array_key_exists($response['answer'], $this->order)) {
+        if (!$this->is_complete_response($response)) {
             return null;
         }
         $ansid = $this->order[$response['answer']];
@@ -186,8 +185,7 @@ class qtype_multichoice_single_question extends qtype_multichoice_base {
     }
 
     public function classify_response(array $response) {
-        if (!array_key_exists('answer', $response) ||
-                !array_key_exists($response['answer'], $this->order)) {
+        if (!$this->is_complete_response($response)) {
             return array($this->id => question_classified_response::no_response());
         }
         $choiceid = $this->order[$response['answer']];
@@ -230,11 +228,18 @@ class qtype_multichoice_single_question extends qtype_multichoice_base {
     }
 
     public function is_same_response(array $prevresponse, array $newresponse) {
+        if (!$this->is_complete_response($prevresponse)) {
+            $prevresponse = [];
+        }
+        if (!$this->is_complete_response($newresponse)) {
+            $newresponse = [];
+        }
         return question_utils::arrays_same_at_key($prevresponse, $newresponse, 'answer');
     }
 
     public function is_complete_response(array $response) {
-        return array_key_exists('answer', $response) && $response['answer'] !== '';
+        return array_key_exists('answer', $response) && $response['answer'] !== ''
+                && (string) $response['answer'] !== '-1';
     }
 
     public function is_gradable_response(array $response) {
index 25a5b2e..b48809a 100644 (file)
@@ -35,6 +35,17 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedback_renderer {
+
+    /**
+     * Method to generating the bits of output after question choices.
+     *
+     * @param question_attempt $qa The question attempt object.
+     * @param question_display_options $options controls what should and should not be displayed.
+     *
+     * @return string HTML output.
+     */
+    protected abstract function after_choices(question_attempt $qa, question_display_options $options);
+
     protected abstract function get_input_type();
 
     protected abstract function get_input_name(question_attempt $qa, $value);
@@ -136,6 +147,8 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb
         }
         $result .= html_writer::end_tag('div'); // Answer.
 
+        $result .= $this->after_choices($qa, $options);
+
         $result .= html_writer::end_tag('div'); // Ablock.
 
         if ($qa->get_state() == question_state::$invalid) {
@@ -252,6 +265,53 @@ class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base
         }
         return $this->correct_choices($right);
     }
+
+    public function after_choices(question_attempt $qa, question_display_options $options) {
+        // Only load the clear choice feature if it's not read only.
+        if ($options->readonly) {
+            return '';
+        }
+
+        $question = $qa->get_question();
+        $response = $question->get_response($qa);
+        $hascheckedchoice = false;
+        foreach ($question->get_order($qa) as $value => $ansid) {
+            if ($question->is_choice_selected($response, $value)) {
+                $hascheckedchoice = true;
+                break;
+            }
+        }
+
+        $clearchoiceid = $this->get_input_id($qa, -1);
+        $clearchoicefieldname = $qa->get_qt_field_name('clearchoice');
+        $clearchoiceradioattrs = [
+            'type' => $this->get_input_type(),
+            'name' => $qa->get_qt_field_name('answer'),
+            'id' => $clearchoiceid,
+            'value' => -1
+        ];
+
+        $cssclass = 'qtype_multichoice_clearchoice';
+        // When no choice selected during rendering, then hide the clear choice option.
+        if (!$hascheckedchoice && $response == -1) {
+            $cssclass .= ' hidden';
+            $clearchoiceradioattrs['checked'] = 'checked';
+        }
+        // Adds an hidden radio that will be checked to give the impression the choice has been cleared.
+        $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs);
+        $clearchoiceradio .= html_writer::tag('label', get_string('clearchoice', 'qtype_multichoice'),
+            ['for' => $clearchoiceid, 'role' => 'button', 'tabindex' => 0]);
+
+        // Now wrap the radio and label inside a div.
+        $result = html_writer::tag('div', $clearchoiceradio, ['id' => $clearchoicefieldname, 'class' => $cssclass]);
+
+        // Load required clearchoice AMD module.
+        $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
+            [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]);
+
+        return $result;
+    }
+
 }
 
 /**
@@ -262,6 +322,10 @@ class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base {
+    protected function after_choices(question_attempt $qa, question_display_options $options) {
+        return '';
+    }
+
     protected function get_input_type() {
         return 'checkbox';
     }