diff --git a/question/type/multianswer/lang/en/qtype_multianswer.php b/question/type/multianswer/lang/en/qtype_multianswer.php index 0fc1391f688..72aaee9d53c 100644 --- a/question/type/multianswer/lang/en/qtype_multianswer.php +++ b/question/type/multianswer/lang/en/qtype_multianswer.php @@ -38,6 +38,7 @@ $string['layoutselectinline'] = 'Drop-down menu in-line in the text'; $string['layoutundefined'] = 'Undefined layout'; $string['layoutvertical'] = 'Vertical column of radio buttons'; $string['missingsubquestion'] = 'This subquestion is missing from your system and cannot be displayed.'; +$string['multichoicex'] = 'Multiple choice {$a}'; $string['nooptionsforsubquestion'] = 'Unable to get options for question part # {$a->sub} (question->id={$a->id})'; $string['noquestions'] = 'The Cloze(multianswer) question "{$a}" does not contain any question'; $string['pleaseananswerallparts'] = 'Please answer all parts of the question.'; diff --git a/question/type/multianswer/renderer.php b/question/type/multianswer/renderer.php index a935ed9bd5a..4c612de461c 100644 --- a/question/type/multianswer/renderer.php +++ b/question/type/multianswer/renderer.php @@ -128,6 +128,7 @@ class qtype_multianswer_renderer extends qtype_renderer { } else { throw new coding_exception('Unexpected subquestion type.', $subq); } + /** @var qtype_multianswer_subq_renderer_base $renderer */ $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer); return $renderer->subquestion($qa, $options, $index, $subq); } @@ -147,6 +148,12 @@ class qtype_multianswer_renderer extends qtype_renderer { */ abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer { + /** @var int[] Stores the counts of answer instances for questions. */ + protected static $answercount = []; + + /** @var question_display_options Question display options instance for any necessary information for rendering the question. */ + protected $displayoptions; + abstract public function subquestion(question_attempt $qa, question_display_options $options, $index, question_graded_automatically $subq); @@ -198,6 +205,36 @@ abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer { return html_writer::tag('span', implode('
', $feedback), array('class' => 'feedbackspan accesshide')); } + + /** + * Generates a label for an answer field. + * + * If the question number is set ({@see qtype_renderer::$questionnumber}), the label will + * include the question number in order to indicate which question the answer field belongs to. + * + * @param string $langkey The lang string key for the lang string that does not include the question number. + * @param string $component The Frankenstyle component name. + * @return string + * @throws coding_exception + */ + protected function get_answer_label( + string $langkey = 'answerx', + string $component = 'question' + ): string { + // There may be multiple answer fields for a question, so we need to increment the answer fields in order to distinguish + // them from one another. + $questionnumber = $this->displayoptions->questionidentifier ?? ''; + $questionnumberindex = $questionnumber !== '' ? $questionnumber : 0; + if (isset(self::$answercount[$questionnumberindex][$langkey])) { + self::$answercount[$questionnumberindex][$langkey]++; + } else { + self::$answercount[$questionnumberindex][$langkey] = 1; + } + + $params = self::$answercount[$questionnumberindex][$langkey]; + + return $this->displayoptions->add_question_identifier_to_label(get_string($langkey, $component, $params)); + } } @@ -213,6 +250,8 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render public function subquestion(question_attempt $qa, question_display_options $options, $index, question_graded_automatically $subq) { + $this->displayoptions = $options; + $fieldprefix = 'sub' . $index . '_'; $fieldname = $fieldprefix . 'answer'; @@ -272,7 +311,8 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render s($correctanswer->answer), $options); $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline d-inline')); - $output .= html_writer::tag('label', get_string('answer'), + + $output .= html_writer::tag('label', $this->get_answer_label(), array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); $output .= html_writer::empty_tag('input', $inputattributes); $output .= $feedbackimg; @@ -296,6 +336,8 @@ class qtype_multianswer_multichoice_inline_renderer public function subquestion(question_attempt $qa, question_display_options $options, $index, question_graded_automatically $subq) { + $this->displayoptions = $options; + $fieldprefix = 'sub' . $index . '_'; $fieldname = $fieldprefix . 'answer'; @@ -340,7 +382,7 @@ class qtype_multianswer_multichoice_inline_renderer $qa, 'question', 'answer', $rightanswer->id), $options); $output = html_writer::start_tag('span', array('class' => 'subquestion')); - $output .= html_writer::tag('label', get_string('answer'), + $output .= html_writer::tag('label', $this->get_answer_label(), array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); $output .= $select; $output .= $feedbackimg; @@ -364,6 +406,8 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ public function subquestion(question_attempt $qa, question_display_options $options, $index, question_graded_automatically $subq) { + $this->displayoptions = $options; + $fieldprefix = 'sub' . $index . '_'; $fieldname = $fieldprefix . 'answer'; $response = $qa->get_last_qt_var($fieldname); @@ -371,6 +415,7 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ $inputattributes = array( 'type' => 'radio', 'name' => $qa->get_qt_field_name($fieldname), + 'class' => 'form-check-input', ); if ($options->readonly) { $inputattributes['disabled'] = 'disabled'; @@ -392,7 +437,7 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ unset($inputattributes['checked']); } - $class = 'r' . ($value % 2); + $class = 'form-check text-wrap text-break'; if ($options->correctness && $isselected) { $feedbackimg = $this->feedback_image($ans->fraction); $class .= ' ' . $this->feedback_class($ans->fraction); @@ -404,7 +449,7 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ $result .= html_writer::empty_tag('input', $inputattributes); $result .= html_writer::tag('label', $subq->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ansid), - array('for' => $inputattributes['id'])); + array('for' => $inputattributes['id'], 'class' => 'form-check-label text-body')); $result .= $feedbackimg; if ($options->feedback && $isselected && trim($ans->feedback)) { @@ -465,14 +510,17 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ * @return string HTML to go before all the choices. */ protected function all_choices_wrapper_start() { - return html_writer::start_tag('div', array('class' => 'answer')); + $wrapperstart = html_writer::start_tag('fieldset', array('class' => 'answer')); + $legendtext = $this->get_answer_label('multichoicex', 'qtype_multianswer'); + $wrapperstart .= html_writer::tag('legend', $legendtext, ['class' => 'sr-only']); + return $wrapperstart; } /** * @return string HTML to go after all the choices. */ protected function all_choices_wrapper_end() { - return html_writer::end_tag('div'); + return html_writer::end_tag('fieldset'); } } @@ -488,21 +536,22 @@ class qtype_multianswer_multichoice_horizontal_renderer extends qtype_multianswer_multichoice_vertical_renderer { protected function choice_wrapper_start($class) { - return html_writer::start_tag('td', array('class' => $class)); + return html_writer::start_tag('div', array('class' => $class . ' form-check-inline')); } protected function choice_wrapper_end() { - return html_writer::end_tag('td'); + return html_writer::end_tag('div'); } protected function all_choices_wrapper_start() { - return html_writer::start_tag('table', array('class' => 'answer')) . - html_writer::start_tag('tbody') . html_writer::start_tag('tr'); + $wrapperstart = html_writer::start_tag('fieldset', ['class' => 'answer']); + $captiontext = $this->get_answer_label('multichoicex', 'qtype_multianswer'); + $wrapperstart .= html_writer::tag('legend', $captiontext, ['class' => 'sr-only']); + return $wrapperstart; } protected function all_choices_wrapper_end() { - return html_writer::end_tag('tr') . html_writer::end_tag('tbody') . - html_writer::end_tag('table'); + return html_writer::end_tag('fieldset'); } }