diff --git a/src/addon/qbehaviour/adaptive/providers/handler.ts b/src/addon/qbehaviour/adaptive/providers/handler.ts index 37a5e908a..90ad015b8 100644 --- a/src/addon/qbehaviour/adaptive/providers/handler.ts +++ b/src/addon/qbehaviour/adaptive/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourAdaptiveHandler implements CoreQuestionBehaviourHand * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts b/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts index 3f788eea5..a8ebebb0b 100644 --- a/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts +++ b/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourAdaptiveNoPenaltyHandler implements CoreQuestionBeha * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/addon/qbehaviour/deferredcbm/providers/handler.ts b/src/addon/qbehaviour/deferredcbm/providers/handler.ts index e15d47c4d..fb11cb208 100644 --- a/src/addon/qbehaviour/deferredcbm/providers/handler.ts +++ b/src/addon/qbehaviour/deferredcbm/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreQuestionState } from '@core/question/providers/question'; @@ -55,11 +55,12 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { if (this.questionHelper.extractQbehaviourCBM(question)) { return [AddonQbehaviourDeferredCBMComponent]; } diff --git a/src/addon/qbehaviour/immediatecbm/providers/handler.ts b/src/addon/qbehaviour/immediatecbm/providers/handler.ts index 0f1493809..4e1aa9ce4 100644 --- a/src/addon/qbehaviour/immediatecbm/providers/handler.ts +++ b/src/addon/qbehaviour/immediatecbm/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { AddonQbehaviourDeferredCBMComponent } from '@addon/qbehaviour/deferredcbm/component/deferredcbm'; @@ -35,11 +35,12 @@ export class AddonQbehaviourImmediateCBMHandler implements CoreQuestionBehaviour * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); if (this.questionHelper.extractQbehaviourCBM(question)) { diff --git a/src/addon/qbehaviour/immediatefeedback/providers/handler.ts b/src/addon/qbehaviour/immediatefeedback/providers/handler.ts index 304e71bdc..2103bba6c 100644 --- a/src/addon/qbehaviour/immediatefeedback/providers/handler.ts +++ b/src/addon/qbehaviour/immediatefeedback/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourImmediateFeedbackHandler implements CoreQuestionBeha * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/addon/qbehaviour/informationitem/providers/handler.ts b/src/addon/qbehaviour/informationitem/providers/handler.ts index ac2df43f2..21d7181ed 100644 --- a/src/addon/qbehaviour/informationitem/providers/handler.ts +++ b/src/addon/qbehaviour/informationitem/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -52,11 +52,12 @@ export class AddonQbehaviourInformationItemHandler implements CoreQuestionBehavi * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { if (this.questionHelper.extractQbehaviourSeenInput(question)) { return [AddonQbehaviourInformationItemComponent]; } diff --git a/src/addon/qbehaviour/interactive/providers/handler.ts b/src/addon/qbehaviour/interactive/providers/handler.ts index af6d6024c..5504da1cc 100644 --- a/src/addon/qbehaviour/interactive/providers/handler.ts +++ b/src/addon/qbehaviour/interactive/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourInteractiveHandler implements CoreQuestionBehaviourH * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/addon/qbehaviour/interactivecountback/providers/handler.ts b/src/addon/qbehaviour/interactivecountback/providers/handler.ts index 612abb104..54646e427 100644 --- a/src/addon/qbehaviour/interactivecountback/providers/handler.ts +++ b/src/addon/qbehaviour/interactivecountback/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourInteractiveCountbackHandler implements CoreQuestionB * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/core/compile/components/compile-html/compile-html.ts b/src/core/compile/components/compile-html/compile-html.ts index 16ca3b26f..c530b08f2 100644 --- a/src/core/compile/components/compile-html/compile-html.ts +++ b/src/core/compile/components/compile-html/compile-html.ts @@ -14,11 +14,10 @@ import { Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ViewChild, ComponentRef, SimpleChange, ChangeDetectorRef, - ElementRef, Optional + ElementRef, Optional, Output, EventEmitter } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreCompileProvider } from '../../providers/compile'; -import { BehaviorSubject } from 'rxjs'; /** * This component has a behaviour similar to $compile for AngularJS. Given an HTML code, it will compile it so all its @@ -43,18 +42,17 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { @Input() text: string; // The HTML text to display. @Input() javascript: string; // The Javascript to execute in the component. @Input() jsData: any; // Data to pass to the fake component. + @Output() created: EventEmitter = new EventEmitter(); // Will emit an event when the component is instantiated. // Get the container where to put the content. @ViewChild('dynamicComponent', { read: ViewContainerRef }) container: ViewContainerRef; protected componentRef: ComponentRef; protected element; - componentObservable: BehaviorSubject; // An observable to notify observers when the component is instantiated. constructor(protected compileProvider: CoreCompileProvider, protected cdr: ChangeDetectorRef, element: ElementRef, @Optional() protected navCtrl: NavController) { this.element = element.nativeElement; - this.componentObservable = new BehaviorSubject(null); } /** @@ -70,7 +68,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { if (factory) { // Create the component. this.componentRef = this.container.createComponent(factory); - this.componentObservable.next(this.componentRef.instance); + this.created.emit(this.componentRef.instance); } }); } diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index bbd988366..70b3c7fb1 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -75,6 +75,9 @@ import { CoreCourseFormatSingleActivityComponent } from '@core/course/formats/si import { CoreSitePluginsModuleIndexComponent } from '@core/siteplugins/components/module-index/module-index'; import { CoreSitePluginsCourseOptionComponent } from '@core/siteplugins/components/course-option/course-option'; import { CoreSitePluginsCourseFormatComponent } from '@core/siteplugins/components/course-format/course-format'; +import { CoreSitePluginsQuestionComponent } from '@core/siteplugins/components/question/question'; +import { CoreSitePluginsQuestionBehaviourComponent } from '@core/siteplugins/components/question-behaviour/question-behaviour'; +import { CoreSitePluginsUserProfileFieldComponent } from '@core/siteplugins/components/user-profile-field/user-profile-field'; /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. @@ -203,6 +206,9 @@ export class CoreCompileProvider { instance['CoreSitePluginsModuleIndexComponent'] = CoreSitePluginsModuleIndexComponent; instance['CoreSitePluginsCourseOptionComponent'] = CoreSitePluginsCourseOptionComponent; instance['CoreSitePluginsCourseFormatComponent'] = CoreSitePluginsCourseFormatComponent; + instance['CoreSitePluginsQuestionComponent'] = CoreSitePluginsQuestionComponent; + instance['CoreSitePluginsQuestionBehaviourComponent'] = CoreSitePluginsQuestionBehaviourComponent; + instance['CoreSitePluginsUserProfileFieldComponent'] = CoreSitePluginsUserProfileFieldComponent; } /** diff --git a/src/core/question/components/question/question.ts b/src/core/question/components/question/question.ts index 57913acc9..f5cc7666c 100644 --- a/src/core/question/components/question/question.ts +++ b/src/core/question/components/question/question.ts @@ -123,11 +123,15 @@ export class CoreQuestionComponent implements OnInit { promise.then(() => { // Handle behaviour. - this.behaviourDelegate.handleQuestion(this.question.preferredBehaviour, this.question).then((comps) => { + this.behaviourDelegate.handleQuestion(this.injector, this.question.preferredBehaviour, this.question) + .then((comps) => { this.behaviourComponents = comps; + }).finally(() => { + this.question.html = this.domUtils.removeElementFromHtml(this.question.html, '.im-controls'); + this.loaded = true; }); + 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); @@ -138,8 +142,6 @@ export class CoreQuestionComponent implements OnInit { // Try to extract the feedback and comment for the question. this.questionHelper.extractQuestionFeedback(this.question); this.questionHelper.extractQuestionComment(this.question); - - this.loaded = true; }); } }).catch(() => { diff --git a/src/core/question/providers/behaviour-delegate.ts b/src/core/question/providers/behaviour-delegate.ts index 3ec8e6249..9333e87f9 100644 --- a/src/core/question/providers/behaviour-delegate.ts +++ b/src/core/question/providers/behaviour-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; @@ -48,11 +48,12 @@ export interface CoreQuestionBehaviourHandler extends CoreDelegateHandler { * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion?(question: any): any[] | Promise; + handleQuestion?(injector: Injector, question: any): any[] | Promise; } /** @@ -90,14 +91,15 @@ export class CoreQuestionBehaviourDelegate extends CoreDelegate { * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return a directive to render it. * + * @param {Injector} injector Injector. * @param {string} behaviour Default behaviour. * @param {any} question The question. * @return {Promise} Promise resolved with components to render some extra data in the question. */ - handleQuestion(behaviour: string, question: any): Promise { + handleQuestion(injector: Injector, behaviour: string, question: any): Promise { behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour); - return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'handleQuestion', [question])); + return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'handleQuestion', [injector, question])); } /** diff --git a/src/core/question/providers/default-behaviour-handler.ts b/src/core/question/providers/default-behaviour-handler.ts index 1eb790924..c26be4549 100644 --- a/src/core/question/providers/default-behaviour-handler.ts +++ b/src/core/question/providers/default-behaviour-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from './behaviour-delegate'; import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question'; @@ -46,11 +46,12 @@ export class CoreQuestionBehaviourDefaultHandler implements CoreQuestionBehaviou * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Nothing to do. return; } diff --git a/src/core/siteplugins/classes/compile-init-component.ts b/src/core/siteplugins/classes/compile-init-component.ts new file mode 100644 index 000000000..9f9e7270f --- /dev/null +++ b/src/core/siteplugins/classes/compile-init-component.ts @@ -0,0 +1,59 @@ +// (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 { CoreSitePluginsProvider } from '../providers/siteplugins'; + +/** + * Base class for components that will display a component using core-compile-html and want to call a + * componentInit function returned by the handler JS. + */ +export class CoreSitePluginsCompileInitComponent { + content = ''; // Content. + jsData: any; // Data to pass to the component. + protected handlerSchema: any; // The handler data. + + constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + + /** + * Function called when the component is created. + * + * @param {any} instance The component instance. + */ + componentCreated(instance: any): void { + // Check if the JS defined an init function. + if (instance && this.handlerSchema && this.handlerSchema.methodJSResult && + this.handlerSchema.methodJSResult.componentInit) { + this.handlerSchema.methodJSResult.componentInit.apply(instance); + } + } + + /** + * Get the handler data. + * + * @param {string} name The name of the handler. + */ + getHandlerData(name: string): void { + // Retrieve the handler data. + const handler = this.sitePluginsProvider.getSitePluginHandler(name); + + this.handlerSchema = handler && handler.handlerSchema; + + if (this.handlerSchema) { + // Load first template. + if (this.handlerSchema.methodTemplates && this.handlerSchema.methodTemplates.length) { + this.content = handler.handlerSchema.methodTemplates[0].html; + } + } + } +} diff --git a/src/core/siteplugins/classes/question-behaviour-handler.ts b/src/core/siteplugins/classes/question-behaviour-handler.ts new file mode 100644 index 000000000..bf4adc7be --- /dev/null +++ b/src/core/siteplugins/classes/question-behaviour-handler.ts @@ -0,0 +1,44 @@ +// (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 { Injector } from '@angular/core'; +import { CoreQuestionBehaviourDefaultHandler } from '@core/question/providers/default-behaviour-handler'; +import { CoreSitePluginsQuestionBehaviourComponent } from '../components/question-behaviour/question-behaviour'; +import { CoreQuestionProvider } from '@core/question/providers/question'; + +/** + * Handler to display a question behaviour site plugin. + */ +export class CoreSitePluginsQuestionBehaviourHandler extends CoreQuestionBehaviourDefaultHandler { + + constructor(questionProvider: CoreQuestionProvider, public name: string, public type: string, public hasTemplate: boolean) { + super(questionProvider); + } + + /** + * Handle a question behaviour. + * If the behaviour requires a submit button, it should add it to question.behaviourButtons. + * If the behaviour requires to show some extra data, it should return the components to render it. + * + * @param {Injector} injector Injector. + * @param {any} question The question. + * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question + * (e.g. certainty options). Don't return anything if no extra data is required. + */ + handleQuestion(injector: Injector, question: any): any[] | Promise { + if (this.hasTemplate) { + return [CoreSitePluginsQuestionBehaviourComponent]; + } + } +} diff --git a/src/core/siteplugins/components/components.module.ts b/src/core/siteplugins/components/components.module.ts index 4c809ca39..f6fe3250f 100644 --- a/src/core/siteplugins/components/components.module.ts +++ b/src/core/siteplugins/components/components.module.ts @@ -24,6 +24,7 @@ import { CoreSitePluginsCourseOptionComponent } from './course-option/course-opt import { CoreSitePluginsCourseFormatComponent } from './course-format/course-format'; import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/user-profile-field'; import { CoreSitePluginsQuestionComponent } from './question/question'; +import { CoreSitePluginsQuestionBehaviourComponent } from './question-behaviour/question-behaviour'; @NgModule({ declarations: [ @@ -32,7 +33,8 @@ import { CoreSitePluginsQuestionComponent } from './question/question'; CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, - CoreSitePluginsQuestionComponent + CoreSitePluginsQuestionComponent, + CoreSitePluginsQuestionBehaviourComponent ], imports: [ CommonModule, @@ -49,14 +51,16 @@ import { CoreSitePluginsQuestionComponent } from './question/question'; CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, - CoreSitePluginsQuestionComponent + CoreSitePluginsQuestionComponent, + CoreSitePluginsQuestionBehaviourComponent ], entryComponents: [ CoreSitePluginsModuleIndexComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, - CoreSitePluginsQuestionComponent + CoreSitePluginsQuestionComponent, + CoreSitePluginsQuestionBehaviourComponent ] }) export class CoreSitePluginsComponentsModule {} diff --git a/src/core/siteplugins/components/question-behaviour/question-behaviour.html b/src/core/siteplugins/components/question-behaviour/question-behaviour.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/siteplugins/components/question-behaviour/question-behaviour.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/question-behaviour/question-behaviour.ts b/src/core/siteplugins/components/question-behaviour/question-behaviour.ts new file mode 100644 index 000000000..1d4777cb7 --- /dev/null +++ b/src/core/siteplugins/components/question-behaviour/question-behaviour.ts @@ -0,0 +1,58 @@ +// (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, Input, Output, EventEmitter } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; + +/** + * Component that displays a question behaviour created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-question-behaviour', + templateUrl: 'question-behaviour.html', +}) +export class CoreSitePluginsQuestionBehaviourComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + @Input() question: any; // The question where the behaviour will be rendered. + @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; // Should emit an event when a behaviour button is clicked. + @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. + + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData = { + question: this.question, + component: this.component, + componentId: this.componentId, + attemptId: this.attemptId, + offlineEnabled: this.offlineEnabled, + buttonClicked: this.buttonClicked, + onAbort: this.onAbort + }; + + if (this.question) { + this.getHandlerData('qbehaviour_' + this.question.preferredBehaviour); + } + } +} diff --git a/src/core/siteplugins/components/question/question.html b/src/core/siteplugins/components/question/question.html index fec5e4726..f58bcd913 100644 --- a/src/core/siteplugins/components/question/question.html +++ b/src/core/siteplugins/components/question/question.html @@ -1 +1 @@ - + diff --git a/src/core/siteplugins/components/question/question.ts b/src/core/siteplugins/components/question/question.ts index f7e46cc52..ccb6fbaa1 100644 --- a/src/core/siteplugins/components/question/question.ts +++ b/src/core/siteplugins/components/question/question.ts @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, Output, EventEmitter, ViewChild, OnDestroy } from '@angular/core'; +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; -import { CoreCompileHtmlComponent } from '@core/compile/components/compile-html/compile-html'; -import { Subscription } from 'rxjs'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; /** * Component that displays a question created using a site plugin. @@ -24,7 +23,7 @@ import { Subscription } from 'rxjs'; selector: 'core-site-plugins-question', templateUrl: 'question.html', }) -export class CoreSitePluginsQuestionComponent implements OnInit, OnDestroy { +export class CoreSitePluginsQuestionComponent extends CoreSitePluginsCompileInitComponent 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. @@ -33,13 +32,9 @@ export class CoreSitePluginsQuestionComponent implements OnInit, OnDestroy { @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. - @ViewChild(CoreCompileHtmlComponent) compileComponent: CoreCompileHtmlComponent; - - content = ''; // Content. - jsData; - protected componentObserver: Subscription; - - constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } /** * Component being initialized. @@ -57,34 +52,7 @@ export class CoreSitePluginsQuestionComponent implements OnInit, OnDestroy { }; if (this.question) { - // Retrieve the handler data. - const handler = this.sitePluginsProvider.getSitePluginHandler('qtype_' + this.question.type), - handlerSchema = handler && handler.handlerSchema; - - if (handlerSchema) { - // Load first template. - if (handlerSchema.methodTemplates && handlerSchema.methodTemplates.length) { - this.content = handler.handlerSchema.methodTemplates[0].html; - } - - // Wait for the instance to be created. - if (this.compileComponent && this.compileComponent.componentObservable && - handlerSchema.methodJSResult && handlerSchema.methodJSResult.componentInit) { - this.componentObserver = this.compileComponent.componentObservable.subscribe((instance) => { - if (instance) { - // Instance created, call component init. - handlerSchema.methodJSResult.componentInit.apply(instance); - } - }); - } - } + this.getHandlerData('qtype_' + this.question.type); } } - - /** - * Component destroyed. - */ - ngOnDestroy(): void { - this.componentObserver && this.componentObserver.unsubscribe(); - } } diff --git a/src/core/siteplugins/components/user-profile-field/user-profile-field.html b/src/core/siteplugins/components/user-profile-field/user-profile-field.html index fec5e4726..f58bcd913 100644 --- a/src/core/siteplugins/components/user-profile-field/user-profile-field.html +++ b/src/core/siteplugins/components/user-profile-field/user-profile-field.html @@ -1 +1 @@ - + diff --git a/src/core/siteplugins/components/user-profile-field/user-profile-field.ts b/src/core/siteplugins/components/user-profile-field/user-profile-field.ts index 84030ba62..f5c2271d2 100644 --- a/src/core/siteplugins/components/user-profile-field/user-profile-field.ts +++ b/src/core/siteplugins/components/user-profile-field/user-profile-field.ts @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, ViewChild, OnDestroy } from '@angular/core'; +import { Component, OnInit, Input } from '@angular/core'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; -import { CoreCompileHtmlComponent } from '@core/compile/components/compile-html/compile-html'; -import { Subscription } from 'rxjs'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; /** * Component that displays a user profile field created using a site plugin. @@ -24,20 +23,16 @@ import { Subscription } from 'rxjs'; selector: 'core-site-plugins-user-profile-field', templateUrl: 'user-profile-field.html', }) -export class CoreSitePluginsUserProfileFieldComponent implements OnInit, OnDestroy { +export class CoreSitePluginsUserProfileFieldComponent extends CoreSitePluginsCompileInitComponent implements OnInit { @Input() field: any; // The profile field to be rendered. @Input() signup = false; // True if editing the field in signup. Defaults to false. @Input() edit = false; // True if editing the field. Defaults to false. @Input() form?: any; // Form where to add the form control. Required if edit=true or signup=true. @Input() registerAuth?: string; // Register auth method. E.g. 'email'. - @ViewChild(CoreCompileHtmlComponent) compileComponent: CoreCompileHtmlComponent; - - content = ''; // Content. - jsData; - protected componentObserver: Subscription; - - constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } /** * Component being initialized. @@ -54,34 +49,7 @@ export class CoreSitePluginsUserProfileFieldComponent implements OnInit, OnDestr }; if (this.field) { - // Retrieve the handler data. - const handler = this.sitePluginsProvider.getSitePluginHandler(this.field.type || this.field.datatype), - handlerSchema = handler && handler.handlerSchema; - - if (handlerSchema) { - // Load first template. - if (handlerSchema.methodTemplates && handlerSchema.methodTemplates.length) { - this.content = handler.handlerSchema.methodTemplates[0].html; - } - - // Wait for the instance to be created. - if (this.compileComponent && this.compileComponent.componentObservable && - handlerSchema.methodJSResult && handlerSchema.methodJSResult.componentInit) { - this.componentObserver = this.compileComponent.componentObservable.subscribe((instance) => { - if (instance) { - // Instance created, call component init. - handlerSchema.methodJSResult.componentInit.apply(instance); - } - }); - } - } + this.getHandlerData(this.field.type || this.field.datatype); } } - - /** - * Component destroyed. - */ - ngOnDestroy(): void { - this.componentObserver && this.componentObserver.unsubscribe(); - } } diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index cdd570dd9..65116a0e9 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -25,6 +25,7 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitePluginsProvider } from './siteplugins'; import { CoreCompileProvider } from '@core/compile/providers/compile'; +import { CoreQuestionProvider } from '@core/question/providers/question'; // Delegates import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; @@ -36,6 +37,7 @@ import { CoreUserDelegate } from '@core/user/providers/user-delegate'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate'; import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; // Handler classes. @@ -48,6 +50,7 @@ import { CoreSitePluginsUserProfileHandler } from '../classes/user-handler'; import { CoreSitePluginsUserProfileFieldHandler } from '../classes/user-profile-field-handler'; import { CoreSitePluginsSettingsHandler } from '../classes/settings-handler'; import { CoreSitePluginsQuestionHandler } from '../classes/question-handler'; +import { CoreSitePluginsQuestionBehaviourHandler } from '../classes/question-behaviour-handler'; import { CoreSitePluginsMessageOutputHandler } from '../classes/message-output-handler'; /** @@ -72,6 +75,7 @@ export class CoreSitePluginsHelperProvider { private courseFormatDelegate: CoreCourseFormatDelegate, private profileFieldDelegate: CoreUserProfileFieldDelegate, private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private settingsDelegate: CoreSettingsDelegate, private questionDelegate: CoreQuestionDelegate, + private questionBehaviourDelegate: CoreQuestionBehaviourDelegate, private questionProvider: CoreQuestionProvider, private messageOutputDelegate: AddonMessageOutputDelegate) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); @@ -450,6 +454,10 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerQuestionHandler(plugin, handlerName, handlerSchema, result)); break; + case 'CoreQuestionBehaviourDelegate': + promise = Promise.resolve(this.registerQuestionBehaviourHandler(plugin, handlerName, handlerSchema, result)); + break; + case 'AddonMessageOutputDelegate': promise = Promise.resolve(this.registerMessageOutputHandler(plugin, handlerName, handlerSchema, result)); break; @@ -666,6 +674,57 @@ export class CoreSitePluginsHelperProvider { }); } + /** + * Given a handler in an plugin, register it in the question behaviour delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of the init WS call. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerQuestionBehaviourHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) + : string | Promise { + if (!handlerSchema.method) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in question behaviour delegate:', plugin, handlerSchema, initResult); + + // Execute the main method and its JS. The template returned will be used in the question component. + return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + type = plugin.component.replace('qbehaviour_', ''), + behaviourHandler = new CoreSitePluginsQuestionBehaviourHandler(this.questionProvider, uniqueName, type, + result.templates.length); + + // Store in handlerSchema some data required by the component. + handlerSchema.methodTemplates = result.templates; + handlerSchema.methodJSResult = result.jsResult; + + if (result && result.jsResult) { + // Override default handler functions with the result of the method JS. + for (const property in behaviourHandler) { + if (property != 'constructor' && typeof behaviourHandler[property] == 'function' && + typeof result.jsResult[property] == 'function') { + behaviourHandler[property] = result.jsResult[property].bind(behaviourHandler); + } + } + } + + this.questionBehaviourDelegate.registerHandler(behaviourHandler); + + return plugin.component; + }).catch((err) => { + this.logger.error('Error executing main method for question', handlerSchema.method, err); + }); + } + /** * Given a handler in an plugin, register it in the settings delegate. *