MOBILE-4270 question: Improve base question component class

main
Pau Ferrer Ocaña 2022-11-30 16:43:47 +01:00
parent 51bd21163a
commit 5cb74fca86
23 changed files with 321 additions and 309 deletions

View File

@ -1,57 +1,57 @@
<ion-list class="addon-qtype-calculated-container" *ngIf="calcQuestion && (calcQuestion.text || calcQuestion.text === '')">
<ion-list class="addon-qtype-calculated-container" *ngIf="question && (question.text || question.text === '')">
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="calcQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<!-- Display unit options before the answer input. -->
<ng-container *ngIf="calcQuestion.options && calcQuestion.options.length && calcQuestion.optionsFirst">
<ng-container *ngIf="question.options && question.options.length && question.optionsFirst">
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
</ng-container>
<ion-item *ngIf="calcQuestion.input" class="ion-text-wrap core-{{calcQuestion.input.correctIconColor}}-item">
<ion-item *ngIf="question.input" class="ion-text-wrap core-{{question.input.correctIconColor}}-item">
<ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
<div class="flex-row">
<!-- Display unit select before the answer input. -->
<ng-container *ngIf="calcQuestion.select && calcQuestion.selectFirst">
<ng-container *ngIf="question.select && question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
<!-- Input to enter the answer. -->
<ion-input type="text" [attr.name]="calcQuestion.input.name"
[placeholder]="calcQuestion.input.readOnly ? '' : 'core.question.answer' | translate" [value]="calcQuestion.input.value"
[disabled]="calcQuestion.input.readOnly" autocorrect="off">
<ion-input type="text" [attr.name]="question.input.name"
[placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate" [value]="question.input.value"
[disabled]="question.input.readOnly" autocorrect="off">
</ion-input>
<!-- Display unit select after the answer input. -->
<ng-container *ngIf="calcQuestion.select && !calcQuestion.selectFirst">
<ng-container *ngIf="question.select && !question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
</div>
<ion-icon *ngIf="calcQuestion.input.correctIcon" class="core-correct-icon ion-align-self-center" slot="end"
[name]="calcQuestion.input.correctIcon" [color]="[calcQuestion.input.correctIconColor]">
<ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon ion-align-self-center" slot="end"
[name]="question.input.correctIcon" [color]="[question.input.correctIconColor]">
</ion-icon>
</ion-item>
<!-- Display unit options after the answer input. -->
<ng-container *ngIf="calcQuestion.options && calcQuestion.options.length && !calcQuestion.optionsFirst">
<ng-container *ngIf="question.options && question.options.length && !question.optionsFirst">
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
</ng-container>
</ion-list>
<!-- Template for units entered using a select. -->
<ng-template #selectUnits>
<label *ngIf="calcQuestion!.select!.accessibilityLabel" class="accesshide" for="{{calcQuestion!.select!.id}}">
{{ calcQuestion!.select!.accessibilityLabel }}
<label *ngIf="question!.select!.accessibilityLabel" class="accesshide" for="{{question!.select!.id}}">
{{ question!.select!.accessibilityLabel }}
</label>
<ion-select id="{{calcQuestion!.select!.id}}" [name]="calcQuestion!.select!.name" [(ngModel)]="calcQuestion!.select!.selected"
interface="action-sheet" [disabled]="calcQuestion!.select!.disabled" [slot]="calcQuestion?.selectFirst ? 'start' : 'end'"
<ion-select id="{{question!.select!.id}}" [name]="question!.select!.name" [(ngModel)]="question!.select!.selected"
interface="action-sheet" [disabled]="question!.select!.disabled" [slot]="question?.selectFirst ? 'start' : 'end'"
[interfaceOptions]="{header: 'addon.mod_quiz.unit' | translate}">
<ion-select-option *ngFor="let option of calcQuestion!.select!.options" [value]="option.value">
<ion-select-option *ngFor="let option of question!.select!.options" [value]="option.value">
{{option.label}}
</ion-select-option>
</ion-select>
@ -59,15 +59,15 @@
<!-- Template for units entered using radio buttons. -->
<ng-template #radioUnits>
<ion-radio-group [(ngModel)]="calcQuestion!.unit" [name]="calcQuestion!.optionsName">
<ion-item class="ion-text-wrap" *ngFor="let option of calcQuestion!.options">
<ion-radio-group [(ngModel)]="question!.unit" [name]="question!.optionsName">
<ion-item class="ion-text-wrap" *ngFor="let option of question!.options">
<ion-label>{{ option.text }}</ion-label>
<ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || calcQuestion!.input?.readOnly"
[color]="calcQuestion!.input?.correctIconColor">
<ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || question!.input?.readOnly"
[color]="question!.input?.correctIconColor">
</ion-radio>
</ion-item>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="calcQuestion!.unit" [attr.name]="calcQuestion!.optionsName">
<input type="hidden" [ngModel]="question!.unit" [attr.name]="question!.optionsName">
</ion-radio-group>
</ng-template>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -23,9 +23,7 @@ import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@feat
selector: 'addon-qtype-calculated',
templateUrl: 'addon-qtype-calculated.html',
})
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent implements OnInit {
calcQuestion?: AddonModQuizCalculatedQuestion;
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent<AddonModQuizCalculatedQuestion> {
constructor(elementRef: ElementRef) {
super('AddonQtypeCalculatedComponent', elementRef);
@ -34,10 +32,8 @@ export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent imp
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initCalculatedComponent();
this.calcQuestion = this.question;
}
}

View File

@ -708,9 +708,15 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
this.topNode = this.container.querySelector<HTMLElement>('.addon-qtype-ddimageortext-container');
this.dragItemsArea = this.topNode?.querySelector<HTMLElement>('div.draghomes') || null;
if (!this.topNode) {
this.logger.error('ddimageortext container not found');
return;
}
if (this.dragItemsArea) {
// On 3.9+ dragitems were removed.
const dragItems = this.topNode!.querySelector('div.dragitems');
const dragItems = this.topNode.querySelector('div.dragitems');
if (dragItems) {
// Remove empty div.dragitems.
@ -718,10 +724,10 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
}
// 3.6+ site, transform HTML so it has the same structure as in Moodle 3.5.
const ddArea = this.topNode!.querySelector('div.ddarea');
const ddArea = this.topNode.querySelector('div.ddarea');
if (ddArea) {
// Move div.dropzones to div.ddarea.
const dropZones = this.topNode!.querySelector('div.dropzones');
const dropZones = this.topNode.querySelector('div.dropzones');
if (dropZones) {
ddArea.appendChild(dropZones);
}
@ -738,7 +744,7 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
draghome.classList.add(`dragitemhomes${index}`);
});
} else {
this.dragItemsArea = this.topNode!.querySelector<HTMLElement>('div.dragitems');
this.dragItemsArea = this.topNode.querySelector<HTMLElement>('div.dragitems');
}
}
@ -797,14 +803,15 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
getClassnameNumericSuffix(node: HTMLElement, prefix: string): number | undefined {
if (node.classList && node.classList.length) {
const patt1 = new RegExp(`^${prefix}([0-9])+$`);
const patt2 = new RegExp('([0-9])+$');
for (let index = 0; index < node.classList.length; index++) {
if (patt1.test(node.classList[index])) {
const match = patt2.exec(node.classList[index]);
const classFound = Array.from(node.classList)
.find((className) => patt1.test(className));
return Number(match![0]);
}
if (classFound) {
const patt2 = new RegExp('([0-9])+$');
const match = patt2.exec(classFound);
return Number(match?.[0]);
}
}

View File

@ -1,24 +1,24 @@
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')" class="addon-qtype-ddimageortext-container">
<div *ngIf="question && (question.text || question.text === '')" class="addon-qtype-ddimageortext-container">
<!-- Content is outside the core-loading to let the script calculate drag items position -->
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
<core-loading [hideUntil]="question.loaded"></core-loading>
<ion-item class="ion-text-wrap" [hidden]="!ddQuestion.loaded">
<ion-item class="ion-text-wrap" [hidden]="!question.loaded">
<ion-label>
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
<ion-card *ngIf="!question.readOnly" class="core-info-card">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
</ion-item>
</ion-card>
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
</core-format-text>
</ion-label>
</ion-item>
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
<core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
</core-format-text>
</div>
</div>

View File

@ -12,11 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { Component, OnDestroy, ElementRef } from '@angular/core';
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
/**
@ -27,9 +26,9 @@ import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
templateUrl: 'addon-qtype-ddimageortext.html',
styleUrls: ['ddimageortext.scss'],
})
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
ddQuestion?: AddonModQuizDdImageOrTextQuestionData;
export class AddonQtypeDdImageOrTextComponent
extends CoreQuestionBaseComponent<AddonModQuizDdImageOrTextQuestionData>
implements OnDestroy {
protected questionInstance?: AddonQtypeDdImageOrTextQuestion;
protected drops?: unknown[]; // The drop zones received in the init object of the question.
@ -44,50 +43,47 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
return;
}
this.ddQuestion = this.question;
const element = CoreDomUtils.convertToElement(this.ddQuestion.html);
const questionElement = this.initComponent();
if (!questionElement) {
return;
}
// Get D&D area and question text.
const ddArea = element.querySelector('.ddarea');
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
if (!ddArea || this.ddQuestion.text === undefined) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
const ddArea = questionElement.querySelector('.ddarea');
if (!ddArea) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
// Set the D&D area HTML.
this.ddQuestion.ddArea = ddArea.outerHTML;
this.ddQuestion.readOnly = false;
this.question.ddArea = ddArea.outerHTML;
this.question.readOnly = false;
if (this.ddQuestion.initObjects) {
if (this.question.initObjects) {
// Moodle version = 3.5.
if (this.ddQuestion.initObjects.drops !== undefined) {
this.drops = <unknown[]> this.ddQuestion.initObjects.drops;
if (this.question.initObjects.drops !== undefined) {
this.drops = <unknown[]> this.question.initObjects.drops;
}
if (this.ddQuestion.initObjects.readonly !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly;
if (this.question.initObjects.readonly !== undefined) {
this.question.readOnly = !!this.question.initObjects.readonly;
}
} else if (this.ddQuestion.amdArgs) {
} else if (this.question.amdArgs) {
// Moodle version >= 3.6.
if (this.ddQuestion.amdArgs[1] !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[1];
if (this.question.amdArgs[1] !== undefined) {
this.question.readOnly = !!this.question.amdArgs[1];
}
if (this.ddQuestion.amdArgs[2] !== undefined) {
this.drops = <unknown[]> this.ddQuestion.amdArgs[2];
if (this.question.amdArgs[2] !== undefined) {
this.drops = <unknown[]> this.question.amdArgs[2];
}
}
this.ddQuestion.loaded = false;
this.question.loaded = false;
}
/**
@ -114,12 +110,12 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
* The question has been rendered.
*/
protected questionRendered(): void {
if (!this.destroyed && this.ddQuestion) {
if (!this.destroyed && this.question) {
// Create the instance.
this.questionInstance = new AddonQtypeDdImageOrTextQuestion(
this.hostElement,
this.ddQuestion,
!!this.ddQuestion.readOnly,
this.question,
!!this.question.readOnly,
this.drops,
);
}

View File

@ -1,23 +1,23 @@
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')" class="addon-qtype-ddmarker-container">
<div *ngIf="question && (question.text || question.text === '')" class="addon-qtype-ddmarker-container">
<!-- Content is outside the core-loading to let the script calculate drag items position -->
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
<core-loading [hideUntil]="question.loaded"></core-loading>
<ion-item class="ion-text-wrap" [hidden]="!ddQuestion.loaded">
<ion-item class="ion-text-wrap" [hidden]="!question.loaded">
<ion-label>
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
<ion-card *ngIf="!question.readOnly" class="core-info-card">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
</ion-item>
</ion-card>
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" #questiontext
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" #questiontext
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
</core-format-text>
</ion-label>
</ion-item>
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
<core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
</core-format-text>
</div>
</div>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -29,12 +29,12 @@ import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
templateUrl: 'addon-qtype-ddmarker.html',
styleUrls: ['ddmarker.scss'],
})
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
export class AddonQtypeDdMarkerComponent
extends CoreQuestionBaseComponent<AddonQtypeDdMarkerQuestionData>
implements OnDestroy {
@ViewChild('questiontext') questionTextEl?: ElementRef;
ddQuestion?: AddonQtypeDdMarkerQuestionData;
protected questionInstance?: AddonQtypeDdMarkerQuestion;
protected dropZones: unknown[] = []; // The drop zones received in the init object of the question.
protected imgSrc?: string; // Background image URL.
@ -49,65 +49,64 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
return;
}
this.ddQuestion = this.question;
const element = CoreDomUtils.convertToElement(this.question.html);
const questionElement = this.initComponent();
if (!questionElement) {
return;
}
// Get D&D area, form and question text.
const ddArea = element.querySelector('.ddarea');
const ddForm = element.querySelector('.ddform');
const ddArea = questionElement.querySelector('.ddarea');
const ddForm = questionElement.querySelector('.ddform');
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
if (!ddArea || !ddForm || this.ddQuestion.text === undefined) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
if (!ddArea || !ddForm) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
// Build the D&D area HTML.
this.ddQuestion.ddArea = ddArea.outerHTML;
this.question.ddArea = ddArea.outerHTML;
const wrongParts = element.querySelector('.wrongparts');
const wrongParts = questionElement.querySelector('.wrongparts');
if (wrongParts) {
this.ddQuestion.ddArea += wrongParts.outerHTML;
this.question.ddArea += wrongParts.outerHTML;
}
this.ddQuestion.ddArea += ddForm.outerHTML;
this.ddQuestion.readOnly = false;
this.question.ddArea += ddForm.outerHTML;
this.question.readOnly = false;
if (this.ddQuestion.initObjects) {
if (this.question.initObjects) {
// Moodle version = 3.5.
if (this.ddQuestion.initObjects.dropzones !== undefined) {
this.dropZones = <unknown[]> this.ddQuestion.initObjects.dropzones;
if (this.question.initObjects.dropzones !== undefined) {
this.dropZones = <unknown[]> this.question.initObjects.dropzones;
}
if (this.ddQuestion.initObjects.readonly !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly;
if (this.question.initObjects.readonly !== undefined) {
this.question.readOnly = !!this.question.initObjects.readonly;
}
} else if (this.ddQuestion.amdArgs) {
} else if (this.question.amdArgs) {
// Moodle version >= 3.6.
let nextIndex = 1;
// Moodle version >= 3.9, imgSrc is not specified, do not advance index.
if (this.ddQuestion.amdArgs[nextIndex] !== undefined && typeof this.ddQuestion.amdArgs[nextIndex] != 'boolean') {
this.imgSrc = <string> this.ddQuestion.amdArgs[nextIndex];
if (this.question.amdArgs[nextIndex] !== undefined && typeof this.question.amdArgs[nextIndex] !== 'boolean') {
this.imgSrc = <string> this.question.amdArgs[nextIndex];
nextIndex++;
}
if (this.ddQuestion.amdArgs[nextIndex] !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[nextIndex];
if (this.question.amdArgs[nextIndex] !== undefined) {
this.question.readOnly = !!this.question.amdArgs[nextIndex];
}
nextIndex++;
if (this.ddQuestion.amdArgs[nextIndex] !== undefined) {
this.dropZones = <unknown[]> this.ddQuestion.amdArgs[nextIndex];
if (this.question.amdArgs[nextIndex] !== undefined) {
this.dropZones = <unknown[]> this.question.amdArgs[nextIndex];
}
}
this.ddQuestion.loaded = false;
this.question.loaded = false;
}
/**
@ -134,9 +133,10 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
* The question has been rendered.
*/
protected async questionRendered(): Promise<void> {
if (this.destroyed) {
if (this.destroyed || !this.question) {
return;
}
// Download background image (3.6+ sites).
let imgSrc = this.imgSrc;
const site = CoreSites.getCurrentSite();
@ -160,8 +160,8 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
// Create the instance.
this.questionInstance = new AddonQtypeDdMarkerQuestion(
this.hostElement,
this.ddQuestion!,
!!this.ddQuestion!.readOnly,
this.question,
!!this.question.readOnly,
this.dropZones,
imgSrc,
);

View File

@ -1,20 +1,20 @@
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')">
<div *ngIf="question && (question.text || question.text === '')">
<!-- Content is outside the core-loading to let the script calculate drag items position -->
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
<core-loading [hideUntil]="question.loaded"></core-loading>
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<ion-card *ngIf="!question.readOnly" class="core-info-card">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
</ion-item>
</ion-card>
<div class="addon-qtype-ddwtos-container">
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" #questiontext (afterRender)="textRendered()">
</core-format-text>
<core-format-text *ngIf="ddQuestion.answers" [component]="component" [componentId]="componentId" [text]="ddQuestion.answers"
<core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers"
[filter]="false" (afterRender)="answersRendered()">
</core-format-text>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -27,12 +27,10 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
templateUrl: 'addon-qtype-ddwtos.html',
styleUrls: ['ddwtos.scss'],
})
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent<AddonModQuizDdwtosQuestionData> implements OnDestroy {
@ViewChild('questiontext') questionTextEl?: ElementRef;
ddQuestion?: AddonModQuizDdwtosQuestionData;
protected questionInstance?: AddonQtypeDdwtosQuestion;
protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored).
protected destroyed = false;
@ -46,52 +44,50 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
return;
}
this.ddQuestion = this.question;
const element = CoreDomUtils.convertToElement(this.ddQuestion.html);
const questionElement = this.initComponent();
if (!questionElement) {
return;
}
// Replace Moodle's correct/incorrect and feedback classes with our own.
CoreQuestionHelper.replaceCorrectnessClasses(element);
CoreQuestionHelper.replaceFeedbackClasses(element);
CoreQuestionHelper.replaceCorrectnessClasses(questionElement);
CoreQuestionHelper.replaceFeedbackClasses(questionElement);
// Treat the correct/incorrect icons.
CoreQuestionHelper.treatCorrectnessIcons(element);
CoreQuestionHelper.treatCorrectnessIcons(questionElement);
const answerContainer = element.querySelector('.answercontainer');
const answerContainer = questionElement.querySelector('.answercontainer');
if (!answerContainer) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
this.ddQuestion.readOnly = answerContainer.classList.contains('readonly');
this.ddQuestion.answers = answerContainer.outerHTML;
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
if (this.ddQuestion.text === undefined) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
this.question.readOnly = answerContainer.classList.contains('readonly');
this.question.answers = answerContainer.outerHTML;
// Get the inputs where the answers will be stored and add them to the question text.
const inputEls = <HTMLElement[]> Array.from(element.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])'));
const inputEls = Array.from(
questionElement.querySelectorAll<HTMLInputElement>('input[type="hidden"]:not([name*=sequencecheck])'),
);
let questionText = this.question.text;
inputEls.forEach((inputEl) => {
this.ddQuestion!.text += inputEl.outerHTML;
questionText += inputEl.outerHTML;
const id = inputEl.getAttribute('id');
if (id) {
this.inputIds.push(id);
}
});
this.ddQuestion.loaded = false;
this.question.text = questionText;
this.question.loaded = false;
}
/**
@ -118,7 +114,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
* The question has been rendered.
*/
protected async questionRendered(): Promise<void> {
if (this.destroyed) {
if (this.destroyed || !this.question) {
return;
}
@ -129,8 +125,8 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
// Create the instance.
this.questionInstance = new AddonQtypeDdwtosQuestion(
this.hostElement,
this.ddQuestion!,
!!this.ddQuestion!.readOnly,
this.question,
!!this.question.readOnly,
this.inputIds,
);
@ -143,7 +139,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
this.courseId,
);
this.ddQuestion!.loaded = true;
this.question.loaded = true;
}

View File

@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
/**
@ -23,7 +22,7 @@ import { CoreQuestionBaseComponent } from '@features/question/classes/base-quest
selector: 'addon-qtype-description',
templateUrl: 'addon-qtype-description.html',
})
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent implements OnInit {
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent {
seenInput?: { name: string; value: string };
@ -34,20 +33,22 @@ export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent im
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
const questionEl = this.initComponent();
if (!questionEl) {
return;
}
// Get the "seen" hidden input.
const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=seen]');
if (input) {
this.seenInput = {
name: input.name,
value: input.value,
};
const input = questionEl.querySelector<HTMLInputElement>('input[type="hidden"][name*=seen]');
if (!input) {
return;
}
this.seenInput = {
name: input.name,
value: input.value,
};
}
}

View File

@ -1,8 +1,8 @@
<ion-list *ngIf="essayQuestion && (essayQuestion.text || essayQuestion.text === '')">
<ion-list *ngIf="question && (question.text || question.text === '')">
<!-- Question text. -->
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
@ -11,27 +11,26 @@
<!-- Editing the question. -->
<ng-container *ngIf="!review">
<!-- Textarea. -->
<ion-item *ngIf="essayQuestion.textarea && (!essayQuestion.hasDraftFiles || uploadFilesSupported)">
<ion-item *ngIf="question.textarea && (!question.hasDraftFiles || uploadFilesSupported)">
<ion-label class="sr-only">{{ 'core.question.answer' | translate }}</ion-label>
<!-- "Format" and draftid hidden inputs -->
<input *ngIf="essayQuestion.formatInput" type="hidden" [name]="essayQuestion.formatInput.name"
[value]="essayQuestion.formatInput.value">
<input *ngIf="essayQuestion.answerDraftIdInput" type="hidden" [name]="essayQuestion.answerDraftIdInput.name"
[value]="essayQuestion.answerDraftIdInput.value">
<input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value">
<input *ngIf="question.answerDraftIdInput" type="hidden" [name]="question.answerDraftIdInput.name"
[value]="question.answerDraftIdInput.value">
<!-- Plain text textarea. -->
<ion-textarea *ngIf="essayQuestion.isPlainText" class="core-question-textarea"
[ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}"
[attr.name]="essayQuestion.textarea.name" [ngModel]="essayQuestion.textarea.text">
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}'
placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name"
[ngModel]="question.textarea.text">
</ion-textarea>
<!-- Rich text editor. -->
<core-rich-text-editor *ngIf="!essayQuestion.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
[control]="formControl" [name]="essayQuestion.textarea.name" [component]="component" [componentId]="componentId"
<core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
[control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId"
[autoSave]="false">
</core-rich-text-editor>
</ion-item>
<!-- Draft files not supported. -->
<ng-container *ngIf="essayQuestion.textarea && essayQuestion.hasDraftFiles && !uploadFilesSupported">
<ng-container *ngIf="question.textarea && question.hasDraftFiles && !uploadFilesSupported">
<ion-item class="ion-text-wrap core-danger-item">
<ion-label class="core-question-warning">
{{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }}
@ -39,7 +38,7 @@
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.textarea.text"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.textarea.text"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
@ -47,15 +46,14 @@
</ng-container>
<!-- Attachments. -->
<ng-container *ngIf="essayQuestion.allowsAttachments">
<core-attachments *ngIf="uploadFilesSupported && essayQuestion.attachmentsDraftIdInput" [files]="attachments"
[component]="component" [componentId]="componentId" [maxSize]="essayQuestion.attachmentsMaxBytes"
[maxSubmissions]="essayQuestion.attachmentsMaxFiles" [allowOffline]="offlineEnabled"
[acceptedTypes]="essayQuestion.attachmentsAcceptedTypes" [courseId]="courseId">
<ng-container *ngIf="question.allowsAttachments">
<core-attachments *ngIf="uploadFilesSupported && question.attachmentsDraftIdInput" [files]="attachments" [component]="component"
[componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles"
[allowOffline]="offlineEnabled" [acceptedTypes]="question.attachmentsAcceptedTypes" [courseId]="courseId">
</core-attachments>
<input *ngIf="essayQuestion.attachmentsDraftIdInput" type="hidden" [name]="essayQuestion.attachmentsDraftIdInput.name"
[value]="essayQuestion.attachmentsDraftIdInput.value">
<input *ngIf="question.attachmentsDraftIdInput" type="hidden" [name]="question.attachmentsDraftIdInput.name"
[value]="question.attachmentsDraftIdInput.value">
<!-- Attachments not supported in this site. -->
<ion-item *ngIf="!uploadFilesSupported" class="ion-text-wrap core-danger-item">
@ -69,36 +67,35 @@
<!-- Reviewing the question. -->
<ng-container *ngIf="review">
<!-- Answer to the question and attachments (reviewing). -->
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.answer || essayQuestion.answer == ''">
<ion-item class="ion-text-wrap" *ngIf="question.answer || question.answer == ''">
<ion-label>
<core-format-text [ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' [component]="component"
[componentId]="componentId" [text]="essayQuestion.answer" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
<core-format-text [ngClass]='{"core-monospaced": question.isMonospaced}' [component]="component" [componentId]="componentId"
[text]="question.answer" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<!-- Word count info. -->
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.wordCountInfo">
<ion-item class="ion-text-wrap" *ngIf="question.wordCountInfo">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.wordCountInfo"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.wordCountInfo"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<!-- Answer plagiarism. -->
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.answerPlagiarism">
<ion-item class="ion-text-wrap" *ngIf="question.answerPlagiarism">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.answerPlagiarism"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.answerPlagiarism"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<!-- List of attachments when reviewing. -->
<core-files *ngIf="essayQuestion.attachments" [files]="essayQuestion.attachments" [component]="component"
[componentId]="componentId" [extraHtml]="essayQuestion.attachmentsPlagiarisms">
<core-files *ngIf="question.attachments" [files]="question.attachments" [component]="component" [componentId]="componentId"
[extraHtml]="question.attachmentsPlagiarisms">
</core-files>
</ng-container>
</ion-list>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { FileEntry } from '@ionic-native/file/ngx';
@ -30,12 +30,11 @@ import { CoreFileEntry } from '@services/file-helper';
selector: 'addon-qtype-essay',
templateUrl: 'addon-qtype-essay.html',
})
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit {
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent<AddonModQuizEssayQuestion> {
formControl?: FormControl;
attachments?: CoreFileEntry[];
uploadFilesSupported = false;
essayQuestion?: AddonModQuizEssayQuestion;
constructor(elementRef: ElementRef, protected fb: FormBuilder) {
super('AddonQtypeEssayComponent', elementRef);
@ -44,14 +43,18 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
/**
* @inheritdoc
*/
ngOnInit(): void {
this.uploadFilesSupported = this.question?.responsefileareas !== undefined;
init(): void {
if (!this.question) {
return;
}
this.uploadFilesSupported = this.question.responsefileareas !== undefined;
this.initEssayComponent(this.review);
this.essayQuestion = this.question;
this.formControl = this.fb.control(this.essayQuestion?.textarea?.text);
this.formControl = this.fb.control(this.question?.textarea?.text);
if (this.essayQuestion?.allowsAttachments && this.uploadFilesSupported && !this.review) {
if (this.question?.allowsAttachments && this.uploadFilesSupported && !this.review) {
this.loadAttachments();
}
}
@ -62,10 +65,14 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
* @returns Promise resolved when done.
*/
async loadAttachments(): Promise<void> {
if (this.offlineEnabled && this.essayQuestion?.localAnswers?.attachments_offline) {
if (!this.question) {
return;
}
if (this.offlineEnabled && this.question.localAnswers?.attachments_offline) {
const attachmentsData: CoreFileUploaderStoreFilesResult = CoreTextUtils.parseJSON(
this.essayQuestion.localAnswers.attachments_offline,
this.question.localAnswers.attachments_offline,
{
online: [],
offline: 0,
@ -75,7 +82,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
if (attachmentsData.offline) {
offlineFiles = <FileEntry[]> await CoreQuestionHelper.getStoredQuestionFiles(
this.essayQuestion,
this.question,
this.component || '',
this.componentId || -1,
);
@ -83,12 +90,12 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
this.attachments = [...attachmentsData.online, ...offlineFiles];
} else {
this.attachments = Array.from(CoreQuestionHelper.getResponseFileAreaFiles(this.question!, 'attachments'));
this.attachments = Array.from(CoreQuestionHelper.getResponseFileAreaFiles(this.question, 'attachments'));
}
CoreFileSession.setFiles(
this.component || '',
CoreQuestion.getQuestionComponentId(this.question!, this.componentId || -1),
CoreQuestion.getQuestionComponentId(this.question, this.componentId || -1),
this.attachments,
);
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -25,7 +25,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper'
templateUrl: 'addon-qtype-gapselect.html',
styleUrls: ['gapselect.scss'],
})
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent implements OnInit {
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent {
constructor(elementRef: ElementRef) {
super('AddonQtypeGapSelectComponent', elementRef);
@ -34,7 +34,7 @@ export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent impl
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initOriginalTextComponent('.qtext');
}

View File

@ -1,12 +1,12 @@
<section class="addon-qtype-match-container" *ngIf="matchQuestion && matchQuestion.loaded">
<section class="addon-qtype-match-container" *ngIf="question && question.loaded">
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="matchQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngFor="let row of matchQuestion.rows">
<ion-item class="ion-text-wrap" *ngFor="let row of question.rows">
<ion-label>
<core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId"
[text]="row.text" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -24,9 +24,7 @@ import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/
templateUrl: 'addon-qtype-match.html',
styleUrls: ['match.scss'],
})
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implements OnInit {
matchQuestion?: AddonModQuizMatchQuestion;
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent<AddonModQuizMatchQuestion> {
constructor(elementRef: ElementRef) {
super('AddonQtypeMatchComponent', elementRef);
@ -35,9 +33,8 @@ export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implemen
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initMatchComponent();
this.matchQuestion = this.question;
}
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -24,7 +24,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper'
templateUrl: 'addon-qtype-multianswer.html',
styleUrls: ['multianswer.scss'],
})
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent {
constructor(elementRef: ElementRef) {
super('AddonQtypeMultiAnswerComponent', elementRef);
@ -33,7 +33,7 @@ export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent im
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initOriginalTextComponent('.formulation');
}

View File

@ -1,23 +1,23 @@
<ion-list *ngIf="multiQuestion && (multiQuestion.text || multiQuestion.text === '')">
<ion-list *ngIf="question && (question.text || question.text === '')">
<!-- Question text first. -->
<ion-item class="ion-text-wrap">
<ion-label>
<p>
<core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.text"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</p>
<p *ngIf="multiQuestion.prompt">
<core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.prompt"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
<p *ngIf="question.prompt">
<core-format-text [component]="component" [componentId]="componentId" [text]="question.prompt" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
<!-- Checkbox for multiple choice. -->
<ng-container *ngIf="multiQuestion.multi">
<ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options">
<ng-container *ngIf="question.multi">
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
<ion-label [color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")' [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
@ -44,8 +44,8 @@
</ng-container>
<!-- Radio buttons for single choice. -->
<ion-radio-group *ngIf="!multiQuestion.multi" [(ngModel)]="multiQuestion.singleChoiceModel" [name]="multiQuestion.optionsName">
<ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options">
<ion-radio-group *ngIf="!question.multi" [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
<ion-label [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
@ -66,12 +66,12 @@
<ion-icon *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-times" color="danger"
[attr.aria-label]="'core.question.incorrect' | translate"></ion-icon>
</ion-item>
<ion-button *ngIf="!multiQuestion.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
[disabled]="!multiQuestion.singleChoiceModel" (click)="clear()" type="button">
<ion-button *ngIf="!question.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
[disabled]="!question.singleChoiceModel" (click)="clear()" type="button">
{{ 'addon.mod_quiz.clearchoice' | translate }}
</ion-button>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="multiQuestion.singleChoiceModel" [attr.name]="multiQuestion.optionsName">
<input type="hidden" [ngModel]="question.singleChoiceModel" [attr.name]="question.optionsName">
</ion-radio-group>
</ion-list>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -24,9 +24,7 @@ import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@fea
templateUrl: 'addon-qtype-multichoice.html',
styleUrls: ['multichoice.scss'],
})
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent implements OnInit {
multiQuestion?: AddonModQuizMultichoiceQuestion;
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent<AddonModQuizMultichoiceQuestion> {
constructor(elementRef: ElementRef) {
super('AddonQtypeMultichoiceComponent', elementRef);
@ -35,16 +33,19 @@ export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent im
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initMultichoiceComponent();
this.multiQuestion = this.question;
}
/**
* Clear selected choices.
*/
clear(): void {
this.multiQuestion!.singleChoiceModel = undefined;
if (!this.question) {
return;
}
this.question.singleChoiceModel = undefined;
}
}

View File

@ -117,7 +117,7 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
// To know if it's single or multi answer we need to search for answers with "choice" in the name.
for (const name in newAnswers) {
if (name.indexOf('choice') != -1) {
if (name.indexOf('choice') !== -1) {
isSingle = false;
if (!CoreUtils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, name)) {
isMultiSame = false;
@ -128,9 +128,9 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
if (isSingle) {
return this.isSameResponseSingle(prevAnswers, newAnswers);
} else {
return isMultiSame;
}
return isMultiSame;
}
/**
@ -151,10 +151,11 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
question: AddonModQuizMultichoiceQuestion,
answers: CoreQuestionsAnswers,
): void {
if (question && !question.multi && answers[question.optionsName!] !== undefined && !answers[question.optionsName!]) {
if (question && !question.multi &&
question.optionsName && answers[question.optionsName] !== undefined && !answers[question.optionsName]) {
/* It's a single choice and the user hasn't answered. Delete the answer because
sending an empty string (default value) will mark the first option as selected. */
delete answers[question.optionsName!];
delete answers[question.optionsName];
}
}

View File

@ -1,20 +1,19 @@
<ion-list *ngIf="textQuestion && (textQuestion.text || textQuestion.text === '')">
<ion-list *ngIf="question && (question.text || question.text === '')">
<ion-item class="ion-text-wrap addon-qtype-shortanswer-text">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="textQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item *ngIf="textQuestion.input && !textQuestion.input.isInline"
class="ion-text-wrap addon-qtype-shortanswer-input core-{{textQuestion.input.correctIconColor}}-item">
<ion-item *ngIf="question.input && !question.input.isInline"
class="ion-text-wrap addon-qtype-shortanswer-input core-{{question.input.correctIconColor}}-item">
<ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
<ion-input type="text" [placeholder]="textQuestion.input.readOnly ? '' : 'core.question.answer' | translate"
[attr.name]="textQuestion.input.name" [value]="textQuestion.input.value" autocorrect="off"
[disabled]="textQuestion.input.readOnly">
<ion-input type="text" [placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate"
[attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly">
</ion-input>
<ion-icon *ngIf="textQuestion.input.correctIcon" class="core-correct-icon" slot="end" [name]="textQuestion.input.correctIcon"
[color]="[textQuestion.input.correctIconColor]">
<ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon" slot="end" [name]="question.input.correctIcon"
[color]="[question.input.correctIconColor]">
</ion-icon>
</ion-item>
</ion-list>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { AddonModQuizTextQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -24,9 +24,7 @@ import { AddonModQuizTextQuestion, CoreQuestionBaseComponent } from '@features/q
templateUrl: 'addon-qtype-shortanswer.html',
styleUrls: ['shortanswer.scss'],
})
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
textQuestion?: AddonModQuizTextQuestion;
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent<AddonModQuizTextQuestion> {
constructor(elementRef: ElementRef) {
super('AddonQtypeShortAnswerComponent', elementRef);
@ -35,9 +33,8 @@ export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent im
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initInputTextComponent();
this.textQuestion = this.question;
}
}

View File

@ -83,9 +83,9 @@ export class AddonQtypeTrueFalseHandlerService implements CoreQuestionHandler {
question: AddonModQuizMultichoiceQuestion,
answers: CoreQuestionsAnswers,
): void | Promise<void> {
if (question && answers[question.optionsName!] !== undefined && !answers[question.optionsName!]) {
if (question && question.optionsName && answers[question.optionsName] !== undefined && !answers[question.optionsName]) {
// The user hasn't answered. Delete the answer to prevent marking one of the answers automatically.
delete answers[question.optionsName!];
delete answers[question.optionsName];
}
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Input, Output, EventEmitter, Component, Optional, Inject, ElementRef } from '@angular/core';
import { Input, Output, EventEmitter, Component, Optional, Inject, ElementRef, OnInit } from '@angular/core';
import { CoreFileHelper } from '@services/file-helper';
import { CoreSites } from '@services/sites';
@ -20,6 +20,7 @@ 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';
@ -29,9 +30,9 @@ import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion }
@Component({
template: '',
})
export class CoreQuestionBaseComponent {
export class CoreQuestionBaseComponent<T extends AddonModQuizQuestion = AddonModQuizQuestion> implements OnInit {
@Input() question?: AddonModQuizQuestion; // The question to render.
@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.
@ -52,6 +53,51 @@ export class CoreQuestionBaseComponent {
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.
*
@ -207,33 +253,6 @@ export class CoreQuestionBaseComponent {
return true;
}
/**
* 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) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
}
this.hostElement.classList.add('core-question-container');
const element = CoreDomUtils.convertToElement(this.question.html);
// Extract question text.
this.question.text = CoreDomUtils.getContentsOfElement(element, '.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 element;
}
/**
* Initialize a question component of type essay.
*
@ -409,15 +428,13 @@ export class CoreQuestionBaseComponent {
*/
initOriginalTextComponent(contentSelector: string): void | HTMLElement {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
return;
}
const element = CoreDomUtils.convertToElement(this.question.html);
// Get question content.
const content = <HTMLElement> element.querySelector(contentSelector);
const content = element.querySelector<HTMLElement>(contentSelector);
if (!content) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
@ -473,15 +490,15 @@ export class CoreQuestionBaseComponent {
if (input.classList.contains('incorrect')) {
question.input.correctClass = 'core-question-incorrect';
question.input.correctIcon = 'fas-times';
question.input.correctIconColor = 'danger';
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 = 'success';
question.input.correctIconColor = CoreIonicColorNames.SUCCESS;
} else if (input.classList.contains('partiallycorrect')) {
question.input.correctClass = 'core-question-partiallycorrect';
question.input.correctIcon = 'fas-check-square';
question.input.correctIconColor = 'warning';
question.input.correctIconColor = CoreIonicColorNames.WARNING;
} else {
question.input.correctClass = '';
question.input.correctIcon = '';