Vmeda.Online/src/core/features/question/classes/base-question-component.ts

859 lines
32 KiB
TypeScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Input, Output, EventEmitter, Component, Optional, Inject, ElementRef, OnInit } from '@angular/core';
import { CoreFileHelper } from '@services/file-helper';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreWSFile } from '@services/ws';
import { CoreIonicColorNames } from '@singletons/colors';
import { CoreLogger } from '@singletons/logger';
import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } from '../services/question-helper';
import { ContextLevel } from '@/core/constants';
/**
* Base class for components to render a question.
*/
@Component({
template: '',
})
export class CoreQuestionBaseComponent<T extends AddonModQuizQuestion = AddonModQuizQuestion> implements OnInit {
@Input() question?: T; // The question to render.
@Input() component?: string; // The component the question belongs to.
@Input() componentId?: number; // ID of the component the question belongs to.
@Input() attemptId?: number; // Attempt ID.
@Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline.
@Input() contextLevel?: ContextLevel; // The context level.
@Input() contextInstanceId?: number; // The instance ID related to the context.
@Input() courseId?: number; // The course the question belongs to (if any).
@Input() review?: boolean; // Whether the user is in review mode.
@Input() preferredBehaviour?: string; // Preferred behaviour.
@Output() buttonClicked = new EventEmitter<CoreQuestionBehaviourButton>(); // Will emit when a behaviour button is clicked.
@Output() onAbort = new EventEmitter<void>(); // Should emit an event if the question should be aborted.
protected logger: CoreLogger;
protected hostElement: HTMLElement;
constructor(@Optional() @Inject('') logName: string, elementRef: ElementRef) {
this.logger = CoreLogger.getInstance(logName);
this.hostElement = elementRef.nativeElement;
}
/**
* @inheritdoc
*/
ngOnInit(): void {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
}
this.init();
}
/**
* Initialize the question component, override it if needed.
*/
init(): void {
this.initComponent();
}
/**
* Initialize the component and the question text.
*
* @returns Element containing the question HTML, void if the data is not valid.
*/
initComponent(): void | HTMLElement {
if (!this.question) {
return;
}
this.hostElement.classList.add('core-question-container');
const questionElement = CoreDomUtils.convertToElement(this.question.html);
// Extract question text.
this.question.text = CoreDomUtils.getContentsOfElement(questionElement, '.qtext');
if (this.question.text === undefined) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
return questionElement;
}
/**
* Initialize a question component of type calculated or calculated simple.
*
* @returns Element containing the question HTML, void if the data is not valid.
*/
initCalculatedComponent(): void | HTMLElement {
// Treat the input text first.
const questionEl = this.initInputTextComponent();
if (!questionEl) {
return;
}
// Check if the question has a select for units.
if (this.treatCalculatedSelectUnits(questionEl)) {
return questionEl;
}
// Check if the question has radio buttons for units.
if (this.treatCalculatedRadioUnits(questionEl)) {
return questionEl;
}
return questionEl;
}
/**
* Treat a calculated question units in case they use radio buttons.
*
* @param questionEl Question HTML element.
* @returns True if question has units using radio buttons.
*/
protected treatCalculatedRadioUnits(questionEl: HTMLElement): boolean {
// Check if the question has radio buttons for units.
const radios = Array.from(questionEl.querySelectorAll<HTMLInputElement>('input[type="radio"]'));
if (!radios.length || !this.question) {
return false;
}
const question: AddonModQuizCalculatedQuestion = this.question;
question.options = [];
for (const i in radios) {
const radioEl = radios[i];
const option: AddonModQuizQuestionRadioOption = {
id: radioEl.id,
class: '',
name: radioEl.name,
value: radioEl.value,
checked: radioEl.checked,
disabled: radioEl.disabled,
};
// Get the label with the question text.
const label = questionEl.querySelector<HTMLLabelElement>('label[for="' + option.id + '"]');
question.optionsName = option.name;
if (!label || option.name === undefined || option.value === undefined) {
// Something went wrong when extracting the questions data. Abort.
this.logger.warn('Aborting because of an error parsing options.', question.slot, option.name);
CoreQuestionHelper.showComponentError(this.onAbort);
return true;
}
option.text = label.innerText;
if (radioEl.checked) {
// If the option is checked we use the model to select the one.
question.unit = option.value;
}
question.options.push(option);
}
// Check which one should be displayed first: the options or the input.
if (question.parsedSettings && question.parsedSettings.unitsleft !== null) {
question.optionsFirst = question.parsedSettings.unitsleft == '1';
} else {
const input = questionEl.querySelector<HTMLInputElement>('input[type="text"][name*=answer]');
question.optionsFirst =
questionEl.innerHTML.indexOf(input?.outerHTML || '') > questionEl.innerHTML.indexOf(radios[0].outerHTML);
}
return true;
}
/**
* Treat a calculated question units in case they use a select.
*
* @param questionEl Question HTML element.
* @returns True if question has units using a select.
*/
protected treatCalculatedSelectUnits(questionEl: HTMLElement): boolean {
// Check if the question has a select for units.
const select = questionEl.querySelector<HTMLSelectElement>('select[name*=unit]');
const options = select && Array.from(select.querySelectorAll('option'));
if (!select || !options?.length || !this.question) {
return false;
}
const question: AddonModQuizCalculatedQuestion = this.question;
const selectModel: AddonModQuizQuestionSelect = {
id: select.id,
name: select.name,
disabled: select.disabled,
options: [],
};
// Treat each option.
for (const i in options) {
const optionEl = options[i];
if (optionEl.value === undefined) {
this.logger.warn('Aborting because couldn\'t find input.', this.question?.slot);
CoreQuestionHelper.showComponentError(this.onAbort);
return true;
}
const option: AddonModQuizQuestionSelectOption = {
value: optionEl.value,
label: optionEl.innerHTML,
selected: optionEl.selected,
};
if (optionEl.selected) {
selectModel.selected = option.value;
}
selectModel.options.push(option);
}
if (!selectModel.selected) {
// No selected option, select the first one.
selectModel.selected = selectModel.options[0].value;
}
// Get the accessibility label.
const accessibilityLabel = questionEl.querySelector<HTMLLabelElement>('label[for="' + select.id + '"]');
selectModel.accessibilityLabel = accessibilityLabel?.innerHTML;
question.select = selectModel;
// Check which one should be displayed first: the select or the input.
if (question.parsedSettings && question.parsedSettings.unitsleft !== null) {
question.selectFirst = question.parsedSettings.unitsleft == '1';
} else {
const input = questionEl.querySelector<HTMLInputElement>('input[type="text"][name*=answer]');
question.selectFirst =
questionEl.innerHTML.indexOf(input?.outerHTML || '') > questionEl.innerHTML.indexOf(select.outerHTML);
}
return true;
}
/**
* Initialize a question component of type essay.
*
* @param review Whether we're in review mode.
* @returns Element containing the question HTML, void if the data is not valid.
*/
initEssayComponent(review?: boolean): void | HTMLElement {
const questionEl = this.initComponent();
if (!questionEl || !this.question) {
return;
}
const question: AddonModQuizEssayQuestion = this.question;
const answerDraftIdInput = questionEl.querySelector<HTMLInputElement>('input[name*="_answer:itemid"]');
if (question.parsedSettings) {
question.allowsAttachments = question.parsedSettings.attachments != '0';
question.allowsAnswerFiles = question.parsedSettings.responseformat == 'editorfilepicker';
question.isMonospaced = question.parsedSettings.responseformat == 'monospaced';
question.isPlainText = question.isMonospaced || question.parsedSettings.responseformat == 'plain';
question.hasInlineText = question.parsedSettings.responseformat != 'noinline';
} else {
question.allowsAttachments = !!questionEl.querySelector('div[id*=filemanager]');
question.allowsAnswerFiles = !!answerDraftIdInput;
question.isMonospaced = !!questionEl.querySelector('.qtype_essay_monospaced');
question.isPlainText = question.isMonospaced || !!questionEl.querySelector('.qtype_essay_plain');
}
if (review) {
// Search the answer and the attachments.
question.answer = CoreDomUtils.getContentsOfElement(questionEl, '.qtype_essay_response');
question.wordCountInfo = questionEl.querySelector('.answer > p')?.innerHTML;
if (question.parsedSettings) {
question.attachments = Array.from(
CoreQuestionHelper.getResponseFileAreaFiles(question, 'attachments'),
);
} else {
question.attachments = CoreQuestionHelper.getQuestionAttachmentsFromHtml(
CoreDomUtils.getContentsOfElement(questionEl, '.attachments') || '',
);
}
// Treat plagiarism.
this.handleEssayPlagiarism(questionEl);
return questionEl;
}
const textarea = questionEl.querySelector<HTMLTextAreaElement>('textarea[name*=_answer]');
question.hasDraftFiles = question.allowsAnswerFiles && CoreQuestionHelper.hasDraftFileUrls(questionEl.innerHTML);
if (!textarea && (question.hasInlineText || !question.allowsAttachments)) {
// Textarea not found, we might be in review. Search the answer and the attachments.
question.answer = CoreDomUtils.getContentsOfElement(questionEl, '.qtype_essay_response');
question.attachments = CoreQuestionHelper.getQuestionAttachmentsFromHtml(
CoreDomUtils.getContentsOfElement(questionEl, '.attachments') || '',
);
return questionEl;
}
if (textarea) {
const input = questionEl.querySelector<HTMLInputElement>('input[type="hidden"][name*=answerformat]');
let content = CoreTextUtils.decodeHTML(textarea.innerHTML || '');
if (question.hasDraftFiles && question.responsefileareas) {
content = CoreTextUtils.replaceDraftfileUrls(
CoreSites.getRequiredCurrentSite().getURL(),
content,
CoreQuestionHelper.getResponseFileAreaFiles(question, 'answer'),
).text;
}
question.textarea = {
id: textarea.id,
name: textarea.name,
text: content,
};
if (input) {
question.formatInput = {
name: input.name,
value: input.value,
};
}
}
if (answerDraftIdInput) {
question.answerDraftIdInput = {
name: answerDraftIdInput.name,
value: Number(answerDraftIdInput.value),
};
}
if (question.allowsAttachments) {
const attachmentsInput = questionEl.querySelector<HTMLInputElement>('.attachments input[name*=_attachments]');
const objectElement = questionEl.querySelector<HTMLObjectElement>('.attachments object');
const fileManagerUrl = objectElement && objectElement.data;
if (attachmentsInput) {
question.attachmentsDraftIdInput = {
name: attachmentsInput.name,
value: Number(attachmentsInput.value),
};
}
if (question.parsedSettings) {
question.attachmentsMaxFiles = Number(question.parsedSettings.attachments);
question.attachmentsAcceptedTypes = (<string[] | undefined> question.parsedSettings.filetypeslist)?.join(',');
}
if (fileManagerUrl) {
const params = CoreUrlUtils.extractUrlParams(fileManagerUrl);
const maxBytes = Number(params.maxbytes);
const areaMaxBytes = Number(params.areamaxbytes);
question.attachmentsMaxBytes = maxBytes === -1 || areaMaxBytes === -1 ?
Math.max(maxBytes, areaMaxBytes) : Math.min(maxBytes, areaMaxBytes);
}
}
return questionEl;
}
/**
* Handle plagiarism in an essay question.
*
* @param questionEl Element with the question html.
*/
protected handleEssayPlagiarism(questionEl: HTMLElement): void {
if (!this.question) {
return;
}
const question: AddonModQuizEssayQuestion = this.question;
const answerPlagiarism = questionEl.querySelector<HTMLSpanElement>('.answer .core_plagiarism_links');
if (answerPlagiarism) {
question.answerPlagiarism = answerPlagiarism.innerHTML;
}
const attachments = question.attachments;
if (!attachments?.length) {
return;
}
const attachmentsPlagiarisms = Array.from(
questionEl.querySelectorAll<HTMLSpanElement>('.attachments .core_plagiarism_links'),
);
const questionAttachmentsPlagiarisms: string[] = [];
attachmentsPlagiarisms.forEach((plagiarism) => {
// Search the URL of the attachment it affects.
const attachmentUrl = plagiarism.parentElement?.querySelector('a')?.href;
if (!attachmentUrl) {
return;
}
const position = attachments.findIndex((file) => CoreFileHelper.getFileUrl(file) === attachmentUrl);
if (position >= 0) {
questionAttachmentsPlagiarisms[position] = plagiarism.innerHTML;
}
});
question.attachmentsPlagiarisms = questionAttachmentsPlagiarisms;
}
/**
* Initialize a question component that uses the original question text with some basic treatment.
*
* @param contentSelector The selector to find the question content (text).
* @returns Element containing the question HTML, void if the data is not valid.
*/
initOriginalTextComponent(contentSelector: string): void | HTMLElement {
if (!this.question) {
return;
}
const element = CoreDomUtils.convertToElement(this.question.html);
// Get question content.
const content = element.querySelector<HTMLElement>(contentSelector);
if (!content) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
// Remove sequencecheck and validation error.
CoreDomUtils.removeElement(content, 'input[name*=sequencecheck]');
CoreDomUtils.removeElement(content, '.validationerror');
// Replace Moodle's correct/incorrect and feedback classes with our own.
CoreQuestionHelper.replaceCorrectnessClasses(element);
CoreQuestionHelper.replaceFeedbackClasses(element);
// Treat the correct/incorrect icons.
CoreQuestionHelper.treatCorrectnessIcons(element);
// Set the question text.
this.question.text = content.innerHTML;
return element;
}
/**
* Initialize a question component that has an input of type "text".
*
* @returns Element containing the question HTML, void if the data is not valid.
*/
initInputTextComponent(): void | HTMLElement {
const questionEl = this.initComponent();
if (!questionEl || !this.question) {
return;
}
// Get the input element.
const question: AddonModQuizTextQuestion = this.question;
const input = questionEl.querySelector<HTMLInputElement>('input[type="text"][name*=answer]');
if (!input) {
this.logger.warn('Aborting because couldn\'t find input.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
question.input = {
id: input.id,
name: input.name,
value: input.value,
readOnly: input.readOnly,
isInline: !!input.closest('.qtext'), // The answer can be inside the question text.
};
// Check if question is marked as correct.
if (input.classList.contains('incorrect')) {
question.input.correctClass = 'core-question-incorrect';
question.input.correctIcon = 'fas-xmark';
question.input.correctIconColor = CoreIonicColorNames.DANGER;
} else if (input.classList.contains('correct')) {
question.input.correctClass = 'core-question-correct';
question.input.correctIcon = 'fas-check';
question.input.correctIconColor = CoreIonicColorNames.SUCCESS;
} else if (input.classList.contains('partiallycorrect')) {
question.input.correctClass = 'core-question-partiallycorrect';
question.input.correctIcon = 'fas-square-check';
question.input.correctIconColor = CoreIonicColorNames.WARNING;
} else {
question.input.correctClass = '';
question.input.correctIcon = '';
question.input.correctIconColor = '';
}
if (question.input.isInline) {
// Handle correct/incorrect classes and icons.
const content = questionEl.querySelector<HTMLElement>('.qtext');
if (content) {
CoreQuestionHelper.replaceCorrectnessClasses(content);
CoreQuestionHelper.treatCorrectnessIcons(content);
question.text = content.innerHTML;
}
}
return questionEl;
}
/**
* Initialize a question component with a "match" behaviour.
*
* @returns Element containing the question HTML, void if the data is not valid.
*/
initMatchComponent(): void | HTMLElement {
const questionEl = this.initComponent();
if (!questionEl || !this.question) {
return;
}
// Find rows.
const question: AddonModQuizMatchQuestion = this.question;
const rows = Array.from(questionEl.querySelectorAll<HTMLTableRowElement>('table.answer tr'));
if (!rows || !rows.length) {
this.logger.warn('Aborting because couldn\'t find any row.', question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
question.rows = [];
for (const i in rows) {
const row = rows[i];
const columns = Array.from(row.querySelectorAll<HTMLTableCellElement>('td'));
if (!columns || columns.length < 2) {
this.logger.warn('Aborting because couldn\'t the right columns.', question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
// Get the select and the options.
const select = columns[1].querySelector<HTMLSelectElement>('select');
const options = Array.from(columns[1].querySelectorAll<HTMLOptionElement>('option'));
if (!select || !options || !options.length) {
this.logger.warn('Aborting because couldn\'t find select or options.', question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
const rowModel: AddonModQuizQuestionMatchSelect = {
id: select.id.replace(/:/g, '\\:'),
name: select.name,
disabled: select.disabled,
options: [],
text: columns[0].innerHTML, // Row's text should be in the first column.
};
// Check if answer is correct.
if (columns[1].className.indexOf('incorrect') >= 0) {
rowModel.isCorrect = 0;
} else if (columns[1].className.indexOf('correct') >= 0) {
rowModel.isCorrect = 1;
}
// Treat each option.
for (const j in options) {
const optionEl = options[j];
if (optionEl.value === undefined) {
this.logger.warn('Aborting because couldn\'t find the value of an option.', question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
const option: AddonModQuizQuestionSelectOption = {
value: optionEl.value,
label: optionEl.innerHTML,
selected: optionEl.selected,
};
if (option.selected) {
rowModel.selected = option.value;
}
rowModel.options.push(option);
}
// Get the accessibility label.
const accessibilityLabel = columns[1].querySelector<HTMLLabelElement>('label.accesshide');
rowModel.accessibilityLabel = accessibilityLabel?.innerHTML;
question.rows.push(rowModel);
}
question.loaded = true;
return questionEl;
}
/**
* Initialize a question component with a multiple choice (checkbox) or single choice (radio).
*
* @returns Element containing the question HTML, void if the data is not valid.
*/
initMultichoiceComponent(): void | HTMLElement {
const questionEl = this.initComponent();
if (!questionEl || !this.question) {
return;
}
// Get the prompt.
const question: AddonModQuizMultichoiceQuestion = this.question;
question.prompt = CoreDomUtils.getContentsOfElement(questionEl, '.prompt');
// Search radio buttons first (single choice).
let options = Array.from(questionEl.querySelectorAll<HTMLInputElement>('input[type="radio"]'));
if (!options || !options.length) {
// Radio buttons not found, it should be a multi answer. Search for checkbox.
question.multi = true;
options = Array.from(questionEl.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'));
if (!options || !options.length) {
// No checkbox found either. Abort.
this.logger.warn('Aborting because of no radio and checkbox found.', question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
}
question.options = [];
question.disabled = true;
for (const i in options) {
const element = options[i];
const option: AddonModQuizQuestionRadioOption = {
id: element.id,
class: '',
name: element.name,
value: element.value,
checked: element.checked,
disabled: element.disabled,
};
const parent = element.parentElement;
if (option.value == '-1') {
// It's the clear choice option, ignore it.
continue;
}
question.optionsName = option.name;
question.disabled = question.disabled && element.disabled;
// Get the label with the question text. Try the new format first.
const labelId = element.getAttribute('aria-labelledby');
let label = labelId ? questionEl.querySelector('#' + labelId.replace(/:/g, '\\:')) : undefined;
if (!label) {
// Not found, use the old format.
label = questionEl.querySelector('label[for="' + option.id + '"]');
}
option.class = label?.className || option.class;
// Check that we were able to successfully extract options required data.
if (!label || option.name === undefined || option.value === undefined) {
// Something went wrong when extracting the questions data. Abort.
this.logger.warn('Aborting because of an error parsing options.', question.slot, option.name);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
option.text = label.innerHTML;
if (element.checked) {
// If the option is checked and it's a single choice we use the model to select the one.
if (!question.multi) {
question.singleChoiceModel = option.value;
}
if (parent) {
// Check if answer is correct.
if (parent && parent.className.indexOf('incorrect') >= 0) {
option.isCorrect = 0;
} else if (parent && parent.className.indexOf('correct') >= 0) {
option.isCorrect = 1;
}
// Search the feedback.
const feedback = parent.querySelector('.specificfeedback');
if (feedback) {
option.feedback = feedback.innerHTML;
}
}
}
question.options.push(option);
}
return questionEl;
}
}
/**
* Any possible types of question.
*/
export type AddonModQuizQuestion = AddonModQuizCalculatedQuestion | AddonModQuizEssayQuestion | AddonModQuizTextQuestion |
AddonModQuizMatchQuestion | AddonModQuizMultichoiceQuestion;
/**
* Basic data for question.
*/
export type AddonModQuizQuestionBasicData = CoreQuestionQuestion & {
text?: string;
};
/**
* Data for calculated question.
*/
export type AddonModQuizCalculatedQuestion = AddonModQuizTextQuestion & {
select?: AddonModQuizQuestionSelect; // Select data if units use a select.
selectFirst?: boolean; // Whether the select is first or after the input.
options?: AddonModQuizQuestionRadioOption[]; // Options if units use radio buttons.
optionsName?: string; // Options name (for radio buttons).
unit?: string; // Option selected (for radio buttons).
optionsFirst?: boolean; // Whether the radio buttons are first or after the input.
};
/**
* Data for a select.
*/
export type AddonModQuizQuestionSelect = {
id: string;
name: string;
disabled: boolean;
options: AddonModQuizQuestionSelectOption[];
selected?: string;
accessibilityLabel?: string;
};
/**
* Data for each option in a select.
*/
export type AddonModQuizQuestionSelectOption = {
value: string;
label: string;
selected: boolean;
};
/**
* Data for radio button.
*/
export type AddonModQuizQuestionRadioOption = {
id: string;
name: string;
class: string;
value: string;
disabled: boolean;
checked: boolean;
text?: string;
isCorrect?: number;
feedback?: string;
};
/**
* Data for essay question.
*/
export type AddonModQuizEssayQuestion = AddonModQuizQuestionBasicData & {
allowsAttachments?: boolean; // Whether the question allows attachments.
allowsAnswerFiles?: boolean; // Whether the question allows adding files in the answer.
isMonospaced?: boolean; // Whether the answer is monospaced.
isPlainText?: boolean; // Whether the answer is plain text.
hasInlineText?: boolean; // // Whether the answer has inline text
answer?: string; // Question answer text.
attachments?: CoreWSFile[]; // Question answer attachments.
hasDraftFiles?: boolean; // Whether the question has draft files.
textarea?: AddonModQuizQuestionTextarea; // Textarea data.
formatInput?: { name: string; value: string }; // Format input data.
answerDraftIdInput?: { name: string; value: number }; // Answer draft id input data.
attachmentsDraftIdInput?: { name: string; value: number }; // Attachments draft id input data.
attachmentsMaxFiles?: number; // Max number of attachments.
attachmentsAcceptedTypes?: string; // Attachments accepted file types.
attachmentsMaxBytes?: number; // Max bytes for attachments.
answerPlagiarism?: string; // Plagiarism HTML for the answer.
attachmentsPlagiarisms?: string[]; // Plagiarism HTML for each attachment.
wordCountInfo?: string; // Info about word count.
};
/**
* Data for textarea.
*/
export type AddonModQuizQuestionTextarea = {
id: string;
name: string;
text: string;
};
/**
* Data for text question.
*/
export type AddonModQuizTextQuestion = AddonModQuizQuestionBasicData & {
input?: AddonModQuizQuestionTextInput;
};
/**
* Data for text input.
*/
export type AddonModQuizQuestionTextInput = {
id: string;
name: string;
value: string;
readOnly: boolean;
isInline: boolean;
correctClass?: string;
correctIcon?: string;
correctIconColor?: string;
};
/**
* Data for match question.
*/
export type AddonModQuizMatchQuestion = AddonModQuizQuestionBasicData & {
loaded?: boolean; // Whether the question is loaded.
rows?: AddonModQuizQuestionMatchSelect[]; // Data for each row.
};
/**
* Each select data for match questions.
*/
export type AddonModQuizQuestionMatchSelect = AddonModQuizQuestionSelect & {
text: string;
isCorrect?: number;
};
/**
* Data for multichoice question.
*/
export type AddonModQuizMultichoiceQuestion = AddonModQuizQuestionBasicData & {
prompt?: string; // Question prompt.
multi?: boolean; // Whether the question allows more than one selected answer.
options?: AddonModQuizQuestionRadioOption[]; // List of options.
disabled?: boolean; // Whether the question is disabled.
optionsName?: string; // Name to use for the options in single choice.
singleChoiceModel?: string; // Model for single choice.
};