MOBILE-2348 quiz: Fix question types

main
Dani Palou 2018-04-04 09:00:29 +02:00
parent d5c3523023
commit 5878d7c3eb
27 changed files with 562 additions and 137 deletions

View File

@ -12,7 +12,7 @@
<form name="itemEdit" (ngSubmit)="addNote()"> <form name="itemEdit" (ngSubmit)="addNote()">
<ion-item> <ion-item>
<ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label> <ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label>
<ion-select [(ngModel)]="publishState" name="publishState"> <ion-select [(ngModel)]="publishState" name="publishState" interface="popover">
<ion-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-option> <ion-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-option>
<ion-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-option> <ion-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-option>
<ion-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-option> <ion-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-option>

View File

@ -1,6 +1,16 @@
<ion-item text-wrap class="addon-qbehaviour-deferredcbm-certainty-title" *ngIf="question.behaviourCertaintyOptions && question.behaviourCertaintyOptions.length"> <div *ngIf="question.behaviourCertaintyOptions && question.behaviourCertaintyOptions.length">
<p>{{ 'core.question.certainty' | translate }}</p> <ion-item text-wrap class="addon-qbehaviour-deferredcbm-certainty-title" >
</ion-item> <p>{{ 'core.question.certainty' | translate }}</p>
<ion-radio *ngFor="let option of question.behaviourCertaintyOptions" id="{{option.id}}" name="{{option.name}}" [ngModel]="question.behaviourCertaintySelected" [value]="option.value" [disabled]="option.disabled"> </ion-item>
<p><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></p> <div radio-group [(ngModel)]="question.behaviourCertaintySelected" [name]="question.behaviourCertaintyOptions[0].name">
</ion-radio> <ion-item text-wrap *ngFor="let option of question.behaviourCertaintyOptions">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text>
</ion-label>
<ion-radio id="{{option.id}}" [value]="option.value" [disabled]="option.disabled"></ion-radio>
</ion-item>
</div>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="question.behaviourCertaintySelected" [attr.name]="question.behaviourCertaintyOptions[0].name">
</div>

View File

@ -8,24 +8,26 @@
<ng-container *ngTemplateOutlet="radioUnits"></ng-container> <ng-container *ngTemplateOutlet="radioUnits"></ng-container>
</ng-container> </ng-container>
<ion-item text-wrap> <ion-item text-wrap ion-grid>
<ion-row> <ion-grid item-content>
<!-- Display unit select before the answer input. --> <ion-row>
<ng-container *ngIf="question.select && question.selectFirst"> <!-- Display unit select before the answer input. -->
<ng-container *ngTemplateOutlet="selectUnits"></ng-container> <ng-container *ngIf="question.select && question.selectFirst">
</ng-container> <ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
<!-- Input to enter the answer. --> <!-- Input to enter the answer. -->
<ion-col> <ion-col>
<ion-input type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" [disabled]="question.input.readOnly" [ngClass]='{"core-question-answer-correct": question.input.isCorrect === 1, "core-question-answer-incorrect": question.input.isCorrect === 0}' autocorrect="off"> <ion-input type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" [disabled]="question.input.readOnly" [ngClass]='{"core-question-answer-correct": question.input.isCorrect === 1, "core-question-answer-incorrect": question.input.isCorrect === 0}' autocorrect="off">
</ion-input> </ion-input>
</ion-col> </ion-col>
<!-- Display unit select after the answer input. --> <!-- Display unit select after the answer input. -->
<ng-container *ngIf="question.select && !question.selectFirst"> <ng-container *ngIf="question.select && !question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container> <ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container> </ng-container>
</ion-row> </ion-row>
</ion-grid>
</ion-item> </ion-item>
<!-- Display unit options after the answer input. --> <!-- Display unit options after the answer input. -->
@ -38,18 +40,26 @@
<ng-template #selectUnits> <ng-template #selectUnits>
<ion-col> <ion-col>
<label *ngIf="question.select.accessibilityLabel" class="accesshide" for="{{question.select.id}}">{{ question.select.accessibilityLabel }}</label> <label *ngIf="question.select.accessibilityLabel" class="accesshide" for="{{question.select.id}}">{{ question.select.accessibilityLabel }}</label>
<ion-select id="{{question.select.id}}" [name]="question.select.name" [ngModel]="question.select.selected"> <ion-select id="{{question.select.id}}" [name]="question.select.name" [(ngModel)]="question.select.selected" interface="popover">
<ion-option *ngFor="let option of question.select.options" [value]="option.value">{{option.label}}</ion-option> <ion-option *ngFor="let option of question.select.options" [value]="option.value">{{option.label}}</ion-option>
</ion-select> </ion-select>
<!-- @todo: select fix? -->
<!-- ion-select doesn't use a select. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="question.select.selected" [attr.name]="question.select.name">
</ion-col> </ion-col>
</ng-template> </ng-template>
<!-- Template for units entered using radio buttons. --> <!-- Template for units entered using radio buttons. -->
<ng-template #radioUnits> <ng-template #radioUnits>
<div radio-group [ngModel]="question.unit" [name]="question.optionsName"> <div radio-group [(ngModel)]="question.unit" [name]="question.optionsName">
<ion-radio *ngFor="let option of question.options" [value]="option.value" [disabled]="option.disabled"> <ion-item text-wrap *ngFor="let option of question.options">
<p>{{option.text}}</p> <ion-label>
</ion-radio> <p>{{option.text}}</p>
</ion-label>
<ion-radio [value]="option.value" [disabled]="option.disabled"></ion-radio>
</ion-item>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="question.unit" [attr.name]="question.optionsName">
</div> </div>
</ng-template> </ng-template>

