+
+
+
+
+ {{option.label}}
+
+
+
+
+
+
diff --git a/src/addon/qtype/match/component/match.ts b/src/addon/qtype/match/component/match.ts
new file mode 100644
index 000000000..117e59af5
--- /dev/null
+++ b/src/addon/qtype/match/component/match.ts
@@ -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();
+ }
+}
diff --git a/src/addon/qtype/match/match.module.ts b/src/addon/qtype/match/match.module.ts
new file mode 100644
index 000000000..b4b56f334
--- /dev/null
+++ b/src/addon/qtype/match/match.module.ts
@@ -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);
+ }
+}
diff --git a/src/addon/qtype/match/providers/handler.ts b/src/addon/qtype/match/providers/handler.ts
new file mode 100644
index 000000000..d9519ad04
--- /dev/null
+++ b/src/addon/qtype/match/providers/handler.ts
@@ -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} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector, question: any): any | Promise {
+ 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} True or promise resolved with true if enabled.
+ */
+ isEnabled(): boolean | Promise {
+ 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);
+ }
+}
diff --git a/src/addon/qtype/qtype.module.ts b/src/addon/qtype/qtype.module.ts
index c7ab3ab46..2f735f24d 100644
--- a/src/addon/qtype/qtype.module.ts
+++ b/src/addon/qtype/qtype.module.ts
@@ -15,8 +15,10 @@
import { NgModule } from '@angular/core';
import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
import { AddonQtypeDescriptionModule } from './description/description.module';
+import { AddonQtypeMatchModule } from './match/match.module';
import { AddonQtypeMultichoiceModule } from './multichoice/multichoice.module';
import { AddonQtypeNumericalModule } from './numerical/numerical.module';
+import { AddonQtypeRandomSaMatchModule } from './randomsamatch/randomsamatch.module';
import { AddonQtypeShortAnswerModule } from './shortanswer/shortanswer.module';
import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
@@ -25,8 +27,10 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
imports: [
AddonQtypeCalculatedMultiModule,
AddonQtypeDescriptionModule,
+ AddonQtypeMatchModule,
AddonQtypeMultichoiceModule,
AddonQtypeNumericalModule,
+ AddonQtypeRandomSaMatchModule,
AddonQtypeShortAnswerModule,
AddonQtypeTrueFalseModule
],
diff --git a/src/addon/qtype/randomsamatch/providers/handler.ts b/src/addon/qtype/randomsamatch/providers/handler.ts
new file mode 100644
index 000000000..eca943805
--- /dev/null
+++ b/src/addon/qtype/randomsamatch/providers/handler.ts
@@ -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} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector, question: any): any | Promise {
+ // 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} True or promise resolved with true if enabled.
+ */
+ isEnabled(): boolean | Promise {
+ 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);
+ }
+}
diff --git a/src/addon/qtype/randomsamatch/randomsamatch.module.ts b/src/addon/qtype/randomsamatch/randomsamatch.module.ts
new file mode 100644
index 000000000..4920b0506
--- /dev/null
+++ b/src/addon/qtype/randomsamatch/randomsamatch.module.ts
@@ -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);
+ }
+}
diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts
index 859c14a67..1d9f29a04 100644
--- a/src/core/question/classes/base-question-component.ts
+++ b/src/core/question/classes/base-question-component.ts
@@ -96,6 +96,98 @@ export class CoreQuestionBaseComponent {
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).
*