MOBILE-2389 qtype: Implement match and randomsamatch types
parent
be573d240b
commit
a89737a5d3
|
@ -0,0 +1,19 @@
|
||||||
|
<section ion-list class="addon-qtype-match-container" *ngIf="question.loaded">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<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-item>
|
||||||
|
</section>
|
|
@ -0,0 +1,40 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||||
|
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a match question.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-qtype-match',
|
||||||
|
templateUrl: 'match.html'
|
||||||
|
})
|
||||||
|
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, questionHelper: CoreQuestionHelperProvider, domUtils: CoreDomUtilsProvider) {
|
||||||
|
super(logger, 'AddonQtypeMatchComponent', questionHelper, domUtils);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.initMatchComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonQtypeMatchHandler } from './providers/handler';
|
||||||
|
import { AddonQtypeMatchComponent } from './component/match';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonQtypeMatchComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreDirectivesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonQtypeMatchHandler
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonQtypeMatchComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonQtypeMatchComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonQtypeMatchModule {
|
||||||
|
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeMatchHandler) {
|
||||||
|
questionDelegate.registerHandler(handler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable, Injector } from '@angular/core';
|
||||||
|
import { CoreQuestionProvider } from '@core/question/providers/question';
|
||||||
|
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||||
|
import { AddonQtypeMatchComponent } from '../component/match';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support match question type.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonQtypeMatchHandler implements CoreQuestionHandler {
|
||||||
|
name = 'AddonQtypeMatch';
|
||||||
|
type = 'qtype_match';
|
||||||
|
|
||||||
|
constructor(private questionProvider: CoreQuestionProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the name of the behaviour to use for the question.
|
||||||
|
* If the question should use the default behaviour you shouldn't implement this function.
|
||||||
|
*
|
||||||
|
* @param {any} question The question.
|
||||||
|
* @param {string} behaviour The default behaviour.
|
||||||
|
* @return {string} The behaviour to use.
|
||||||
|
*/
|
||||||
|
getBehaviour(question: any, behaviour: string): string {
|
||||||
|
if (behaviour === 'interactive') {
|
||||||
|
return 'interactivecountback';
|
||||||
|
}
|
||||||
|
|
||||||
|
return behaviour;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the question.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @param {Injector} injector Injector.
|
||||||
|
* @param {any} question The question to render.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(injector: Injector, question: any): any | Promise<any> {
|
||||||
|
return AddonQtypeMatchComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a response is complete.
|
||||||
|
*
|
||||||
|
* @param {any} question The question.
|
||||||
|
* @param {any} answers Object with the question answers (without prefix).
|
||||||
|
* @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
|
||||||
|
*/
|
||||||
|
isCompleteResponse(question: any, answers: any): number {
|
||||||
|
// We should always get a value for each select so we can assume we receive all the possible answers.
|
||||||
|
for (const name in answers) {
|
||||||
|
const value = answers[name];
|
||||||
|
if (!value || value === '0') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a student has provided enough of an answer for the question to be graded automatically,
|
||||||
|
* or whether it must be considered aborted.
|
||||||
|
*
|
||||||
|
* @param {any} question The question.
|
||||||
|
* @param {any} answers Object with the question answers (without prefix).
|
||||||
|
* @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine.
|
||||||
|
*/
|
||||||
|
isGradableResponse(question: any, answers: any): number {
|
||||||
|
// We should always get a value for each select so we can assume we receive all the possible answers.
|
||||||
|
for (const name in answers) {
|
||||||
|
const value = answers[name];
|
||||||
|
if (value && value !== '0') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if two responses are the same.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
* @param {any} prevAnswers Object with the previous question answers.
|
||||||
|
* @param {any} newAnswers Object with the new question answers.
|
||||||
|
* @return {boolean} Whether they're the same.
|
||||||
|
*/
|
||||||
|
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
|
||||||
|
return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,8 +15,10 @@
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
|
import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
|
||||||
import { AddonQtypeDescriptionModule } from './description/description.module';
|
import { AddonQtypeDescriptionModule } from './description/description.module';
|
||||||
|
import { AddonQtypeMatchModule } from './match/match.module';
|
||||||
import { AddonQtypeMultichoiceModule } from './multichoice/multichoice.module';
|
import { AddonQtypeMultichoiceModule } from './multichoice/multichoice.module';
|
||||||
import { AddonQtypeNumericalModule } from './numerical/numerical.module';
|
import { AddonQtypeNumericalModule } from './numerical/numerical.module';
|
||||||
|
import { AddonQtypeRandomSaMatchModule } from './randomsamatch/randomsamatch.module';
|
||||||
import { AddonQtypeShortAnswerModule } from './shortanswer/shortanswer.module';
|
import { AddonQtypeShortAnswerModule } from './shortanswer/shortanswer.module';
|
||||||
import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
||||||
|
|
||||||
|
@ -25,8 +27,10 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
||||||
imports: [
|
imports: [
|
||||||
AddonQtypeCalculatedMultiModule,
|
AddonQtypeCalculatedMultiModule,
|
||||||
AddonQtypeDescriptionModule,
|
AddonQtypeDescriptionModule,
|
||||||
|
AddonQtypeMatchModule,
|
||||||
AddonQtypeMultichoiceModule,
|
AddonQtypeMultichoiceModule,
|
||||||
AddonQtypeNumericalModule,
|
AddonQtypeNumericalModule,
|
||||||
|
AddonQtypeRandomSaMatchModule,
|
||||||
AddonQtypeShortAnswerModule,
|
AddonQtypeShortAnswerModule,
|
||||||
AddonQtypeTrueFalseModule
|
AddonQtypeTrueFalseModule
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable, Injector } from '@angular/core';
|
||||||
|
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||||
|
import { AddonQtypeMatchHandler } from '@addon/qtype/match/providers/handler';
|
||||||
|
import { AddonQtypeMatchComponent } from '@addon/qtype/match/component/match';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support random short-answer matching question type.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler {
|
||||||
|
name = 'AddonQtypeRandomSaMatch';
|
||||||
|
type = 'qtype_randomsamatch';
|
||||||
|
|
||||||
|
constructor(private matchHandler: AddonQtypeMatchHandler) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the question.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @param {Injector} injector Injector.
|
||||||
|
* @param {any} question The question to render.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(injector: Injector, question: any): any | Promise<any> {
|
||||||
|
// Random behaves like a match question, use the same component.
|
||||||
|
return AddonQtypeMatchComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a response is complete.
|
||||||
|
*
|
||||||
|
* @param {any} question The question.
|
||||||
|
* @param {any} answers Object with the question answers (without prefix).
|
||||||
|
* @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
|
||||||
|
*/
|
||||||
|
isCompleteResponse(question: any, answers: any): number {
|
||||||
|
// This question behaves like a match question.
|
||||||
|
return this.matchHandler.isCompleteResponse(question, answers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a student has provided enough of an answer for the question to be graded automatically,
|
||||||
|
* or whether it must be considered aborted.
|
||||||
|
*
|
||||||
|
* @param {any} question The question.
|
||||||
|
* @param {any} answers Object with the question answers (without prefix).
|
||||||
|
* @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine.
|
||||||
|
*/
|
||||||
|
isGradableResponse(question: any, answers: any): number {
|
||||||
|
// This question behaves like a match question.
|
||||||
|
return this.matchHandler.isGradableResponse(question, answers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if two responses are the same.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
* @param {any} prevAnswers Object with the previous question answers.
|
||||||
|
* @param {any} newAnswers Object with the new question answers.
|
||||||
|
* @return {boolean} Whether they're the same.
|
||||||
|
*/
|
||||||
|
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
|
||||||
|
// This question behaves like a match question.
|
||||||
|
return this.matchHandler.isSameResponse(question, prevAnswers, newAnswers);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||||
|
import { AddonQtypeRandomSaMatchHandler } from './providers/handler';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonQtypeRandomSaMatchHandler
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonQtypeRandomSaMatchModule {
|
||||||
|
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeRandomSaMatchHandler) {
|
||||||
|
questionDelegate.registerHandler(handler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -96,6 +96,98 @@ export class CoreQuestionBaseComponent {
|
||||||
return questionDiv;
|
return questionDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a question component with a "match" behaviour.
|
||||||
|
*
|
||||||
|
* @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
|
||||||
|
*/
|
||||||
|
initMatchComponent(): void | HTMLElement {
|
||||||
|
const questionDiv = this.initComponent();
|
||||||
|
|
||||||
|
if (questionDiv) {
|
||||||
|
// Find rows.
|
||||||
|
const rows = Array.from(questionDiv.querySelectorAll('tr'));
|
||||||
|
if (!rows || !rows.length) {
|
||||||
|
this.logger.warn('Aborting because couldn\'t find any row.', this.question.name);
|
||||||
|
|
||||||
|
return this.questionHelper.showComponentError(this.onAbort);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.question.rows = [];
|
||||||
|
|
||||||
|
for (const i in rows) {
|
||||||
|
const row = rows[i],
|
||||||
|
rowModel: any = {},
|
||||||
|
columns = Array.from(row.querySelectorAll('td'));
|
||||||
|
|
||||||
|
if (!columns || columns.length < 2) {
|
||||||
|
this.logger.warn('Aborting because couldn\'t the right columns.', this.question.name);
|
||||||
|
|
||||||
|
return this.questionHelper.showComponentError(this.onAbort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the row's text. It should be in the first column.
|
||||||
|
rowModel.text = columns[0].innerHTML;
|
||||||
|
|
||||||
|
// Get the select and the options.
|
||||||
|
const select = columns[1].querySelector('select'),
|
||||||
|
options = Array.from(columns[1].querySelectorAll('option'));
|
||||||
|
|
||||||
|
if (!select || !options || !options.length) {
|
||||||
|
this.logger.warn('Aborting because couldn\'t find select or options.', this.question.name);
|
||||||
|
|
||||||
|
return this.questionHelper.showComponentError(this.onAbort);
|
||||||
|
}
|
||||||
|
|
||||||
|
rowModel.id = select.id;
|
||||||
|
rowModel.name = select.name;
|
||||||
|
rowModel.disabled = select.disabled;
|
||||||
|
rowModel.selected = false;
|
||||||
|
rowModel.options = [];
|
||||||
|
|
||||||
|
// Check if answer is correct.
|
||||||
|
if (columns[1].className.indexOf('incorrect') >= 0) {
|
||||||
|
rowModel.isCorrect = 0;
|
||||||
|
} else if (columns[1].className.indexOf('correct') >= 0) {
|
||||||
|
rowModel.isCorrect = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat each option.
|
||||||
|
for (const j in options) {
|
||||||
|
const optionEl = options[j];
|
||||||
|
|
||||||
|
if (typeof optionEl.value == 'undefined') {
|
||||||
|
this.logger.warn('Aborting because couldn\'t find the value of an option.', this.question.name);
|
||||||
|
|
||||||
|
return this.questionHelper.showComponentError(this.onAbort);
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
value: optionEl.value,
|
||||||
|
label: optionEl.innerHTML,
|
||||||
|
selected: optionEl.selected
|
||||||
|
};
|
||||||
|
|
||||||
|
if (option.selected) {
|
||||||
|
rowModel.selected = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
rowModel.options.push(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the accessibility label.
|
||||||
|
const accessibilityLabel = columns[1].querySelector('label.accesshide');
|
||||||
|
rowModel.accessibilityLabel = accessibilityLabel && accessibilityLabel.innerHTML;
|
||||||
|
|
||||||
|
this.question.rows.push(rowModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.question.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return questionDiv;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a question component with a multiple choice (checkbox) or single choice (radio).
|
* Initialize a question component with a multiple choice (checkbox) or single choice (radio).
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue