MDL-68864 qtype_multichoice: clearchoice button styles
[moodle.git] / question / type / multichoice / renderer.php
CommitLineData
c9c989a0 1<?php
c9c989a0
TH
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
c9c989a0
TH
17/**
18 * Multiple choice question renderer classes.
19 *
7764183a
TH
20 * @package qtype
21 * @subpackage multichoice
22 * @copyright 2009 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c9c989a0
TH
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
29
c9c989a0
TH
30/**
31 * Base class for generating the bits of output common to multiple choice
32 * single and multiple questions.
33 *
7764183a
TH
34 * @copyright 2009 The Open University
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c9c989a0
TH
36 */
37abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedback_renderer {
f6a77849
SL
38
39 /**
40 * Method to generating the bits of output after question choices.
41 *
42 * @param question_attempt $qa The question attempt object.
43 * @param question_display_options $options controls what should and should not be displayed.
44 *
45 * @return string HTML output.
46 */
47 protected abstract function after_choices(question_attempt $qa, question_display_options $options);
48
c7df5006 49 protected abstract function get_input_type();
c9c989a0 50
c7df5006 51 protected abstract function get_input_name(question_attempt $qa, $value);
c9c989a0 52
c7df5006 53 protected abstract function get_input_value($value);
c9c989a0 54
c7df5006 55 protected abstract function get_input_id(question_attempt $qa, $value);
c9c989a0
TH
56
57 /**
58 * Whether a choice should be considered right, wrong or partially right.
59 * @param question_answer $ans representing one of the choices.
60 * @return fload 1.0, 0.0 or something in between, respectively.
61 */
c7df5006 62 protected abstract function is_right(question_answer $ans);
c9c989a0 63
c7df5006 64 protected abstract function prompt();
c9c989a0
TH
65
66 public function formulation_and_controls(question_attempt $qa,
67 question_display_options $options) {
68
69 $question = $qa->get_question();
c9c989a0
TH
70 $response = $question->get_response($qa);
71
72 $inputname = $qa->get_qt_field_name('answer');
73 $inputattributes = array(
74 'type' => $this->get_input_type(),
75 'name' => $inputname,
76 );
77
78 if ($options->readonly) {
79 $inputattributes['disabled'] = 'disabled';
80 }
81
82 $radiobuttons = array();
83 $feedbackimg = array();
84 $feedback = array();
85 $classes = array();
7ac7977c 86 foreach ($question->get_order($qa) as $value => $ansid) {
c9c989a0
TH
87 $ans = $question->answers[$ansid];
88 $inputattributes['name'] = $this->get_input_name($qa, $value);
89 $inputattributes['value'] = $this->get_input_value($value);
90 $inputattributes['id'] = $this->get_input_id($qa, $value);
91 $isselected = $question->is_choice_selected($response, $value);
92 if ($isselected) {
93 $inputattributes['checked'] = 'checked';
94 } else {
95 unset($inputattributes['checked']);
96 }
97 $hidden = '';
98 if (!$options->readonly && $this->get_input_type() == 'checkbox') {
99 $hidden = html_writer::empty_tag('input', array(
100 'type' => 'hidden',
101 'name' => $inputattributes['name'],
102 'value' => 0,
103 ));
104 }
105 $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) .
12039f2e 106 html_writer::tag('label',
3543eabc 107 html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber') .
40d1487b
BB
108 html_writer::tag('div',
109 $question->format_text(
110 $ans->answer, $ans->answerformat,
111 $qa, 'question', 'answer', $ansid),
112 array('class' => 'flex-fill ml-1')),
113 array('for' => $inputattributes['id'], 'class' => 'd-flex w-100'));
c9c989a0 114
3d9645ae 115 // Param $options->suppresschoicefeedback is a hack specific to the
c9c989a0
TH
116 // oumultiresponse question type. It would be good to refactor to
117 // avoid refering to it here.
118 if ($options->feedback && empty($options->suppresschoicefeedback) &&
119 $isselected && trim($ans->feedback)) {
120 $feedback[] = html_writer::tag('div',
35c9b652
TH
121 $question->make_html_inline($question->format_text(
122 $ans->feedback, $ans->feedbackformat,
123 $qa, 'question', 'answerfeedback', $ansid)),
c9c989a0
TH
124 array('class' => 'specificfeedback'));
125 } else {
126 $feedback[] = '';
127 }
128 $class = 'r' . ($value % 2);
129 if ($options->correctness && $isselected) {
130 $feedbackimg[] = $this->feedback_image($this->is_right($ans));
131 $class .= ' ' . $this->feedback_class($this->is_right($ans));
132 } else {
133 $feedbackimg[] = '';
134 }
135 $classes[] = $class;
136 }
137
138 $result = '';
139 $result .= html_writer::tag('div', $question->format_questiontext($qa),
140 array('class' => 'qtext'));
141
142 $result .= html_writer::start_tag('div', array('class' => 'ablock'));
00055ef6
MK
143 if ($question->showstandardinstruction == 1) {
144 $result .= html_writer::tag('div', $this->prompt(), array('class' => 'prompt'));
145 }
c9c989a0
TH
146
147 $result .= html_writer::start_tag('div', array('class' => 'answer'));
148 foreach ($radiobuttons as $key => $radio) {
a18fda20 149 $result .= html_writer::tag('div', $radio . ' ' . $feedbackimg[$key] . $feedback[$key],
c9c989a0
TH
150 array('class' => $classes[$key])) . "\n";
151 }
3d9645ae 152 $result .= html_writer::end_tag('div'); // Answer.
c9c989a0 153
f6a77849
SL
154 $result .= $this->after_choices($qa, $options);
155
3d9645ae 156 $result .= html_writer::end_tag('div'); // Ablock.
c9c989a0
TH
157
158 if ($qa->get_state() == question_state::$invalid) {
159 $result .= html_writer::nonempty_tag('div',
160 $question->get_validation_error($qa->get_last_qt_data()),
161 array('class' => 'validationerror'));
162 }
163
164 return $result;
165 }
166
167 protected function number_html($qnum) {
168 return $qnum . '. ';
169 }
170
171 /**
172 * @param int $num The number, starting at 0.
173 * @param string $style The style to render the number in. One of the
174 * options returned by {@link qtype_multichoice:;get_numbering_styles()}.
175 * @return string the number $num in the requested style.
176 */
177 protected function number_in_style($num, $style) {
178 switch($style) {
179 case 'abc':
180 $number = chr(ord('a') + $num);
181 break;
182 case 'ABCD':
183 $number = chr(ord('A') + $num);
184 break;
185 case '123':
186 $number = $num + 1;
187 break;
188 case 'iii':
189 $number = question_utils::int_to_roman($num + 1);
190 break;
191 case 'IIII':
192 $number = strtoupper(question_utils::int_to_roman($num + 1));
193 break;
194 case 'none':
195 return '';
196 default:
197 return 'ERR';
198 }
199 return $this->number_html($number);
200 }
201
202 public function specific_feedback(question_attempt $qa) {
203 return $this->combined_feedback($qa);
204 }
4358ee38
A
205
206 /**
207 * Function returns string based on number of correct answers
208 * @param array $right An Array of correct responses to the current question
209 * @return string based on number of correct responses
210 */
211 protected function correct_choices(array $right) {
212 // Return appropriate string for single/multiple correct answer(s).
213 if (count($right) == 1) {
214 return get_string('correctansweris', 'qtype_multichoice',
215 implode(', ', $right));
216 } else if (count($right) > 1) {
217 return get_string('correctanswersare', 'qtype_multichoice',
218 implode(', ', $right));
219 } else {
220 return "";
221 }
222 }
c9c989a0
TH
223}
224
225
226/**
227 * Subclass for generating the bits of output specific to multiple choice
228 * single questions.
229 *
7764183a
TH
230 * @copyright 2009 The Open University
231 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c9c989a0
TH
232 */
233class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base {
234 protected function get_input_type() {
235 return 'radio';
236 }
237
238 protected function get_input_name(question_attempt $qa, $value) {
239 return $qa->get_qt_field_name('answer');
240 }
241
242 protected function get_input_value($value) {
243 return $value;
244 }
245
246 protected function get_input_id(question_attempt $qa, $value) {
247 return $qa->get_qt_field_name('answer' . $value);
248 }
249
250 protected function is_right(question_answer $ans) {
251 return $ans->fraction;
252 }
253
254 protected function prompt() {
255 return get_string('selectone', 'qtype_multichoice');
256 }
257
258 public function correct_response(question_attempt $qa) {
259 $question = $qa->get_question();
260
4358ee38
A
261 // Put all correct answers (100% grade) into $right.
262 $right = array();
c9c989a0
TH
263 foreach ($question->answers as $ansid => $ans) {
264 if (question_state::graded_state_for_fraction($ans->fraction) ==
265 question_state::$gradedright) {
4358ee38
A
266 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
267 $qa, 'question', 'answer', $ansid));
c9c989a0
TH
268 }
269 }
4358ee38 270 return $this->correct_choices($right);
c9c989a0 271 }
f6a77849
SL
272
273 public function after_choices(question_attempt $qa, question_display_options $options) {
274 // Only load the clear choice feature if it's not read only.
275 if ($options->readonly) {
276 return '';
277 }
278
279 $question = $qa->get_question();
280 $response = $question->get_response($qa);
281 $hascheckedchoice = false;
282 foreach ($question->get_order($qa) as $value => $ansid) {
283 if ($question->is_choice_selected($response, $value)) {
284 $hascheckedchoice = true;
285 break;
286 }
287 }
288
b59de78b
BB
289 $clearchoiceid = $this->get_input_id($qa, -1);
290 $clearchoicefieldname = $qa->get_qt_field_name('clearchoice');
291 $clearchoiceradioattrs = [
292 'type' => $this->get_input_type(),
293 'name' => $qa->get_qt_field_name('answer'),
294 'id' => $clearchoiceid,
295 'value' => -1,
296 'class' => 'sr-only'
297 ];
f6a77849 298
b59de78b 299 $cssclass = 'qtype_multichoice_clearchoice';
f6a77849 300 // When no choice selected during rendering, then hide the clear choice option.
233eafd8 301 $linktabindex = 0;
f6a77849 302 if (!$hascheckedchoice && $response == -1) {
b59de78b
BB
303 $cssclass .= ' sr-only';
304 $clearchoiceradioattrs['checked'] = 'checked';
233eafd8 305 $linktabindex = -1;
f6a77849 306 }
b59de78b
BB
307 // Adds an hidden radio that will be checked to give the impression the choice has been cleared.
308 $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs);
309 $clearchoiceradio .= html_writer::link('', get_string('clearchoice', 'qtype_multichoice'),
5d515390
BB
310 ['for' => $clearchoiceid, 'role' => 'button', 'tabindex' => $linktabindex,
311 'class' => 'btn btn-link ml-4 pl-1 mt-2']);
f6a77849 312
b59de78b
BB
313 // Now wrap the radio and label inside a div.
314 $result = html_writer::tag('div', $clearchoiceradio, ['id' => $clearchoicefieldname, 'class' => $cssclass]);
f6a77849
SL
315
316 // Load required clearchoice AMD module.
317 $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
b59de78b 318 [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]);
f6a77849 319
b59de78b 320 return $result;
f6a77849
SL
321 }
322
c9c989a0
TH
323}
324
325/**
326 * Subclass for generating the bits of output specific to multiple choice
327 * multi=select questions.
328 *
7764183a
TH
329 * @copyright 2009 The Open University
330 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c9c989a0
TH
331 */
332class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base {
f6a77849
SL
333 protected function after_choices(question_attempt $qa, question_display_options $options) {
334 return '';
335 }
336
c9c989a0
TH
337 protected function get_input_type() {
338 return 'checkbox';
339 }
340
341 protected function get_input_name(question_attempt $qa, $value) {
342 return $qa->get_qt_field_name('choice' . $value);
343 }
344
345 protected function get_input_value($value) {
346 return 1;
347 }
348
349 protected function get_input_id(question_attempt $qa, $value) {
350 return $this->get_input_name($qa, $value);
351 }
352
353 protected function is_right(question_answer $ans) {
354 if ($ans->fraction > 0) {
355 return 1;
356 } else {
357 return 0;
358 }
359 }
360
361 protected function prompt() {
362 return get_string('selectmulti', 'qtype_multichoice');
363 }
364
365 public function correct_response(question_attempt $qa) {
366 $question = $qa->get_question();
367
368 $right = array();
369 foreach ($question->answers as $ansid => $ans) {
370 if ($ans->fraction > 0) {
99942334
TH
371 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
372 $qa, 'question', 'answer', $ansid));
c9c989a0
TH
373 }
374 }
4358ee38 375 return $this->correct_choices($right);
c9c989a0
TH
376 }
377
378 protected function num_parts_correct(question_attempt $qa) {
379 if ($qa->get_question()->get_num_selected_choices($qa->get_last_qt_data()) >
380 $qa->get_question()->get_num_correct_choices()) {
381 return get_string('toomanyselected', 'qtype_multichoice');
382 }
383
384 return parent::num_parts_correct($qa);
385 }
386}