View File

@ -45,6 +45,7 @@ export class AddonQtypeDdImageOrTextQuestion {
protected topNode: HTMLElement; protected topNode: HTMLElement;
protected proportion = 1; protected proportion = 1;
protected selected: HTMLElement; // Selected element (being "dragged"). protected selected: HTMLElement; // Selected element (being "dragged").
protected resizeFunction;
/** /**
* Create the this. * Create the this.
@ -182,7 +183,10 @@ export class AddonQtypeDdImageOrTextQuestion {
*/ */
destroy(): void { destroy(): void {
this.stopPolling(); this.stopPolling();
window.removeEventListener('resize', this.resizeFunction);
if (this.resizeFunction) {
window.removeEventListener('resize', this.resizeFunction);
}
} }
/** /**
@ -192,7 +196,7 @@ export class AddonQtypeDdImageOrTextQuestion {
* @return {AddonQtypeDdImageOrTextQuestionDocStructure} The object. * @return {AddonQtypeDdImageOrTextQuestionDocStructure} The object.
*/ */
docStructure(slot: number): AddonQtypeDdImageOrTextQuestionDocStructure { docStructure(slot: number): AddonQtypeDdImageOrTextQuestionDocStructure {
const topNode = <HTMLElement> this.container.querySelector(`#core-question-${slot} .addon-qtype-ddimageortext-container`), const topNode = <HTMLElement> this.container.querySelector('.addon-qtype-ddimageortext-container'),
dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems'), dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems'),
doc: AddonQtypeDdImageOrTextQuestionDocStructure = {}; doc: AddonQtypeDdImageOrTextQuestionDocStructure = {};
@ -456,6 +460,7 @@ export class AddonQtypeDdImageOrTextQuestion {
this.pollForImageLoad(); this.pollForImageLoad();
}); });
this.resizeFunction = this.repositionDragsForQuestion.bind(this);
window.addEventListener('resize', this.resizeFunction); window.addEventListener('resize', this.resizeFunction);
} }
@ -637,13 +642,6 @@ export class AddonQtypeDdImageOrTextQuestion {
} }
} }
/**
* Function to call when the window is resized.
*/
resizeFunction(): void {
this.repositionDragsForQuestion();
}
/** /**
* Mark a draggable element as selected. * Mark a draggable element as selected.
* *

View File

@ -3,11 +3,11 @@
<core-loading [hideUntil]="question.loaded"></core-loading> <core-loading [hideUntil]="question.loaded"></core-loading>
<ion-item text-wrap [ngClass]="{invisible: !question.loaded}"> <ion-item text-wrap [ngClass]="{invisible: !question.loaded}">
<p *ngIf="!question.readOnly" class="core-info-card-icon"> <p *ngIf="!question.readOnly" class="core-info-card" icon-start>
<ion-icon name="information" item-start></ion-icon> <ion-icon name="information"></ion-icon>
{{ 'core.question.howtodraganddrop' | translate }} {{ 'core.question.howtodraganddrop' | translate }}
</p> </p>
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p> <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea"></core-format-text> <core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea" (afterRender)="questionRendered()"></core-format-text>
</ion-item> </ion-item>
</section> </section>

View File

@ -0,0 +1,101 @@
// Style ddimageortext content a bit. Almost all these styles are copied from Moodle.
addon-qtype-ddimageortext {
.qtext {
margin-bottom: 0.5em;
display: block;
}
div.droparea img {
border: 1px solid $gray-darker;
max-width: 100%;
}
.draghome {
vertical-align: top;
margin: 5px;
visibility : hidden;
}
.draghome img {
display: block;
}
div.draghome {
border: 1px solid $gray-darker;
cursor: pointer;
background-color: #B0C4DE;
display:inline-block;
height: auto;
width: auto;
zoom: 1;
}
.group1 {
background-color: $white;
}
.group2 {
background-color: $blue-light;
}
.group3 {
background-color: #DCDCDC;
}
.group4 {
background-color: #D8BFD8;
}
.group5 {
background-color: #87CEFA;
}
.group6 {
background-color: #DAA520;
}
.group7 {
background-color: #FFD700;
}
.group8 {
background-color: #F0E68C;
}
.drag {
border: 1px solid $gray-darker;
cursor: pointer;
z-index: 2;
}
.dragitems.readonly .drag {
cursor: auto;
}
.dragitems>div {
clear: both;
}
.dragitems {
cursor: pointer;
}
.dragitems.readonly {
cursor: auto;
}
.drag img {
display: block;
}
div.ddarea {
text-align : center;
position: relative;
}
.dropbackground {
margin:0 auto;
}
.dropzone {
border: 1px solid $gray-darker;
position: absolute;
z-index: 1;
cursor: pointer;
}
.readonly .dropzone {
cursor: auto;
}
div.dragitems div.draghome, div.dragitems div.drag {
font:13px/1.231 arial,helvetica,clean,sans-serif;
}
.drag.beingdragged {
z-index: 3;
box-shadow: 3px 3px 4px $gray-darker;
}
}

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, AfterViewInit, Injector, ElementRef } from '@angular/core'; import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext'; import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
@ -24,14 +24,15 @@ import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
selector: 'addon-qtype-ddimageortext', selector: 'addon-qtype-ddimageortext',
templateUrl: 'ddimageortext.html' templateUrl: 'ddimageortext.html'
}) })
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy { export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
protected element: HTMLElement; protected element: HTMLElement;
protected questionInstance: AddonQtypeDdImageOrTextQuestion; protected questionInstance: AddonQtypeDdImageOrTextQuestion;
protected drops: any[]; // The drop zones received in the init object of the question. protected drops: any[]; // The drop zones received in the init object of the question.
protected destroyed = false;
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) { constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) {
super(logger, 'AddonQtypeDdImageOrTextComponent', injector); super(loggerProvider, 'AddonQtypeDdImageOrTextComponent', injector);
this.element = element.nativeElement; this.element = element.nativeElement;
} }
@ -76,18 +77,21 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
} }
/** /**
* View has been initialized. * The question has been rendered.
*/ */
ngAfterViewInit(): void { questionRendered(): void {
// Create the instance. if (!this.destroyed) {
this.questionInstance = new AddonQtypeDdImageOrTextQuestion(this.logger, this.domUtils, this.element, // Create the instance.
this.question, this.question.readOnly, this.drops); this.questionInstance = new AddonQtypeDdImageOrTextQuestion(this.loggerProvider, this.domUtils, this.element,
this.question, this.question.readOnly, this.drops);
}
} }
/** /**
* Component being destroyed. * Component being destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed = true;
this.questionInstance && this.questionInstance.destroy(); this.questionInstance && this.questionInstance.destroy();
} }
} }

View File

@ -50,6 +50,7 @@ export class AddonQtypeDdMarkerQuestion {
protected proportion = 1; protected proportion = 1;
protected selected: HTMLElement; // Selected element (being "dragged"). protected selected: HTMLElement; // Selected element (being "dragged").
protected graphics: AddonQtypeDdMarkerGraphicsApi; protected graphics: AddonQtypeDdMarkerGraphicsApi;
protected resizeFunction;
doc: AddonQtypeDdMarkerQuestionDocStructure; doc: AddonQtypeDdMarkerQuestionDocStructure;
shapes = []; shapes = [];
@ -157,7 +158,9 @@ export class AddonQtypeDdMarkerQuestion {
* Function to call when the instance is no longer needed. * Function to call when the instance is no longer needed.
*/ */
destroy(): void { destroy(): void {
window.removeEventListener('resize', this.resizeFunction); if (this.resizeFunction) {
window.removeEventListener('resize', this.resizeFunction);
}
} }
/** /**
@ -167,7 +170,7 @@ export class AddonQtypeDdMarkerQuestion {
* @return {AddonQtypeDdMarkerQuestionDocStructure} The object. * @return {AddonQtypeDdMarkerQuestionDocStructure} The object.
*/ */
docStructure(slot: number): AddonQtypeDdMarkerQuestionDocStructure { docStructure(slot: number): AddonQtypeDdMarkerQuestionDocStructure {
const topNode = <HTMLElement> this.container.querySelector('#core-question-' + slot + ' .addon-qtype-ddmarker-container'), const topNode = <HTMLElement> this.container.querySelector('.addon-qtype-ddmarker-container'),
dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems'); dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems');
return { return {
@ -293,9 +296,9 @@ export class AddonQtypeDdMarkerQuestion {
const markerTexts = this.doc.markerTexts(); const markerTexts = this.doc.markerTexts();
// Check if there is already a marker text for this drop zone. // Check if there is already a marker text for this drop zone.
if (link) { if (link) {
existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo + ' a'); existingMarkerText = <HTMLElement> markerTexts.querySelector('span.markertext' + dropZoneNo + ' a');
} else { } else {
existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo); existingMarkerText = <HTMLElement> markerTexts.querySelector('span.markertext' + dropZoneNo);
} }
if (existingMarkerText) { if (existingMarkerText) {
@ -538,7 +541,7 @@ export class AddonQtypeDdMarkerQuestion {
dragging = (this.doc.dragItemBeingDragged(choiceNo) !== null), dragging = (this.doc.dragItemBeingDragged(choiceNo) !== null),
coords: number[][] = []; coords: number[][] = [];
if (fv !== '' && typeof fv != 'undefined') { if (fv !== '' && typeof fv != 'undefined' && fv !== null) {
// Get all the coordinates in the input and add them to the coords list. // Get all the coordinates in the input and add them to the coords list.
const coordsStrings = fv.split(';'); const coordsStrings = fv.split(';');
@ -645,6 +648,7 @@ export class AddonQtypeDdMarkerQuestion {
this.pollForImageLoad(); this.pollForImageLoad();
}); });
this.resizeFunction = this.redrawDragsAndDrops.bind(this);
window.addEventListener('resize', this.resizeFunction); window.addEventListener('resize', this.resizeFunction);
} }
@ -731,6 +735,9 @@ export class AddonQtypeDdMarkerQuestion {
}, 500); }, 500);
} }
/**
* Redraw all draggables and drop zones.
*/
redrawDragsAndDrops(): void { redrawDragsAndDrops(): void {
// Mark all the draggable items as not placed. // Mark all the draggable items as not placed.
const drags = this.doc.dragItems(); const drags = this.doc.dragItems();
@ -789,7 +796,7 @@ export class AddonQtypeDdMarkerQuestion {
dropZone = this.dropZones[dropZoneNo], dropZone = this.dropZones[dropZoneNo],
dzNo = Number(dropZoneNo); dzNo = Number(dropZoneNo);
this.drawDropZone(dzNo, dropZone.markerText, dropZone.shape, dropZone.coords, colourForDropZone, true); this.drawDropZone(dzNo, dropZone.markertext, dropZone.shape, dropZone.coords, colourForDropZone, true);
} }
} }
} }
@ -803,13 +810,6 @@ export class AddonQtypeDdMarkerQuestion {
this.setFormValue(choiceNo, ''); this.setFormValue(choiceNo, '');
} }
/**
* Function to call when the window is resized.
*/
resizeFunction(): void {
this.redrawDragsAndDrops();
}
/** /**
* Restart the colour index. * Restart the colour index.
*/ */

