MOBILE-2230 question: Implement question component
parent
521d234def
commit
e059bdd997
|
@ -67,6 +67,7 @@ import { CoreCoursesComponentsModule } from '@core/courses/components/components
|
||||||
import { CoreSitePluginsDirectivesModule } from '@core/siteplugins/directives/directives.module';
|
import { CoreSitePluginsDirectivesModule } from '@core/siteplugins/directives/directives.module';
|
||||||
import { CoreSiteHomeComponentsModule } from '@core/sitehome/components/components.module';
|
import { CoreSiteHomeComponentsModule } from '@core/sitehome/components/components.module';
|
||||||
import { CoreUserComponentsModule } from '@core/user/components/components.module';
|
import { CoreUserComponentsModule } from '@core/user/components/components.module';
|
||||||
|
import { CoreQuestionComponentsModule } from '@core/question/components/components.module';
|
||||||
|
|
||||||
// Import some components listed in entryComponents so they can be injected dynamically.
|
// Import some components listed in entryComponents so they can be injected dynamically.
|
||||||
import { CoreCourseUnsupportedModuleComponent } from '@core/course/components/unsupported-module/unsupported-module';
|
import { CoreCourseUnsupportedModuleComponent } from '@core/course/components/unsupported-module/unsupported-module';
|
||||||
|
@ -93,7 +94,7 @@ export class CoreCompileProvider {
|
||||||
protected IMPORTS = [
|
protected IMPORTS = [
|
||||||
IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule,
|
IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule,
|
||||||
CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule,
|
CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule,
|
||||||
CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule
|
CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule, CoreQuestionComponentsModule
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(protected injector: Injector, logger: CoreLoggerProvider, protected compiler: Compiler) {
|
constructor(protected injector: Injector, logger: CoreLoggerProvider, protected compiler: Compiler) {
|
||||||
|
|
|
@ -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 { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreQuestionComponent } from './question/question';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreQuestionComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CoreQuestionComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CoreQuestionComponentsModule {}
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
<!-- Question contents. -->
|
||||||
|
<core-dynamic-component *ngIf="loaded" [component]="componentClass" [data]="data" class="core-question-{{question.slot}}">
|
||||||
|
<!-- This content will only be shown if there's no component to render the question. -->
|
||||||
|
<p padding>{{ 'core.question.errorquestionnotsupported' | translate:{$a: question.type} }}</p>
|
||||||
|
</core-dynamic-component>
|
||||||
|
|
||||||
|
<!-- Sequence check input. -->
|
||||||
|
<input *ngIf="seqCheck" type="hidden" name="{{seqCheck.name}}" value="{{seqCheck.value}}" >
|
||||||
|
|
||||||
|
<!-- Question behaviour components. -->
|
||||||
|
<core-dynamic-component *ngFor="let componentClass of behaviourComponents" [component]="componentClass" [data]="data"></core-dynamic-component>
|
||||||
|
|
||||||
|
<!-- Question validation error. -->
|
||||||
|
<ion-item text-wrap class="core-error-item" *ngIf="question.validationError">
|
||||||
|
<p>{{ question.validationError }}</p>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Question behaviour buttons. -->
|
||||||
|
<ion-item text-wrap *ngFor="let button of question.behaviourButtons">
|
||||||
|
<a ion-button block (click)="action.emit(button)" [disabled]="button.disabled">{{ button.value }}</a>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Question feedback. -->
|
||||||
|
<ion-item text-wrap class="core-question-feedback-container" *ngIf="question.feedbackHtml">
|
||||||
|
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.feedbackHtml"></core-format-text></p>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Question comment. -->
|
||||||
|
<ion-item text-wrap class="core-question-comment" *ngIf="question.commentHtml">
|
||||||
|
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.commentHtml"></core-format-text></p>
|
||||||
|
</ion-item>
|
|
@ -0,0 +1,149 @@
|
||||||
|
// (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, Input, Output, OnInit, Injector, EventEmitter } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreQuestionProvider } from '../../providers/question';
|
||||||
|
import { CoreQuestionDelegate } from '../../providers/delegate';
|
||||||
|
import { CoreQuestionBehaviourDelegate } from '../../providers/behaviour-delegate';
|
||||||
|
import { CoreQuestionHelperProvider } from '../../providers/helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a question.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-question',
|
||||||
|
templateUrl: 'question.html'
|
||||||
|
})
|
||||||
|
export class CoreQuestionComponent implements OnInit {
|
||||||
|
@Input() question: any; // The question to render.
|
||||||
|
@Input() component: string; // The component the question belongs to.
|
||||||
|
@Input() componentId: number; // ID of the component the question belongs to.
|
||||||
|
@Input() attemptId: number; // Attempt ID.
|
||||||
|
@Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline.
|
||||||
|
@Output() buttonClicked: EventEmitter<any>; // Will emit an event when a behaviour button is clicked.
|
||||||
|
@Output() onAbort: EventEmitter<void>; // Will emit an event if the question should be aborted.
|
||||||
|
|
||||||
|
componentClass: any; // The class of the component to render.
|
||||||
|
data: any = {}; // Data to pass to the component.
|
||||||
|
seqCheck: {name: string, value: string}; // Sequenche check name and value (if any).
|
||||||
|
behaviourComponents: any[] = []; // Components to render the question behaviour.
|
||||||
|
loaded = false;
|
||||||
|
|
||||||
|
protected logger;
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, protected injector: Injector, protected questionDelegate: CoreQuestionDelegate,
|
||||||
|
protected utils: CoreUtilsProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate,
|
||||||
|
protected questionHelper: CoreQuestionHelperProvider, protected translate: TranslateService,
|
||||||
|
protected questionProvider: CoreQuestionProvider, protected domUtils: CoreDomUtilsProvider) {
|
||||||
|
logger = logger.getInstance('CoreQuestionComponent');
|
||||||
|
|
||||||
|
this.buttonClicked = new EventEmitter();
|
||||||
|
this.onAbort = new EventEmitter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.offlineEnabled = this.utils.isTrueOrOne(this.offlineEnabled);
|
||||||
|
|
||||||
|
if (!this.question) {
|
||||||
|
this.loaded = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the component to render the question.
|
||||||
|
this.questionDelegate.getComponentForQuestion(this.injector, this.question).then((componentClass) => {
|
||||||
|
this.componentClass = componentClass;
|
||||||
|
|
||||||
|
if (componentClass) {
|
||||||
|
// Set up the data needed by the question and behaviour components.
|
||||||
|
this.data = {
|
||||||
|
question: this.question,
|
||||||
|
component: this.component,
|
||||||
|
componentId: this.componentId,
|
||||||
|
attemptId: this.attemptId,
|
||||||
|
offlineEnabled: this.offlineEnabled,
|
||||||
|
buttonClicked: this.buttonClicked,
|
||||||
|
onAbort: this.onAbort
|
||||||
|
};
|
||||||
|
|
||||||
|
// Treat the question.
|
||||||
|
this.questionHelper.extractQuestionScripts(this.question);
|
||||||
|
|
||||||
|
// Handle question behaviour.
|
||||||
|
const behaviour = this.questionDelegate.getBehaviourForQuestion(this.question, this.question.preferredBehaviour);
|
||||||
|
if (!this.behaviourDelegate.isBehaviourSupported(behaviour)) {
|
||||||
|
// Behaviour not supported, abort.
|
||||||
|
this.logger.warn('Aborting question because the behaviour is not supported.', this.question.name);
|
||||||
|
this.questionHelper.showComponentError(this.onAbort,
|
||||||
|
this.translate.instant('addon.mod_quiz.errorbehaviournotsupported') + ' ' + behaviour);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the sequence check (hidden input). This is required.
|
||||||
|
this.seqCheck = this.questionHelper.getQuestionSequenceCheckFromHtml(this.question.html);
|
||||||
|
if (!this.seqCheck) {
|
||||||
|
this.logger.warn('Aborting question because couldn\'t retrieve sequence check.', this.question.name);
|
||||||
|
this.questionHelper.showComponentError(this.onAbort);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load local answers if offline is enabled.
|
||||||
|
let promise;
|
||||||
|
if (this.offlineEnabled) {
|
||||||
|
promise = this.questionProvider.getQuestionAnswers(this.component, this.attemptId, this.question.slot)
|
||||||
|
.then((answers) => {
|
||||||
|
this.question.localAnswers = this.questionProvider.convertAnswersArrayToObject(answers, true);
|
||||||
|
}).catch(() => {
|
||||||
|
this.question.localAnswers = {};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.question.localAnswers = {};
|
||||||
|
promise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(() => {
|
||||||
|
// Handle behaviour.
|
||||||
|
this.behaviourDelegate.handleQuestion(this.question, this.question.preferredBehaviour).then((comps) => {
|
||||||
|
this.behaviourComponents = comps;
|
||||||
|
});
|
||||||
|
this.questionHelper.extractQbehaviourRedoButton(this.question);
|
||||||
|
this.question.html = this.domUtils.removeElementFromHtml(this.question.html, '.im-controls');
|
||||||
|
|
||||||
|
// Extract the validation error of the question.
|
||||||
|
this.question.validationError = this.questionHelper.getValidationErrorFromHtml(this.question.html);
|
||||||
|
|
||||||
|
// Load the local answers in the HTML.
|
||||||
|
this.questionHelper.loadLocalAnswersInHtml(this.question);
|
||||||
|
|
||||||
|
// Try to extract the feedback and comment for the question.
|
||||||
|
this.questionHelper.extractQuestionFeedback(this.question);
|
||||||
|
this.questionHelper.extractQuestionComment(this.question);
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,306 @@
|
||||||
|
// (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, EventEmitter } from '@angular/core';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreQuestionProvider } from './question';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service with some common functions to handle questions.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CoreQuestionHelperProvider {
|
||||||
|
protected lastErrorShown = 0;
|
||||||
|
protected div = document.createElement('div'); // A div element to search in HTML code.
|
||||||
|
|
||||||
|
constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider,
|
||||||
|
private questionProvider: CoreQuestionProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a behaviour button to the question's "behaviourButtons" property.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
* @param {HTMLInputElement} button Behaviour button (DOM element).
|
||||||
|
*/
|
||||||
|
protected addBehaviourButton(question: any, button: HTMLInputElement): void {
|
||||||
|
if (!button || !question) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!question.behaviourButtons) {
|
||||||
|
question.behaviourButtons = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the data we want.
|
||||||
|
question.behaviourButtons.push({
|
||||||
|
id: button.id,
|
||||||
|
name: button.name,
|
||||||
|
value: button.value,
|
||||||
|
disabled: button.disabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the question has a redo button and, if so, add it to "behaviourButtons" property
|
||||||
|
* and remove it from the HTML.
|
||||||
|
*
|
||||||
|
* @param {any} question Question to treat.
|
||||||
|
*/
|
||||||
|
extractQbehaviourRedoButton(question: any): void {
|
||||||
|
// Create a fake div element so we can search using querySelector.
|
||||||
|
const redoSelector = 'input[type="submit"][name*=redoslot], input[type="submit"][name*=tryagain]';
|
||||||
|
|
||||||
|
// Search redo button in feedback.
|
||||||
|
if (!this.searchBehaviourButton(question, 'html', '.outcome ' + redoSelector)) {
|
||||||
|
// Not found in question HTML.
|
||||||
|
if (question.feedbackHtml) {
|
||||||
|
// We extracted the feedback already, search it in there.
|
||||||
|
if (this.searchBehaviourButton(question, 'feedbackHtml', redoSelector)) {
|
||||||
|
// Button found, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button still not found. Now search in the info box if it exists.
|
||||||
|
if (question.infoHtml) {
|
||||||
|
this.searchBehaviourButton(question, 'infoHtml', redoSelector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the comment from the question HTML code and adds it in a new "commentHtml" property.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
*/
|
||||||
|
extractQuestionComment(question: any): void {
|
||||||
|
this.extractQuestionLastElementNotInContent(question, '.comment', 'commentHtml');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the feedback from the question HTML code and adds it in a new "feedbackHtml" property.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
*/
|
||||||
|
extractQuestionFeedback(question: any): void {
|
||||||
|
this.extractQuestionLastElementNotInContent(question, '.outcome', 'feedbackHtml');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the info box from a question and add it to an "infoHtml" property.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
* @param {string} selector Selector to search the element.
|
||||||
|
*/
|
||||||
|
extractQuestionInfoBox(question: any, selector: string): void {
|
||||||
|
this.extractQuestionLastElementNotInContent(question, selector, 'infoHtml');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches the last occurrence of a certain element and check it's not in the question contents.
|
||||||
|
* If found, removes it from the question HTML and adds it to a new property inside question.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
* @param {string} selector Selector to search the element.
|
||||||
|
* @param {string} attrName Name of the attribute to store the HTML in.
|
||||||
|
*/
|
||||||
|
protected extractQuestionLastElementNotInContent(question: any, selector: string, attrName: string): void {
|
||||||
|
this.div.innerHTML = question.html;
|
||||||
|
|
||||||
|
const matches = <HTMLElement[]> Array.from(this.div.querySelectorAll(selector));
|
||||||
|
|
||||||
|
// Get the last element and check it's not in the question contents.
|
||||||
|
let last = matches.pop();
|
||||||
|
while (last) {
|
||||||
|
if (!this.domUtils.closest(last, '.formulation')) {
|
||||||
|
// Not in question contents. Add it to a separate attribute and remove it from the HTML.
|
||||||
|
question[attrName] = last.innerHTML;
|
||||||
|
last.parentElement.removeChild(last);
|
||||||
|
question.html = this.div.innerHTML;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's inside the question content, treat next element.
|
||||||
|
last = matches.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the scripts from a question's HTML and adds it in a new 'scriptsCode' property.
|
||||||
|
* It will also search for init_question functions of the question type and add the object to an 'initObjects' property.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
*/
|
||||||
|
extractQuestionScripts(question: any): void {
|
||||||
|
question.scriptsCode = '';
|
||||||
|
question.initObjects = [];
|
||||||
|
|
||||||
|
if (question.html) {
|
||||||
|
// Search the scripts.
|
||||||
|
const matches = question.html.match(/<script[^>]*>[\s\S]*?<\/script>/mg);
|
||||||
|
if (!matches) {
|
||||||
|
// No scripts, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.forEach((match: string) => {
|
||||||
|
// Add the script to scriptsCode and remove it from html.
|
||||||
|
question.scriptsCode += match;
|
||||||
|
question.html = question.html.replace(match, '');
|
||||||
|
|
||||||
|
// Search init_question functions for this type.
|
||||||
|
const initMatches = match.match(new RegExp('M\.qtype_' + question.type + '\.init_question\\(.*?}\\);', 'mg'));
|
||||||
|
if (initMatches) {
|
||||||
|
let initMatch = initMatches.pop();
|
||||||
|
|
||||||
|
// Remove start and end of the match, we only want the object.
|
||||||
|
initMatch = initMatch.replace('M.qtype_' + question.type + '.init_question(', '');
|
||||||
|
initMatch = initMatch.substr(0, initMatch.length - 2);
|
||||||
|
|
||||||
|
// Try to convert it to an object and add it to the question.
|
||||||
|
question.initObjects = this.textUtils.parseJSON(initMatch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sequence check from a question HTML.
|
||||||
|
*
|
||||||
|
* @param {string} html Question's HTML.
|
||||||
|
* @return {{name: string, value: string}} Object with the sequencecheck name and value.
|
||||||
|
*/
|
||||||
|
getQuestionSequenceCheckFromHtml(html: string): {name: string, value: string} {
|
||||||
|
if (html) {
|
||||||
|
this.div.innerHTML = html;
|
||||||
|
|
||||||
|
// Search the input holding the sequencecheck.
|
||||||
|
const input = <HTMLInputElement> this.div.querySelector('input[name*=sequencecheck]');
|
||||||
|
if (input && typeof input.name != 'undefined' && typeof input.value != 'undefined') {
|
||||||
|
return {
|
||||||
|
name: input.name,
|
||||||
|
value: input.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation error message from a question HTML if it's there.
|
||||||
|
*
|
||||||
|
* @param {string} html Question's HTML.
|
||||||
|
* @return {string} Validation error message if present.
|
||||||
|
*/
|
||||||
|
getValidationErrorFromHtml(html: string): string {
|
||||||
|
this.div.innerHTML = html;
|
||||||
|
|
||||||
|
return this.domUtils.getContentsOfElement(this.div, '.validationerror');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each input element found in the HTML, search if there's a local answer stored and
|
||||||
|
* override the HTML's value with the local one.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
*/
|
||||||
|
loadLocalAnswersInHtml(question: any): void {
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.innerHTML = question.html;
|
||||||
|
|
||||||
|
// Search all input elements.
|
||||||
|
Array.from(form.elements).forEach((element: HTMLInputElement | HTMLButtonElement) => {
|
||||||
|
let name = element.name || '';
|
||||||
|
// Ignore flag and submit inputs.
|
||||||
|
if (!name || name.match(/_:flagged$/) || element.type == 'submit' || element.tagName == 'BUTTON') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search if there's a local answer.
|
||||||
|
name = this.questionProvider.removeQuestionPrefix(name);
|
||||||
|
if (question.localAnswers && typeof question.localAnswers[name] != 'undefined') {
|
||||||
|
|
||||||
|
if (element.tagName == 'TEXTAREA') {
|
||||||
|
// Just put the answer inside the textarea.
|
||||||
|
element.innerHTML = question.localAnswers[name];
|
||||||
|
} else if (element.tagName == 'SELECT') {
|
||||||
|
// Search the selected option and select it.
|
||||||
|
const selected = element.querySelector('option[value="' + question.localAnswers[name] + '"]');
|
||||||
|
if (selected) {
|
||||||
|
selected.setAttribute('selected', 'selected');
|
||||||
|
}
|
||||||
|
} else if (element.type == 'radio' || element.type == 'checkbox') {
|
||||||
|
// Check if this radio or checkbox is selected.
|
||||||
|
if (element.value == question.localAnswers[name]) {
|
||||||
|
element.setAttribute('checked', 'checked');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Put the answer in the value.
|
||||||
|
element.setAttribute('value', question.localAnswers[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the question HTML.
|
||||||
|
question.html = form.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search a behaviour button in a certain question property containing HTML.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
* @param {string} htmlProperty The name of the property containing the HTML to search.
|
||||||
|
* @param {string} selector The selector to find the button.
|
||||||
|
* @return {boolean} Whether the button is found.
|
||||||
|
*/
|
||||||
|
protected searchBehaviourButton(question: any, htmlProperty: string, selector: string): boolean {
|
||||||
|
this.div.innerHTML = question[htmlProperty];
|
||||||
|
|
||||||
|
const button = <HTMLInputElement> this.div.querySelector(selector);
|
||||||
|
if (button) {
|
||||||
|
// Add a behaviour button to the question's "behaviourButtons" property.
|
||||||
|
this.addBehaviourButton(question, button);
|
||||||
|
|
||||||
|
// Remove the button from the HTML.
|
||||||
|
button.parentElement.removeChild(button);
|
||||||
|
|
||||||
|
// Update the question's html.
|
||||||
|
question[htmlProperty] = this.div.innerHTML;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to show a parsing error and abort.
|
||||||
|
*
|
||||||
|
* @param {EventEmitter<void>} [onAbort] If supplied, will emit an event.
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAbort && onAbort.emit();
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,12 +18,14 @@ import { CoreQuestionDelegate } from './providers/delegate';
|
||||||
import { CoreQuestionBehaviourDelegate } from './providers/behaviour-delegate';
|
import { CoreQuestionBehaviourDelegate } from './providers/behaviour-delegate';
|
||||||
import { CoreQuestionDefaultHandler } from './providers/default-question-handler';
|
import { CoreQuestionDefaultHandler } from './providers/default-question-handler';
|
||||||
import { CoreQuestionBehaviourDefaultHandler } from './providers/default-behaviour-handler';
|
import { CoreQuestionBehaviourDefaultHandler } from './providers/default-behaviour-handler';
|
||||||
|
import { CoreQuestionHelperProvider } from './providers/helper';
|
||||||
|
|
||||||
// List of providers (without handlers).
|
// List of providers (without handlers).
|
||||||
export const CORE_QUESTION_PROVIDERS: any[] = [
|
export const CORE_QUESTION_PROVIDERS: any[] = [
|
||||||
CoreQuestionProvider,
|
CoreQuestionProvider,
|
||||||
CoreQuestionDelegate,
|
CoreQuestionDelegate,
|
||||||
CoreQuestionBehaviourDelegate
|
CoreQuestionBehaviourDelegate,
|
||||||
|
CoreQuestionHelperProvider
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
Loading…
Reference in New Issue