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()">
<ion-item>
<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="course">{{ 'addon.notes.coursenotes' | 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">
<p>{{ 'core.question.certainty' | translate }}</p>
</ion-item>
<ion-radio *ngFor="let option of question.behaviourCertaintyOptions" id="{{option.id}}" name="{{option.name}}" [ngModel]="question.behaviourCertaintySelected" [value]="option.value" [disabled]="option.disabled">
<p><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></p>
</ion-radio>
<div *ngIf="question.behaviourCertaintyOptions && question.behaviourCertaintyOptions.length">
<ion-item text-wrap class="addon-qbehaviour-deferredcbm-certainty-title" >
<p>{{ 'core.question.certainty' | translate }}</p>
</ion-item>
<div radio-group [(ngModel)]="question.behaviourCertaintySelected" [name]="question.behaviourCertaintyOptions[0].name">
<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>
<ion-item text-wrap>
<ion-row>
<!-- Display unit select before the answer input. -->
<ng-container *ngIf="question.select && question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
<ion-item text-wrap ion-grid>
<ion-grid item-content>
<ion-row>
<!-- Display unit select before the answer input. -->
<ng-container *ngIf="question.select && question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
<!-- Input to enter the answer. -->
<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>
</ion-col>
<!-- Input to enter the answer. -->
<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>
</ion-col>
<!-- Display unit select after the answer input. -->
<ng-container *ngIf="question.select && !question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
</ion-row>
<!-- Display unit select after the answer input. -->
<ng-container *ngIf="question.select && !question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
</ion-row>
</ion-grid>
</ion-item>
<!-- Display unit options after the answer input. -->
@ -38,18 +40,26 @@
<ng-template #selectUnits>
<ion-col>
<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-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>
</ng-template>
<!-- Template for units entered using radio buttons. -->
<ng-template #radioUnits>
<div radio-group [ngModel]="question.unit" [name]="question.optionsName">
<ion-radio *ngFor="let option of question.options" [value]="option.value" [disabled]="option.disabled">
<p>{{option.text}}</p>
</ion-radio>
<div radio-group [(ngModel)]="question.unit" [name]="question.optionsName">
<ion-item text-wrap *ngFor="let option of question.options">
<ion-label>
<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>
</ng-template>

View File

@ -45,6 +45,7 @@ export class AddonQtypeDdImageOrTextQuestion {
protected topNode: HTMLElement;
protected proportion = 1;
protected selected: HTMLElement; // Selected element (being "dragged").
protected resizeFunction;
/**
* Create the this.
@ -182,7 +183,10 @@ export class AddonQtypeDdImageOrTextQuestion {
*/
destroy(): void {
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.
*/
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'),
doc: AddonQtypeDdImageOrTextQuestionDocStructure = {};
@ -456,6 +460,7 @@ export class AddonQtypeDdImageOrTextQuestion {
this.pollForImageLoad();
});
this.resizeFunction = this.repositionDragsForQuestion.bind(this);
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.
*

View File

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

View File

@ -50,6 +50,7 @@ export class AddonQtypeDdMarkerQuestion {
protected proportion = 1;
protected selected: HTMLElement; // Selected element (being "dragged").
protected graphics: AddonQtypeDdMarkerGraphicsApi;
protected resizeFunction;
doc: AddonQtypeDdMarkerQuestionDocStructure;
shapes = [];
@ -157,7 +158,9 @@ export class AddonQtypeDdMarkerQuestion {
* Function to call when the instance is no longer needed.
*/
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.
*/
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');
return {
@ -293,9 +296,9 @@ export class AddonQtypeDdMarkerQuestion {
const markerTexts = this.doc.markerTexts();
// Check if there is already a marker text for this drop zone.
if (link) {
existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo + ' a');
existingMarkerText = <HTMLElement> markerTexts.querySelector('span.markertext' + dropZoneNo + ' a');
} else {
existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo);
existingMarkerText = <HTMLElement> markerTexts.querySelector('span.markertext' + dropZoneNo);
}
if (existingMarkerText) {
@ -538,7 +541,7 @@ export class AddonQtypeDdMarkerQuestion {
dragging = (this.doc.dragItemBeingDragged(choiceNo) !== null),
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.
const coordsStrings = fv.split(';');
@ -645,6 +648,7 @@ export class AddonQtypeDdMarkerQuestion {
this.pollForImageLoad();
});
this.resizeFunction = this.redrawDragsAndDrops.bind(this);
window.addEventListener('resize', this.resizeFunction);
}
@ -731,6 +735,9 @@ export class AddonQtypeDdMarkerQuestion {
}, 500);
}
/**
* Redraw all draggables and drop zones.
*/
redrawDragsAndDrops(): void {
// Mark all the draggable items as not placed.
const drags = this.doc.dragItems();
@ -789,7 +796,7 @@ export class AddonQtypeDdMarkerQuestion {
dropZone = this.dropZones[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, '');
}
/**
* Function to call when the window is resized.
*/
resizeFunction(): void {
this.redrawDragsAndDrops();
}
/**
* Restart the colour index.
*/

View File

@ -3,11 +3,11 @@
<core-loading [hideUntil]="question.loaded"></core-loading>
<ion-item text-wrap [ngClass]="{invisible: !question.loaded}">
<p *ngIf="!question.readOnly" class="core-info-card-icon">
<ion-icon name="information" item-start></ion-icon>
<p *ngIf="!question.readOnly" class="core-info-card" icon-start>
<ion-icon name="information"></ion-icon>
{{ 'core.question.howtodraganddrop' | translate }}
</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>
</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
// 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 { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
@ -24,14 +24,15 @@ import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
selector: 'addon-qtype-ddmarker',
templateUrl: 'ddmarker.html'
})
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy {
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
protected element: HTMLElement;
protected questionInstance: AddonQtypeDdMarkerQuestion;
protected dropZones: any[]; // The drop zones received in the init object of the question.
protected destroyed = false;
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
super(logger, 'AddonQtypeDdMarkerComponent', injector);
constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) {
super(loggerProvider, 'AddonQtypeDdMarkerComponent', injector);
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 {
// Create the instance.
this.questionInstance = new AddonQtypeDdMarkerQuestion(this.logger, this.domUtils, this.textUtils, this.element,
this.question, this.question.readOnly, this.dropZones);
questionRendered(): void {
if (!this.destroyed) {
// Create the instance.
this.questionInstance = new AddonQtypeDdMarkerQuestion(this.loggerProvider, this.domUtils, this.textUtils, this.element,
this.question, this.question.readOnly, this.dropZones);
}
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.destroyed = true;
this.questionInstance && this.questionInstance.destroy();
}
}

View File

@ -46,6 +46,7 @@ export class AddonQtypeDdwtosQuestion {
protected selectors: AddonQtypeDdwtosQuestionCSSSelectors; // Result of cssSelectors.
protected placed: {[no: number]: number}; // Map that relates drag elements numbers with drop zones numbers.
protected selected: HTMLElement; // Selected element (being "dragged").
protected resizeFunction;
/**
* Create the instance.
@ -125,7 +126,7 @@ export class AddonQtypeDdwtosQuestion {
* @return {AddonQtypeDdwtosQuestionCSSSelectors} Object with the functions to get the selectors.
*/
cssSelectors(slot: number): AddonQtypeDdwtosQuestionCSSSelectors {
const topNode = '#core-question-' + slot + ' .addon-qtype-ddwtos-container',
const topNode = '.addon-qtype-ddwtos-container',
selectors: AddonQtypeDdwtosQuestionCSSSelectors = {};
selectors.topNode = (): string => {
@ -193,7 +194,9 @@ export class AddonQtypeDdwtosQuestion {
* Function to call when the instance is no longer needed.
*/
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.resizeFunction = this.positionDragItems.bind(this);
window.addEventListener('resize', this.resizeFunction);
}
@ -488,13 +492,6 @@ export class AddonQtypeDdwtosQuestion {
this.placeDragInDrop(null, drop);
}
/**
* Function to call when the window is resized.
*/
resizeFunction(): void {
this.positionDragItems();
}
/**
* 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">
<ion-item text-wrap>
<p *ngIf="!question.readOnly" class="core-info-card-icon">
<ion-icon name="information" item-start></ion-icon>
<section ion-list *ngIf="question.text || question.text === ''">
<ion-item text-wrap class="addon-qtype-ddwtos-container">
<p *ngIf="!question.readOnly" class="core-info-card" icon-start>
<ion-icon name="information"></ion-icon>
{{ 'core.question.howtodraganddrop' | translate }}
</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>
</ion-item>
</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
// 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 { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
@ -24,14 +24,15 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
selector: 'addon-qtype-ddwtos',
templateUrl: 'ddwtos.html'
})
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy {
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
protected element: HTMLElement;
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) {
super(logger, 'AddonQtypeDdwtosComponent', injector);
constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) {
super(loggerProvider, 'AddonQtypeDdwtosComponent', injector);
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.
const inputEls = <HTMLElement[]> Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])')),
inputIds = [];
const inputEls = <HTMLElement[]> Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])'));
inputEls.forEach((inputEl) => {
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 {
// Create the instance.
this.questionInstance = new AddonQtypeDdwtosQuestion(this.logger, this.domUtils, this.element, this.question,
this.question.readOnly, this.inputIds);
questionRendered(): void {
if (!this.destroyed) {
// Create the instance.
this.questionInstance = new AddonQtypeDdwtosQuestion(this.loggerProvider, this.domUtils, this.element, this.question,
this.question.readOnly, this.inputIds);
}
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.destroyed = true;
this.questionInstance && this.questionInstance.destroy();
}
}

View File

@ -7,13 +7,13 @@
<!-- Textarea. -->
<ion-item *ngIf="question.textarea && !question.hasDraftFiles">
<!-- "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. -->
<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. -->
<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:
model="textarea" [name]="textarea.name" [component]="component" [componentId]="componentId" -->
[component]="component" [componentId]="componentId" -->
</ion-item>
<!-- Draft files not supported. -->

View File

@ -15,6 +15,7 @@
import { Component, OnInit, Injector } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { FormControl, FormBuilder } from '@angular/forms';
/**
* 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 {
constructor(logger: CoreLoggerProvider, injector: Injector) {
protected formControl: FormControl;
constructor(logger: CoreLoggerProvider, injector: Injector, protected fb: FormBuilder) {
super(logger, 'AddonQtypeEssayComponent', injector);
}
@ -34,5 +37,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
*/
ngOnInit(): void {
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>
</ion-item>
<ion-item text-wrap *ngFor="let row of question.rows">
<ion-row>
<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>
<ion-col [ngClass]='{"core-question-answer-correct": row.isCorrect === 1, "core-question-answer-incorrect": row.isCorrect === 0}'>
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">{{ row.accessibilityLabel }}</label>
<ion-select id="{{row.id}}" [name]="row.name" [attr.aria-labelledby]="'addon-qtype-match-question-' + row.id" [ngModel]="row.selected">
<ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option>
</ion-select>
<!-- @todo: select fix? -->
</ion-col>
</ion-row>
<ion-grid item-content>
<ion-row>
<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>
<ion-col [ngClass]='{"core-question-answer-correct": row.isCorrect === 1, "core-question-answer-incorrect": row.isCorrect === 0}'>
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">{{ row.accessibilityLabel }}</label>
<ion-select id="{{row.id}}" [name]="row.name" [attr.aria-labelledby]="'addon-qtype-match-question-' + row.id" [(ngModel)]="row.selected" interface="popover">
<ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option>
</ion-select>
<!-- 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>
</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. -->
<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}">
<p><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></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-checkbox>
<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}">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text>
<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>
<!-- 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-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}'>
<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-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>
</section>

View File

@ -37,6 +37,7 @@
.opacity-hide { opacity: 0; }
.core-big { font-size: 115%; }
.invisible { visibility: hidden; }
@include media-breakpoint-up(sm) {
.core-center-view .scroll-content {
@ -510,3 +511,13 @@ textarea {
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 [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">
<button tappable (click)="toggleEditor($event)">Toggle Editor</button>
</div>

View File

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

View File

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

View File

@ -123,7 +123,7 @@ export class CoreQuestionComponent implements OnInit {
promise.then(() => {
// 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.questionHelper.extractQbehaviourRedoButton(this.question);

View File

@ -319,7 +319,7 @@ export class CoreQuestionHelperProvider {
elements = Array.from(form.elements);
elements.forEach((element: HTMLInputElement) => {
const name = element.name || '';
const name = element.name || element.getAttribute('ng-reflect-name') || '';
// Ignore flag and submit inputs.
if (!name || name.match(/_:flagged$/) || element.type == 'submit' || element.tagName == 'BUTTON') {
@ -588,13 +588,11 @@ export class CoreQuestionHelperProvider {
* @param {string} [error] Error to show.
*/
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.
const now = Date.now();
if (now - this.lastErrorShown > 500) {
this.lastErrorShown = now;
this.domUtils.showErrorModal(error);
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorparsequestions', true);
}
onAbort && onAbort.emit();