View File

@ -3,11 +3,11 @@
<core-loading [hideUntil]="question.loaded"></core-loading> <core-loading [hideUntil]="question.loaded"></core-loading>
<ion-item text-wrap [ngClass]="{invisible: !question.loaded}"> <ion-item text-wrap [ngClass]="{invisible: !question.loaded}">
<p *ngIf="!question.readOnly" class="core-info-card-icon"> <p *ngIf="!question.readOnly" class="core-info-card" icon-start>
<ion-icon name="information" item-start></ion-icon> <ion-icon name="information"></ion-icon>
{{ 'core.question.howtodraganddrop' | translate }} {{ 'core.question.howtodraganddrop' | translate }}
</p> </p>
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p> <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea"></core-format-text> <core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea" (afterRender)="questionRendered()"></core-format-text>
</ion-item> </ion-item>
</section> </section>

View File

@ -0,0 +1,97 @@
// Style ddmarker content a bit. Almost all these styles are copied from Moodle.
addon-qtype-ddmarker {
.qtext {
margin-bottom: 0.5em;
display: block;
}
div.droparea img {
border: 1px solid $gray-darker;
max-width: 100%;
}
.draghome img, .draghome span {
visibility: hidden;
}
.dragitems .dragitem {
cursor: pointer;
position: absolute;
z-index: 2;
}
.dropzones {
position: absolute;
}
.dropzones svg {
z-index: 3;
}
.dragitem.beingdragged .markertext {
z-index: 5;
box-shadow: 3px 3px 4px $gray-darker;
}
.dragitems .draghome {
margin: 10px;
display: inline-block;
}
.dragitems.readonly .dragitem {
cursor: auto;
}
div.ddarea {
text-align: center;
}
div.ddarea .markertexts {
min-height: 80px;
position: absolute;
text-align: left;
}
.dropbackground {
margin: 0 auto;
}
div.dragitems div.draghome, div.dragitems div.dragitem,
div.draghome, div.drag {
font: 13px/1.231 arial,helvetica,clean,sans-serif;
}
div.dragitems span.markertext,
div.markertexts span.markertext {
margin: 0 5px;
z-index: 2;
background-color: $white;
border: 2px solid $gray-darker;
padding: 5px;
display: inline-block;
zoom: 1;
border-radius: 10px;
}
div.markertexts span.markertext {
z-index: 3;
background-color: $yellow-light;
border-style: solid;
border-width: 2px;
border-color: $yellow;
position: absolute;
}
span.wrongpart {
background-color: $yellow-light;
border-style: solid;
border-width: 2px;
border-color: $yellow;
padding: 5px;
border-radius: 10px;
filter: alpha(opacity=60);
opacity: 0.6;
margin: 5px;
display: inline-block;
}
div.dragitems img.target {
position: absolute;
left: -7px; /* This must be half the size of the target image, minus 0.5. */
top: -7px; /* In other words, this works for a 15x15 cross-hair. */
}
div.dragitems div.draghome img.target {
display: none;
}
}

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, AfterViewInit, Injector, ElementRef } from '@angular/core'; import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker'; import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
@ -24,14 +24,15 @@ import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
selector: 'addon-qtype-ddmarker', selector: 'addon-qtype-ddmarker',
templateUrl: 'ddmarker.html' templateUrl: 'ddmarker.html'
}) })
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy { export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
protected element: HTMLElement; protected element: HTMLElement;
protected questionInstance: AddonQtypeDdMarkerQuestion; protected questionInstance: AddonQtypeDdMarkerQuestion;
protected dropZones: any[]; // The drop zones received in the init object of the question. protected dropZones: any[]; // The drop zones received in the init object of the question.
protected destroyed = false;
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) { constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) {
super(logger, 'AddonQtypeDdMarkerComponent', injector); super(loggerProvider, 'AddonQtypeDdMarkerComponent', injector);
this.element = element.nativeElement; this.element = element.nativeElement;
} }
@ -83,18 +84,21 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
} }
/** /**
* View has been initialized. * The question has been rendered.
*/ */
ngAfterViewInit(): void { questionRendered(): void {
// Create the instance. if (!this.destroyed) {
this.questionInstance = new AddonQtypeDdMarkerQuestion(this.logger, this.domUtils, this.textUtils, this.element, // Create the instance.
this.question, this.question.readOnly, this.dropZones); this.questionInstance = new AddonQtypeDdMarkerQuestion(this.loggerProvider, this.domUtils, this.textUtils, this.element,
this.question, this.question.readOnly, this.dropZones);
}
} }
/** /**
* Component being destroyed. * Component being destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed = true;
this.questionInstance && this.questionInstance.destroy(); this.questionInstance && this.questionInstance.destroy();
} }
} }

View File

@ -46,6 +46,7 @@ export class AddonQtypeDdwtosQuestion {
protected selectors: AddonQtypeDdwtosQuestionCSSSelectors; // Result of cssSelectors. protected selectors: AddonQtypeDdwtosQuestionCSSSelectors; // Result of cssSelectors.
protected placed: {[no: number]: number}; // Map that relates drag elements numbers with drop zones numbers. protected placed: {[no: number]: number}; // Map that relates drag elements numbers with drop zones numbers.
protected selected: HTMLElement; // Selected element (being "dragged"). protected selected: HTMLElement; // Selected element (being "dragged").
protected resizeFunction;
/** /**
* Create the instance. * Create the instance.
@ -125,7 +126,7 @@ export class AddonQtypeDdwtosQuestion {
* @return {AddonQtypeDdwtosQuestionCSSSelectors} Object with the functions to get the selectors. * @return {AddonQtypeDdwtosQuestionCSSSelectors} Object with the functions to get the selectors.
*/ */
cssSelectors(slot: number): AddonQtypeDdwtosQuestionCSSSelectors { cssSelectors(slot: number): AddonQtypeDdwtosQuestionCSSSelectors {
const topNode = '#core-question-' + slot + ' .addon-qtype-ddwtos-container', const topNode = '.addon-qtype-ddwtos-container',
selectors: AddonQtypeDdwtosQuestionCSSSelectors = {}; selectors: AddonQtypeDdwtosQuestionCSSSelectors = {};
selectors.topNode = (): string => { selectors.topNode = (): string => {
@ -193,7 +194,9 @@ export class AddonQtypeDdwtosQuestion {
* Function to call when the instance is no longer needed. * Function to call when the instance is no longer needed.
*/ */
destroy(): void { destroy(): void {
window.removeEventListener('resize', this.resizeFunction); if (this.resizeFunction) {
window.removeEventListener('resize', this.resizeFunction);
}
} }
/** /**
@ -285,6 +288,7 @@ export class AddonQtypeDdwtosQuestion {
this.positionDragItems(); this.positionDragItems();
}); });
this.resizeFunction = this.positionDragItems.bind(this);
window.addEventListener('resize', this.resizeFunction); window.addEventListener('resize', this.resizeFunction);
} }
@ -488,13 +492,6 @@ export class AddonQtypeDdwtosQuestion {
this.placeDragInDrop(null, drop); this.placeDragInDrop(null, drop);
} }
/**
* Function to call when the window is resized.
*/
resizeFunction(): void {
this.positionDragItems();
}
/** /**
* Select a certain element as being "dragged". * Select a certain element as being "dragged".
* *

View File

@ -1,11 +1,11 @@
<section ion-list *ngIf="question.text || question.text === ''" class="addon-qtype-ddwtos-container"> <section ion-list *ngIf="question.text || question.text === ''">
<ion-item text-wrap> <ion-item text-wrap class="addon-qtype-ddwtos-container">
<p *ngIf="!question.readOnly" class="core-info-card-icon"> <p *ngIf="!question.readOnly" class="core-info-card" icon-start>
<ion-icon name="information" item-start></ion-icon> <ion-icon name="information"></ion-icon>
{{ 'core.question.howtodraganddrop' | translate }} {{ 'core.question.howtodraganddrop' | translate }}
</p> </p>
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p> <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
<core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers"></core-format-text> <core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers" (afterRender)="questionRendered()"></core-format-text>
<div class="drags"></div> <div class="drags"></div>
</ion-item> </ion-item>
</section> </section>

View File

@ -0,0 +1,108 @@
// Style ddwtos content a bit. Almost all these styles are copied from Moodle.
addon-qtype-ddwtos {
.qtext {
margin-bottom: 0.5em;
display: block;
}
.draghome {
margin-bottom: 1em;
}
.answertext {
margin-bottom: 0.5em;
}
.drop {
display: inline-block;
text-align: center;
border: 1px solid $gray-darker;
margin-bottom: 2px;
border-radius: 5px;
}
.draghome, .drag {
display: inline-block;
text-align: center;
background: transparent;
border: 0;
}
.draghome, .drag.unplaced{
border: 1px solid $gray-darker;
border-radius: 5px;
}
.draghome {
visibility: hidden;
}
.drag {
z-index: 2;
border-radius: 5px;
}
.drag.selected {
z-index: 3;
box-shadow: 3px 3px 4px $gray-darker;
}
.drop.selected {
border-color: $yellow-light;
box-shadow: 0 0 5px 5px $yellow-light;
}
&.notreadonly .drag,
&.notreadonly .draghome,
&.notreadonly .drop,
&.notreadonly .answercontainer {
cursor: pointer;
border-radius: 5px;
}
&.readonly .drag,
&.readonly .draghome,
&.readonly .drop,
&.readonly .answercontainer {
cursor: default;
}
span.incorrect {
background-color: $red-light;
}
span.correct {
background-color: $green-light;
}
.group1 {
background-color: $white;
}
.group2 {
background-color: #DCDCDC;
}
.group3 {
background-color: $blue-light;
}
.group4 {
background-color: #D8BFD8;
}
.group5 {
background-color: #87CEFA;
}
.group6 {
background-color: #DAA520;
}
.group7 {
background-color: #FFD700;
}
.group8 {
background-color: #F0E68C;
}
sub, sup {
font-size: 80%;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.4em;
}
sub {
bottom: -0.2em;
}
}

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, AfterViewInit, Injector, ElementRef } from '@angular/core'; import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos'; import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
@ -24,14 +24,15 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
selector: 'addon-qtype-ddwtos', selector: 'addon-qtype-ddwtos',
templateUrl: 'ddwtos.html' templateUrl: 'ddwtos.html'
}) })
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy { export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
protected element: HTMLElement; protected element: HTMLElement;
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;
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) { constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) {
super(logger, 'AddonQtypeDdwtosComponent', injector); super(loggerProvider, 'AddonQtypeDdwtosComponent', injector);
this.element = element.nativeElement; this.element = element.nativeElement;
} }
@ -74,28 +75,30 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
} }
// 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(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])')), const inputEls = <HTMLElement[]> Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])'));
inputIds = [];
inputEls.forEach((inputEl) => { inputEls.forEach((inputEl) => {
this.question.text += inputEl.outerHTML; this.question.text += inputEl.outerHTML;
inputIds.push(inputEl.getAttribute('id')); this.inputIds.push(inputEl.getAttribute('id'));
}); });
} }
/** /**
* View has been initialized. * The question has been rendered.
*/ */
ngAfterViewInit(): void { questionRendered(): void {
// Create the instance. if (!this.destroyed) {
this.questionInstance = new AddonQtypeDdwtosQuestion(this.logger, this.domUtils, this.element, this.question, // Create the instance.
this.question.readOnly, this.inputIds); this.questionInstance = new AddonQtypeDdwtosQuestion(this.loggerProvider, this.domUtils, this.element, this.question,
this.question.readOnly, this.inputIds);
}
} }
/** /**
* Component being destroyed. * Component being destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed = true;
this.questionInstance && this.questionInstance.destroy(); this.questionInstance && this.questionInstance.destroy();
} }
} }

View File

@ -7,13 +7,13 @@
<!-- Textarea. --> <!-- Textarea. -->
<ion-item *ngIf="question.textarea && !question.hasDraftFiles"> <ion-item *ngIf="question.textarea && !question.hasDraftFiles">
<!-- "Format" hidden input --> <!-- "Format" hidden input -->
<input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value" > <input item-content *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value" >
<!-- Plain text textarea. --> <!-- Plain text textarea. -->
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true">{{question.textarea.text}}</ion-textarea> <ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true" [ngModel]="question.textarea.text"></ion-textarea>
<!-- Rich text editor. --> <!-- Rich text editor. -->
<core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"></core-rich-text-editor> <core-rich-text-editor item-content *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}" [control]="formControl" [name]="question.textarea.name"></core-rich-text-editor>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet: <!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
model="textarea" [name]="textarea.name" [component]="component" [componentId]="componentId" --> [component]="component" [componentId]="componentId" -->
</ion-item> </ion-item>
<!-- Draft files not supported. --> <!-- Draft files not supported. -->

View File

@ -15,6 +15,7 @@
import { Component, OnInit, Injector } from '@angular/core'; import { Component, OnInit, Injector } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { FormControl, FormBuilder } from '@angular/forms';
/** /**
* Component to render an essay question. * Component to render an essay question.
@ -25,7 +26,9 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-
}) })
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit { export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit {
constructor(logger: CoreLoggerProvider, injector: Injector) { protected formControl: FormControl;
constructor(logger: CoreLoggerProvider, injector: Injector, protected fb: FormBuilder) {
super(logger, 'AddonQtypeEssayComponent', injector); super(logger, 'AddonQtypeEssayComponent', injector);
} }
@ -34,5 +37,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
*/ */
ngOnInit(): void { ngOnInit(): void {
this.initEssayComponent(); this.initEssayComponent();
this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text);
} }
} }

View File

@ -0,0 +1,19 @@
// Style gapselect content a bit. All these styles are copied from Moodle.
addon-qtype-gapselect {
p {
margin: 0 0 .5em;
}
select {
height: 30px;
line-height: 30px;
display: inline-block;
border: 1px solid $gray-dark;
padding: 4px 6px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
margin-bottom: 10px;
background: $gray-lighter;
}
}

View File

@ -3,17 +3,21 @@
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p> <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
</ion-item> </ion-item>
<ion-item text-wrap *ngFor="let row of question.rows"> <ion-item text-wrap *ngFor="let row of question.rows">
<ion-row> <ion-grid item-content>
<ion-col> <ion-row>
<p><core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId" [text]="row.text"></core-format-text></p> <ion-col>
</ion-col> <p><core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId" [text]="row.text"></core-format-text></p>
<ion-col [ngClass]='{"core-question-answer-correct": row.isCorrect === 1, "core-question-answer-incorrect": row.isCorrect === 0}'> </ion-col>
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">{{ row.accessibilityLabel }}</label> <ion-col [ngClass]='{"core-question-answer-correct": row.isCorrect === 1, "core-question-answer-incorrect": row.isCorrect === 0}'>
<ion-select id="{{row.id}}" [name]="row.name" [attr.aria-labelledby]="'addon-qtype-match-question-' + row.id" [ngModel]="row.selected"> <label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">{{ row.accessibilityLabel }}</label>
<ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option> <ion-select id="{{row.id}}" [name]="row.name" [attr.aria-labelledby]="'addon-qtype-match-question-' + row.id" [(ngModel)]="row.selected" interface="popover">
</ion-select> <ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option>
<!-- @todo: select fix? --> </ion-select>
</ion-col>
</ion-row> <!-- ion-select doesn't use a select. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="row.selected" [attr.name]="row.name">
</ion-col>
</ion-row>
</ion-grid>
</ion-item> </ion-item>
</section> </section>

View File

@ -0,0 +1,43 @@
// Style multianswer content a bit. All these styles are copied from Moodle.
addon-qtype-multianswer {
p {
margin: 0 0 .5em;
}
.answer div.r0, .answer div.r1, .answer td.r0, .answer td.r1 {
padding: 0.3em;
}
table {
width: 100%;
display: table;
}
tr {
display: table-row;
}
td {
display: table-cell;
}
input, select {
display: inline-block;
border: 1px solid #ccc;
padding: 4px 6px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
margin-bottom: 10px;
}
select {
height: 30px;
line-height: 30px;
}
input[type="radio"], input[type="checkbox"] {
margin-top: -4px;
margin-right: 7px;
}
}

View File

@ -7,19 +7,28 @@
<!-- Checkbox for multiple choice. --> <!-- Checkbox for multiple choice. -->
<ng-container *ngIf="question.multi"> <ng-container *ngIf="question.multi">
<ion-checkbox text-wrap [name]="option.name" [ngModel]="option.checked" [disabled]="option.disabled" *ngFor="let option of question.options" [ngClass]="{'core-question-answer-correct': option.isCorrect === 1, 'core-question-answer-incorrect': option.isCorrect === 0}"> <ion-item text-wrap *ngFor="let option of question.options" [ngClass]="{'core-question-answer-correct': option.isCorrect === 1, 'core-question-answer-incorrect': option.isCorrect === 0}">
<p><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></p> <ion-label>
<p *ngIf="option.feedback" class="core-question-feedback-container"><core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"></core-format-text></p> <core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text>
</ion-checkbox> <p *ngIf="option.feedback" class="core-question-feedback-container"><core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"></core-format-text></p>
</ion-label>
<ion-checkbox [name]="option.name" [(ngModel)]="option.checked" [disabled]="option.disabled" item-end></ion-checkbox>
<!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. -->
<input item-content type="hidden" [ngModel]="option.checked" [attr.name]="option.name">
</ion-item>
</ng-container> </ng-container>
<!-- Radio buttons for single choice. --> <!-- Radio buttons for single choice. -->
<div *ngIf="!question.multi" radio-group [ngModel]="question.singleChoiceModel" [name]="question.optionsName"> <div *ngIf="!question.multi" radio-group [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
<ion-item text-wrap *ngFor="let option of question.options"> <ion-item text-wrap *ngFor="let option of question.options">
<ion-label><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></ion-label> <ion-label><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></ion-label>
<ion-radio [value]="option.value" [disabled]="option.disabled" [ngClass]='{"core-question-answer-correct": option.isCorrect === 1, "core-question-answer-incorrect": option.isCorrect === 0}'> <ion-radio [value]="option.value" [disabled]="option.disabled" [ngClass]='{"core-question-answer-correct": option.isCorrect === 1, "core-question-answer-incorrect": option.isCorrect === 0}'>
<p *ngIf="option.feedback" class="core-question-feedback-container"><core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"></core-format-text></p> <p *ngIf="option.feedback" class="core-question-feedback-container"><core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"></core-format-text></p>
</ion-radio> </ion-radio>
</ion-item> </ion-item>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="question.singleChoiceModel" [attr.name]="question.optionsName">
</div> </div>
</section> </section>

View File

@ -37,6 +37,7 @@
.opacity-hide { opacity: 0; } .opacity-hide { opacity: 0; }
.core-big { font-size: 115%; } .core-big { font-size: 115%; }
.invisible { visibility: hidden; }
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
.core-center-view .scroll-content { .core-center-view .scroll-content {
@ -510,3 +511,13 @@ textarea {
color: $color-base; color: $color-base;
} }
} }
.accesshide {
position: absolute;
left: -10000px;
font-weight: normal;
font-size: 1em; }
.core-monospaced {
font-family: Andale Mono,Monaco,Courier New,DejaVu Sans Mono,monospace;
}

View File

@ -20,7 +20,7 @@
</div> </div>
<div [hidden]="rteEnabled"> <div [hidden]="rteEnabled">
<ion-textarea #textarea class="core-textarea" [placeholder]="placeholder" ngControl="control" (ionChange)="onChange($event)"></ion-textarea> <ion-textarea #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)"></ion-textarea>
<div class="formatOptions"> <div class="formatOptions">
<button tappable (click)="toggleEditor($event)">Toggle Editor</button> <button tappable (click)="toggleEditor($event)">Toggle Editor</button>
</div> </div>

View File

@ -42,6 +42,7 @@ export class CoreRichTextEditorComponent {
@Input() placeholder = ''; // Placeholder to set in textarea. @Input() placeholder = ''; // Placeholder to set in textarea.
@Input() control: FormControl; // Form control. @Input() control: FormControl; // Form control.
@Input() name = 'core-rich-text-editor'; // Name to set to the textarea.
@Output() contentChanged: EventEmitter<string>; @Output() contentChanged: EventEmitter<string>;
@ViewChild('editor') editor: ElementRef; // WYSIWYG editor. @ViewChild('editor') editor: ElementRef; // WYSIWYG editor.
@ -109,6 +110,7 @@ export class CoreRichTextEditorComponent {
this.clearText(); this.clearText();
} else { } else {
this.control.setValue(this.editorElement.innerHTML); this.control.setValue(this.editorElement.innerHTML);
this.textarea.value = this.editorElement.innerHTML;
} }
} else { } else {
if (this.isNullOrWhiteSpace(this.textarea.value)) { if (this.isNullOrWhiteSpace(this.textarea.value)) {
@ -117,6 +119,7 @@ export class CoreRichTextEditorComponent {
this.control.setValue(this.textarea.value); this.control.setValue(this.textarea.value);
} }
} }
this.contentChanged.emit(this.control.value); this.contentChanged.emit(this.control.value);
} }

