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

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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'; import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -23,9 +23,7 @@ import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@feat
selector: 'addon-qtype-calculated', selector: 'addon-qtype-calculated',
templateUrl: 'addon-qtype-calculated.html', templateUrl: 'addon-qtype-calculated.html',
}) })
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent implements OnInit { export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent<AddonModQuizCalculatedQuestion> {
calcQuestion?: AddonModQuizCalculatedQuestion;
constructor(elementRef: ElementRef) { constructor(elementRef: ElementRef) {
super('AddonQtypeCalculatedComponent', elementRef); super('AddonQtypeCalculatedComponent', elementRef);
@ -34,10 +32,8 @@ export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent imp
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { init(): void {
this.initCalculatedComponent(); 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.topNode = this.container.querySelector<HTMLElement>('.addon-qtype-ddimageortext-container');
this.dragItemsArea = this.topNode?.querySelector<HTMLElement>('div.draghomes') || null; this.dragItemsArea = this.topNode?.querySelector<HTMLElement>('div.draghomes') || null;
if (!this.topNode) {
this.logger.error('ddimageortext container not found');
return;
}
if (this.dragItemsArea) { if (this.dragItemsArea) {
// On 3.9+ dragitems were removed. // On 3.9+ dragitems were removed.
const dragItems = this.topNode!.querySelector('div.dragitems'); const dragItems = this.topNode.querySelector('div.dragitems');
if (dragItems) { if (dragItems) {
// Remove empty div.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. // 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) { if (ddArea) {
// Move div.dropzones to div.ddarea. // Move div.dropzones to div.ddarea.
const dropZones = this.topNode!.querySelector('div.dropzones'); const dropZones = this.topNode.querySelector('div.dropzones');
if (dropZones) { if (dropZones) {
ddArea.appendChild(dropZones); ddArea.appendChild(dropZones);
} }
@ -738,7 +744,7 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
draghome.classList.add(`dragitemhomes${index}`); draghome.classList.add(`dragitemhomes${index}`);
}); });
} else { } 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 { getClassnameNumericSuffix(node: HTMLElement, prefix: string): number | undefined {
if (node.classList && node.classList.length) { if (node.classList && node.classList.length) {
const patt1 = new RegExp(`^${prefix}([0-9])+$`); const patt1 = new RegExp(`^${prefix}([0-9])+$`);
const patt2 = new RegExp('([0-9])+$');
for (let index = 0; index < node.classList.length; index++) { const classFound = Array.from(node.classList)
if (patt1.test(node.classList[index])) { .find((className) => patt1.test(className));
const match = patt2.exec(node.classList[index]);
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 --> <!-- 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-label>
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card"> <ion-card *ngIf="!question.readOnly" class="core-info-card">
<ion-item> <ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label> <ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
</ion-item> </ion-item>
</ion-card> </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()"> [contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded"> <div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" <core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()"> [text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
</core-format-text> </core-format-text>
</div> </div>
</div> </div>

View File

@ -12,11 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper'; import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext'; import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
/** /**
@ -27,9 +26,9 @@ import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
templateUrl: 'addon-qtype-ddimageortext.html', templateUrl: 'addon-qtype-ddimageortext.html',
styleUrls: ['ddimageortext.scss'], styleUrls: ['ddimageortext.scss'],
}) })
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy { export class AddonQtypeDdImageOrTextComponent
extends CoreQuestionBaseComponent<AddonModQuizDdImageOrTextQuestionData>
ddQuestion?: AddonModQuizDdImageOrTextQuestionData; implements OnDestroy {
protected questionInstance?: AddonQtypeDdImageOrTextQuestion; protected questionInstance?: AddonQtypeDdImageOrTextQuestion;
protected drops?: unknown[]; // The drop zones received in the init object of the question. protected drops?: unknown[]; // The drop zones received in the init object of the question.
@ -44,50 +43,47 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { init(): void {
if (!this.question) { if (!this.question) {
this.logger.warn('Aborting because of no question received.'); return;
return CoreQuestionHelper.showComponentError(this.onAbort);
} }
this.ddQuestion = this.question; const questionElement = this.initComponent();
if (!questionElement) {
const element = CoreDomUtils.convertToElement(this.ddQuestion.html); return;
}
// Get D&D area and question text. // Get D&D area and question text.
const ddArea = element.querySelector('.ddarea'); const ddArea = questionElement.querySelector('.ddarea');
if (!ddArea) {
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext'); this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
if (!ddArea || this.ddQuestion.text === undefined) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
return CoreQuestionHelper.showComponentError(this.onAbort); return CoreQuestionHelper.showComponentError(this.onAbort);
} }
// Set the D&D area HTML. // Set the D&D area HTML.
this.ddQuestion.ddArea = ddArea.outerHTML; this.question.ddArea = ddArea.outerHTML;
this.ddQuestion.readOnly = false; this.question.readOnly = false;
if (this.ddQuestion.initObjects) { if (this.question.initObjects) {
// Moodle version = 3.5. // Moodle version = 3.5.
if (this.ddQuestion.initObjects.drops !== undefined) { if (this.question.initObjects.drops !== undefined) {
this.drops = <unknown[]> this.ddQuestion.initObjects.drops; this.drops = <unknown[]> this.question.initObjects.drops;
} }
if (this.ddQuestion.initObjects.readonly !== undefined) { if (this.question.initObjects.readonly !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly; this.question.readOnly = !!this.question.initObjects.readonly;
} }
} else if (this.ddQuestion.amdArgs) { } else if (this.question.amdArgs) {
// Moodle version >= 3.6. // Moodle version >= 3.6.
if (this.ddQuestion.amdArgs[1] !== undefined) { if (this.question.amdArgs[1] !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[1]; this.question.readOnly = !!this.question.amdArgs[1];
} }
if (this.ddQuestion.amdArgs[2] !== undefined) { if (this.question.amdArgs[2] !== undefined) {
this.drops = <unknown[]> this.ddQuestion.amdArgs[2]; 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. * The question has been rendered.
*/ */
protected questionRendered(): void { protected questionRendered(): void {
if (!this.destroyed && this.ddQuestion) { if (!this.destroyed && this.question) {
// Create the instance. // Create the instance.
this.questionInstance = new AddonQtypeDdImageOrTextQuestion( this.questionInstance = new AddonQtypeDdImageOrTextQuestion(
this.hostElement, this.hostElement,
this.ddQuestion, this.question,
!!this.ddQuestion.readOnly, !!this.question.readOnly,
this.drops, 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 --> <!-- 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-label>
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card"> <ion-card *ngIf="!question.readOnly" class="core-info-card">
<ion-item> <ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label> <ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
</ion-item> </ion-item>
</ion-card> </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()"> [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded"> <div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" <core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()"> [text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
</core-format-text> </core-format-text>
</div> </div>
</div> </div>

View File

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

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper'; import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -27,12 +27,10 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
templateUrl: 'addon-qtype-ddwtos.html', templateUrl: 'addon-qtype-ddwtos.html',
styleUrls: ['ddwtos.scss'], styleUrls: ['ddwtos.scss'],
}) })
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy { export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent<AddonModQuizDdwtosQuestionData> implements OnDestroy {
@ViewChild('questiontext') questionTextEl?: ElementRef; @ViewChild('questiontext') questionTextEl?: ElementRef;
ddQuestion?: AddonModQuizDdwtosQuestionData;
protected questionInstance?: AddonQtypeDdwtosQuestion; protected questionInstance?: AddonQtypeDdwtosQuestion;
protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored). protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored).
protected destroyed = false; protected destroyed = false;
@ -46,52 +44,50 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { init(): void {
if (!this.question) { if (!this.question) {
this.logger.warn('Aborting because of no question received.'); return;
return CoreQuestionHelper.showComponentError(this.onAbort);
} }
this.ddQuestion = this.question; const questionElement = this.initComponent();
const element = CoreDomUtils.convertToElement(this.ddQuestion.html); if (!questionElement) {
return;
}
// Replace Moodle's correct/incorrect and feedback classes with our own. // Replace Moodle's correct/incorrect and feedback classes with our own.
CoreQuestionHelper.replaceCorrectnessClasses(element); CoreQuestionHelper.replaceCorrectnessClasses(questionElement);
CoreQuestionHelper.replaceFeedbackClasses(element); CoreQuestionHelper.replaceFeedbackClasses(questionElement);
// Treat the correct/incorrect icons. // Treat the correct/incorrect icons.
CoreQuestionHelper.treatCorrectnessIcons(element); CoreQuestionHelper.treatCorrectnessIcons(questionElement);
const answerContainer = element.querySelector('.answercontainer'); const answerContainer = questionElement.querySelector('.answercontainer');
if (!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); return CoreQuestionHelper.showComponentError(this.onAbort);
} }
this.ddQuestion.readOnly = answerContainer.classList.contains('readonly'); this.question.readOnly = answerContainer.classList.contains('readonly');
this.ddQuestion.answers = answerContainer.outerHTML; this.question.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);
}
// Get the inputs where the answers will be stored and add them to the question text. // 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) => { inputEls.forEach((inputEl) => {
this.ddQuestion!.text += inputEl.outerHTML; questionText += inputEl.outerHTML;
const id = inputEl.getAttribute('id'); const id = inputEl.getAttribute('id');
if (id) { if (id) {
this.inputIds.push(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. * The question has been rendered.
*/ */
protected async questionRendered(): Promise<void> { protected async questionRendered(): Promise<void> {
if (this.destroyed) { if (this.destroyed || !this.question) {
return; return;
} }
@ -129,8 +125,8 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
// Create the instance. // Create the instance.
this.questionInstance = new AddonQtypeDdwtosQuestion( this.questionInstance = new AddonQtypeDdwtosQuestion(
this.hostElement, this.hostElement,
this.ddQuestion!, this.question,
!!this.ddQuestion!.readOnly, !!this.question.readOnly,
this.inputIds, this.inputIds,
); );
@ -143,7 +139,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
this.courseId, 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 // See the License for the specific language governing permissions and
// limitations under the License. // 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 { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
/** /**
@ -23,7 +22,7 @@ import { CoreQuestionBaseComponent } from '@features/question/classes/base-quest
selector: 'addon-qtype-description', selector: 'addon-qtype-description',
templateUrl: 'addon-qtype-description.html', templateUrl: 'addon-qtype-description.html',
}) })
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent implements OnInit { export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent {
seenInput?: { name: string; value: string }; seenInput?: { name: string; value: string };
@ -34,20 +33,22 @@ export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent im
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { init(): void {
const questionEl = this.initComponent(); const questionEl = this.initComponent();
if (!questionEl) { if (!questionEl) {
return; return;
} }
// Get the "seen" hidden input. // Get the "seen" hidden input.
const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=seen]'); const input = questionEl.querySelector<HTMLInputElement>('input[type="hidden"][name*=seen]');
if (input) { if (!input) {
this.seenInput = { return;
name: input.name,
value: input.value,
};
} }
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. --> <!-- Question text. -->
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <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"> [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
@ -11,27 +11,26 @@
<!-- Editing the question. --> <!-- Editing the question. -->
<ng-container *ngIf="!review"> <ng-container *ngIf="!review">
<!-- Textarea. --> <!-- 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> <ion-label class="sr-only">{{ 'core.question.answer' | translate }}</ion-label>
<!-- "Format" and draftid hidden inputs --> <!-- "Format" and draftid hidden inputs -->
<input *ngIf="essayQuestion.formatInput" type="hidden" [name]="essayQuestion.formatInput.name" <input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value">
[value]="essayQuestion.formatInput.value"> <input *ngIf="question.answerDraftIdInput" type="hidden" [name]="question.answerDraftIdInput.name"
<input *ngIf="essayQuestion.answerDraftIdInput" type="hidden" [name]="essayQuestion.answerDraftIdInput.name" [value]="question.answerDraftIdInput.value">
[value]="essayQuestion.answerDraftIdInput.value">
<!-- Plain text textarea. --> <!-- Plain text textarea. -->
<ion-textarea *ngIf="essayQuestion.isPlainText" class="core-question-textarea" <ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}'
[ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name"
[attr.name]="essayQuestion.textarea.name" [ngModel]="essayQuestion.textarea.text"> [ngModel]="question.textarea.text">
</ion-textarea> </ion-textarea>
<!-- Rich text editor. --> <!-- Rich text editor. -->
<core-rich-text-editor *ngIf="!essayQuestion.isPlainText" placeholder="{{ 'core.question.answer' | translate }}" <core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
[control]="formControl" [name]="essayQuestion.textarea.name" [component]="component" [componentId]="componentId" [control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId"
[autoSave]="false"> [autoSave]="false">
</core-rich-text-editor> </core-rich-text-editor>
</ion-item> </ion-item>
<!-- Draft files not supported. --> <!-- 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-item class="ion-text-wrap core-danger-item">
<ion-label class="core-question-warning"> <ion-label class="core-question-warning">
{{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }} {{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }}
@ -39,7 +38,7 @@
</ion-item> </ion-item>
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <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"> [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
@ -47,15 +46,14 @@
</ng-container> </ng-container>
<!-- Attachments. --> <!-- Attachments. -->
<ng-container *ngIf="essayQuestion.allowsAttachments"> <ng-container *ngIf="question.allowsAttachments">
<core-attachments *ngIf="uploadFilesSupported && essayQuestion.attachmentsDraftIdInput" [files]="attachments" <core-attachments *ngIf="uploadFilesSupported && question.attachmentsDraftIdInput" [files]="attachments" [component]="component"
[component]="component" [componentId]="componentId" [maxSize]="essayQuestion.attachmentsMaxBytes" [componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles"
[maxSubmissions]="essayQuestion.attachmentsMaxFiles" [allowOffline]="offlineEnabled" [allowOffline]="offlineEnabled" [acceptedTypes]="question.attachmentsAcceptedTypes" [courseId]="courseId">
[acceptedTypes]="essayQuestion.attachmentsAcceptedTypes" [courseId]="courseId">
</core-attachments> </core-attachments>
<input *ngIf="essayQuestion.attachmentsDraftIdInput" type="hidden" [name]="essayQuestion.attachmentsDraftIdInput.name" <input *ngIf="question.attachmentsDraftIdInput" type="hidden" [name]="question.attachmentsDraftIdInput.name"
[value]="essayQuestion.attachmentsDraftIdInput.value"> [value]="question.attachmentsDraftIdInput.value">
<!-- Attachments not supported in this site. --> <!-- Attachments not supported in this site. -->
<ion-item *ngIf="!uploadFilesSupported" class="ion-text-wrap core-danger-item"> <ion-item *ngIf="!uploadFilesSupported" class="ion-text-wrap core-danger-item">
@ -69,36 +67,35 @@
<!-- Reviewing the question. --> <!-- Reviewing the question. -->
<ng-container *ngIf="review"> <ng-container *ngIf="review">
<!-- Answer to the question and attachments (reviewing). --> <!-- 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> <ion-label>
<core-format-text [ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' [component]="component" <core-format-text [ngClass]='{"core-monospaced": question.isMonospaced}' [component]="component" [componentId]="componentId"
[componentId]="componentId" [text]="essayQuestion.answer" [contextLevel]="contextLevel" [text]="question.answer" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- Word count info. --> <!-- Word count info. -->
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.wordCountInfo"> <ion-item class="ion-text-wrap" *ngIf="question.wordCountInfo">
<ion-label> <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"> [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- Answer plagiarism. --> <!-- Answer plagiarism. -->
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.answerPlagiarism"> <ion-item class="ion-text-wrap" *ngIf="question.answerPlagiarism">
<ion-label> <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"> [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- List of attachments when reviewing. --> <!-- List of attachments when reviewing. -->
<core-files *ngIf="essayQuestion.attachments" [files]="essayQuestion.attachments" [component]="component" <core-files *ngIf="question.attachments" [files]="question.attachments" [component]="component" [componentId]="componentId"
[componentId]="componentId" [extraHtml]="essayQuestion.attachmentsPlagiarisms"> [extraHtml]="question.attachmentsPlagiarisms">
</core-files> </core-files>
</ng-container> </ng-container>
</ion-list> </ion-list>

View File

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

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper'; 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', templateUrl: 'addon-qtype-gapselect.html',
styleUrls: ['gapselect.scss'], styleUrls: ['gapselect.scss'],
}) })
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent implements OnInit { export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent {
constructor(elementRef: ElementRef) { constructor(elementRef: ElementRef) {
super('AddonQtypeGapSelectComponent', elementRef); super('AddonQtypeGapSelectComponent', elementRef);
@ -34,7 +34,7 @@ export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent impl
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { init(): void {
this.initOriginalTextComponent('.qtext'); 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-item class="ion-text-wrap">
<ion-label> <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"> [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </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> <ion-label>
<core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId" <core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId"
[text]="row.text" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"> [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 // See the License for the specific language governing permissions and
// limitations under the License. // 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'; import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -24,9 +24,7 @@ import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/
templateUrl: 'addon-qtype-match.html', templateUrl: 'addon-qtype-match.html',
styleUrls: ['match.scss'], styleUrls: ['match.scss'],
}) })
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implements OnInit { export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent<AddonModQuizMatchQuestion> {
matchQuestion?: AddonModQuizMatchQuestion;
constructor(elementRef: ElementRef) { constructor(elementRef: ElementRef) {
super('AddonQtypeMatchComponent', elementRef); super('AddonQtypeMatchComponent', elementRef);
@ -35,9 +33,8 @@ export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implemen
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { init(): void {
this.initMatchComponent(); this.initMatchComponent();
this.matchQuestion = this.question;
} }
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper'; 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', templateUrl: 'addon-qtype-multianswer.html',
styleUrls: ['multianswer.scss'], styleUrls: ['multianswer.scss'],
}) })
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent implements OnInit { export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent {
constructor(elementRef: ElementRef) { constructor(elementRef: ElementRef) {
super('AddonQtypeMultiAnswerComponent', elementRef); super('AddonQtypeMultiAnswerComponent', elementRef);
@ -33,7 +33,7 @@ export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent im
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { init(): void {
this.initOriginalTextComponent('.formulation'); 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. --> <!-- Question text first. -->
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label>
<p> <p>
<core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.text" <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"> [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</p> </p>
<p *ngIf="multiQuestion.prompt"> <p *ngIf="question.prompt">
<core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.prompt" <core-format-text [component]="component" [componentId]="componentId" [text]="question.prompt" [contextLevel]="contextLevel"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"> [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- Checkbox for multiple choice. --> <!-- Checkbox for multiple choice. -->
<ng-container *ngIf="multiQuestion.multi"> <ng-container *ngIf="question.multi">
<ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options"> <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"> <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" <core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId"> [contextInstanceId]="contextInstanceId" [courseId]="courseId">
@ -44,8 +44,8 @@
</ng-container> </ng-container>
<!-- Radio buttons for single choice. --> <!-- Radio buttons for single choice. -->
<ion-radio-group *ngIf="!multiQuestion.multi" [(ngModel)]="multiQuestion.singleChoiceModel" [name]="multiQuestion.optionsName"> <ion-radio-group *ngIf="!question.multi" [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
<ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options"> <ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
<ion-label [class]="option.class"> <ion-label [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel" <core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId"> [contextInstanceId]="contextInstanceId" [courseId]="courseId">
@ -66,12 +66,12 @@
<ion-icon *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-times" color="danger" <ion-icon *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-times" color="danger"
[attr.aria-label]="'core.question.incorrect' | translate"></ion-icon> [attr.aria-label]="'core.question.incorrect' | translate"></ion-icon>
</ion-item> </ion-item>
<ion-button *ngIf="!multiQuestion.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline" <ion-button *ngIf="!question.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
[disabled]="!multiQuestion.singleChoiceModel" (click)="clear()" type="button"> [disabled]="!question.singleChoiceModel" (click)="clear()" type="button">
{{ 'addon.mod_quiz.clearchoice' | translate }} {{ 'addon.mod_quiz.clearchoice' | translate }}
</ion-button> </ion-button>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. --> <!-- 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-radio-group>
</ion-list> </ion-list>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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'; import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -24,9 +24,7 @@ import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@fea
templateUrl: 'addon-qtype-multichoice.html', templateUrl: 'addon-qtype-multichoice.html',
styleUrls: ['multichoice.scss'], styleUrls: ['multichoice.scss'],
}) })
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent implements OnInit { export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent<AddonModQuizMultichoiceQuestion> {
multiQuestion?: AddonModQuizMultichoiceQuestion;
constructor(elementRef: ElementRef) { constructor(elementRef: ElementRef) {
super('AddonQtypeMultichoiceComponent', elementRef); super('AddonQtypeMultichoiceComponent', elementRef);
@ -35,16 +33,19 @@ export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent im
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { init(): void {
this.initMultichoiceComponent(); this.initMultichoiceComponent();
this.multiQuestion = this.question;
} }
/** /**
* Clear selected choices. * Clear selected choices.
*/ */
clear(): void { 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. // 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) { for (const name in newAnswers) {
if (name.indexOf('choice') != -1) { if (name.indexOf('choice') !== -1) {
isSingle = false; isSingle = false;
if (!CoreUtils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, name)) { if (!CoreUtils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, name)) {
isMultiSame = false; isMultiSame = false;
@ -128,9 +128,9 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
if (isSingle) { if (isSingle) {
return this.isSameResponseSingle(prevAnswers, newAnswers); return this.isSameResponseSingle(prevAnswers, newAnswers);
} else {
return isMultiSame;
} }
return isMultiSame;
} }
/** /**
@ -151,10 +151,11 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
question: AddonModQuizMultichoiceQuestion, question: AddonModQuizMultichoiceQuestion,
answers: CoreQuestionsAnswers, answers: CoreQuestionsAnswers,
): void { ): 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 /* 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. */ 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-item class="ion-text-wrap addon-qtype-shortanswer-text">
<ion-label> <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"> [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item *ngIf="textQuestion.input && !textQuestion.input.isInline" <ion-item *ngIf="question.input && !question.input.isInline"
class="ion-text-wrap addon-qtype-shortanswer-input core-{{textQuestion.input.correctIconColor}}-item"> 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-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
<ion-input type="text" [placeholder]="textQuestion.input.readOnly ? '' : 'core.question.answer' | translate" <ion-input type="text" [placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate"
[attr.name]="textQuestion.input.name" [value]="textQuestion.input.value" autocorrect="off" [attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly">
[disabled]="textQuestion.input.readOnly">
</ion-input> </ion-input>
<ion-icon *ngIf="textQuestion.input.correctIcon" class="core-correct-icon" slot="end" [name]="textQuestion.input.correctIcon" <ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon" slot="end" [name]="question.input.correctIcon"
[color]="[textQuestion.input.correctIconColor]"> [color]="[question.input.correctIconColor]">
</ion-icon> </ion-icon>
</ion-item> </ion-item>
</ion-list> </ion-list>

View File

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

View File

@ -83,9 +83,9 @@ export class AddonQtypeTrueFalseHandlerService implements CoreQuestionHandler {
question: AddonModQuizMultichoiceQuestion, question: AddonModQuizMultichoiceQuestion,
answers: CoreQuestionsAnswers, answers: CoreQuestionsAnswers,
): void | Promise<void> { ): 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. // 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 // See the License for the specific language governing permissions and
// limitations under the License. // 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 { CoreFileHelper } from '@services/file-helper';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
@ -20,6 +20,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { CoreWSFile } from '@services/ws'; import { CoreWSFile } from '@services/ws';
import { CoreIonicColorNames } from '@singletons/colors';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } from '../services/question-helper'; import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } from '../services/question-helper';
@ -29,9 +30,9 @@ import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion }
@Component({ @Component({
template: '', 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() component?: string; // The component the question belongs to.
@Input() componentId?: number; // ID of the component the question belongs to. @Input() componentId?: number; // ID of the component the question belongs to.
@Input() attemptId?: number; // Attempt ID. @Input() attemptId?: number; // Attempt ID.
@ -52,6 +53,51 @@ export class CoreQuestionBaseComponent {
this.hostElement = elementRef.nativeElement; 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. * Initialize a question component of type calculated or calculated simple.
* *
@ -207,33 +253,6 @@ export class CoreQuestionBaseComponent {
return true; 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. * Initialize a question component of type essay.
* *
@ -409,15 +428,13 @@ export class CoreQuestionBaseComponent {
*/ */
initOriginalTextComponent(contentSelector: string): void | HTMLElement { initOriginalTextComponent(contentSelector: string): void | HTMLElement {
if (!this.question) { if (!this.question) {
this.logger.warn('Aborting because of no question received.'); return;
return CoreQuestionHelper.showComponentError(this.onAbort);
} }
const element = CoreDomUtils.convertToElement(this.question.html); const element = CoreDomUtils.convertToElement(this.question.html);
// Get question content. // Get question content.
const content = <HTMLElement> element.querySelector(contentSelector); const content = element.querySelector<HTMLElement>(contentSelector);
if (!content) { if (!content) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot); 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')) { if (input.classList.contains('incorrect')) {
question.input.correctClass = 'core-question-incorrect'; question.input.correctClass = 'core-question-incorrect';
question.input.correctIcon = 'fas-times'; question.input.correctIcon = 'fas-times';
question.input.correctIconColor = 'danger'; question.input.correctIconColor = CoreIonicColorNames.DANGER;
} else if (input.classList.contains('correct')) { } else if (input.classList.contains('correct')) {
question.input.correctClass = 'core-question-correct'; question.input.correctClass = 'core-question-correct';
question.input.correctIcon = 'fas-check'; question.input.correctIcon = 'fas-check';
question.input.correctIconColor = 'success'; question.input.correctIconColor = CoreIonicColorNames.SUCCESS;
} else if (input.classList.contains('partiallycorrect')) { } else if (input.classList.contains('partiallycorrect')) {
question.input.correctClass = 'core-question-partiallycorrect'; question.input.correctClass = 'core-question-partiallycorrect';
question.input.correctIcon = 'fas-check-square'; question.input.correctIcon = 'fas-check-square';
question.input.correctIconColor = 'warning'; question.input.correctIconColor = CoreIonicColorNames.WARNING;
} else { } else {
question.input.correctClass = ''; question.input.correctClass = '';
question.input.correctIcon = ''; question.input.correctIcon = '';