MOBILE-4270 question: Improve base question component class
This commit is contained in:
		
							parent
							
								
									51bd21163a
								
							
						
					
					
						commit
						5cb74fca86
					
				| @ -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> | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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]); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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, | ||||
|             ); | ||||
|         } | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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, | ||||
|         ); | ||||
|  | ||||
| @ -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> | ||||
| 
 | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @ -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'); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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"> | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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'); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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 = ''; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user