View File

@ -83,7 +83,6 @@ export class CoreQuestionBaseComponent {
if (optionEl.selected) { if (optionEl.selected) {
selectModel.selected = option.value; selectModel.selected = option.value;
selectModel.selectedLabel = option.label;
} }
selectModel.options.push(option); selectModel.options.push(option);
@ -92,7 +91,6 @@ export class CoreQuestionBaseComponent {
if (!selectModel.selected) { if (!selectModel.selected) {
// No selected option, select the first one. // No selected option, select the first one.
selectModel.selected = selectModel.options[0].value; selectModel.selected = selectModel.options[0].value;
selectModel.selectedLabel = selectModel.options[0].label;
} }
// Get the accessibility label. // Get the accessibility label.
@ -158,7 +156,7 @@ export class CoreQuestionBaseComponent {
// Check which one should be displayed first: the options or the input. // Check which one should be displayed first: the options or the input.
const input = questionDiv.querySelector('input[type="text"][name*=answer]'); const input = questionDiv.querySelector('input[type="text"][name*=answer]');
this.question.optionsFirst = this.question.optionsFirst =
questionDiv.innerHTML.indexOf(input.outerHTML) > questionDiv.innerHTML.indexOf(options[0].outerHTML); questionDiv.innerHTML.indexOf(input.outerHTML) > questionDiv.innerHTML.indexOf(radios[0].outerHTML);
} }
} }
@ -313,7 +311,7 @@ export class CoreQuestionBaseComponent {
if (questionDiv) { if (questionDiv) {
// Find rows. // Find rows.
const rows = Array.from(questionDiv.querySelectorAll('tr')); const rows = Array.from(questionDiv.querySelectorAll('table.answer tr'));
if (!rows || !rows.length) { if (!rows || !rows.length) {
this.logger.warn('Aborting because couldn\'t find any row.', this.question.name); this.logger.warn('Aborting because couldn\'t find any row.', this.question.name);
@ -376,7 +374,7 @@ export class CoreQuestionBaseComponent {
}; };
if (option.selected) { if (option.selected) {
rowModel.selected = option; rowModel.selected = option.value;
} }
rowModel.options.push(option); rowModel.options.push(option);
@ -404,8 +402,6 @@ export class CoreQuestionBaseComponent {
const questionDiv = this.initComponent(); const questionDiv = this.initComponent();
if (questionDiv) { if (questionDiv) {
// Create the model for radio buttons.
this.question.singleChoiceModel = {};
// Get the prompt. // Get the prompt.
this.question.prompt = this.domUtils.getContentsOfElement(questionDiv, '.prompt'); this.question.prompt = this.domUtils.getContentsOfElement(questionDiv, '.prompt');
@ -481,6 +477,11 @@ export class CoreQuestionBaseComponent {
return this.questionHelper.showComponentError(this.onAbort); return this.questionHelper.showComponentError(this.onAbort);
} }
if (!this.question.multi && typeof this.question.singleChoiceModel == 'undefined') {
// We couldn't find the option to select, select the first one.
this.question.singleChoiceModel = options[0].value;
}
} }
return questionDiv; return questionDiv;

View File

@ -123,7 +123,7 @@ export class CoreQuestionComponent implements OnInit {
promise.then(() => { promise.then(() => {
// Handle behaviour. // Handle behaviour.
this.behaviourDelegate.handleQuestion(this.question, this.question.preferredBehaviour).then((comps) => { this.behaviourDelegate.handleQuestion(this.question.preferredBehaviour, this.question).then((comps) => {
this.behaviourComponents = comps; this.behaviourComponents = comps;
}); });
this.questionHelper.extractQbehaviourRedoButton(this.question); this.questionHelper.extractQbehaviourRedoButton(this.question);

View File

@ -319,7 +319,7 @@ export class CoreQuestionHelperProvider {
elements = Array.from(form.elements); elements = Array.from(form.elements);
elements.forEach((element: HTMLInputElement) => { elements.forEach((element: HTMLInputElement) => {
const name = element.name || ''; const name = element.name || element.getAttribute('ng-reflect-name') || '';
// Ignore flag and submit inputs. // Ignore flag and submit inputs.
if (!name || name.match(/_:flagged$/) || element.type == 'submit' || element.tagName == 'BUTTON') { if (!name || name.match(/_:flagged$/) || element.type == 'submit' || element.tagName == 'BUTTON') {
@ -588,13 +588,11 @@ export class CoreQuestionHelperProvider {
* @param {string} [error] Error to show. * @param {string} [error] Error to show.
*/ */
showComponentError(onAbort: EventEmitter<void>, error?: string): void { showComponentError(onAbort: EventEmitter<void>, error?: string): void {
error = error || 'Error processing the question. This could be caused by custom modifications in your site.';
// Prevent consecutive errors. // Prevent consecutive errors.
const now = Date.now(); const now = Date.now();
if (now - this.lastErrorShown > 500) { if (now - this.lastErrorShown > 500) {
this.lastErrorShown = now; this.lastErrorShown = now;
this.domUtils.showErrorModal(error); this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorparsequestions', true);
} }
onAbort && onAbort.emit(); onAbort && onAbort.emit();