MOBILE-2345 lesson: Implement player page
parent
8a2fdca74b
commit
ff06a812d2
|
@ -21,6 +21,7 @@ import { AddonModLessonComponentsModule } from './components/components.module';
|
|||
import { AddonModLessonProvider } from './providers/lesson';
|
||||
import { AddonModLessonOfflineProvider } from './providers/lesson-offline';
|
||||
import { AddonModLessonSyncProvider } from './providers/lesson-sync';
|
||||
import { AddonModLessonHelperProvider } from './providers/helper';
|
||||
import { AddonModLessonModuleHandler } from './providers/module-handler';
|
||||
import { AddonModLessonPrefetchHandler } from './providers/prefetch-handler';
|
||||
import { AddonModLessonSyncCronHandler } from './providers/sync-cron-handler';
|
||||
|
@ -38,6 +39,7 @@ import { AddonModLessonReportLinkHandler } from './providers/report-link-handler
|
|||
AddonModLessonProvider,
|
||||
AddonModLessonOfflineProvider,
|
||||
AddonModLessonSyncProvider,
|
||||
AddonModLessonHelperProvider,
|
||||
AddonModLessonModuleHandler,
|
||||
AddonModLessonPrefetchHandler,
|
||||
AddonModLessonSyncCronHandler,
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title>{{ pageInstance.lesson.name }}</ion-title>
|
||||
<ion-buttons end>
|
||||
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content class="addon-mod_lesson-menu-modal">
|
||||
<nav>
|
||||
<ion-list>
|
||||
<!-- Media file. -->
|
||||
<ng-container *ngIf="pageInstance.mediaFile">
|
||||
<ion-item-divider color="light"><h2>{{ 'addon.mod_lesson.linkedmedia' | translate }}</h2></ion-item-divider>
|
||||
<core-file [file]="pageInstance.mediaFile" [component]="pageInstance.component" [componentId]="pageInstance.lesson.coursemodule"></core-file>
|
||||
</ng-container>
|
||||
|
||||
<!-- Lesson menu. -->
|
||||
<ng-container *ngIf="pageInstance.displayMenu">
|
||||
<ion-item-divider color="light"><h2>{{ 'addon.mod_lesson.lessonmenu' | translate }}</h2></ion-item-divider>
|
||||
<ion-item text-center *ngIf="pageInstance.loadingMenu">
|
||||
<ion-spinner></ion-spinner>
|
||||
</ion-item>
|
||||
<div *ngIf="!pageInstance.loadingMenu">
|
||||
<ng-container *ngFor="let page of pageInstance.lessonPages">
|
||||
<a ion-item text-wrap *ngIf="page.display && page.displayinmenublock" (click)="loadPage(page.id)" [ngClass]='{"addon-mod_lesson-selected core-white-push-arrow": !pageInstance.eolData && pageInstance.currentPage == page.id}'>
|
||||
<p><core-format-text [text]="page.title"></core-format-text></p>
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</nav>
|
||||
</ion-content>
|
|
@ -0,0 +1,33 @@
|
|||
// (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 { IonicPageModule } from 'ionic-angular';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModLessonMenuModalPage } from './menu-modal';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModLessonMenuModalPage
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
IonicPageModule.forChild(AddonModLessonMenuModalPage),
|
||||
TranslateModule.forChild()
|
||||
]
|
||||
})
|
||||
export class AddonModLessonMenuModalPageModule {}
|
|
@ -0,0 +1,5 @@
|
|||
page-addon-mod-lesson-menu-modal {
|
||||
.addon-mod_lesson-selected, .item.addon-mod_lesson-selected {
|
||||
background: $blue-light;
|
||||
}
|
||||
}
|
|
@ -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 } from '@angular/core';
|
||||
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
|
||||
|
||||
/**
|
||||
* Modal that renders the lesson menu and media file.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-lesson-menu-modal' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-lesson-menu-modal',
|
||||
templateUrl: 'menu-modal.html',
|
||||
})
|
||||
export class AddonModLessonMenuModalPage {
|
||||
|
||||
/**
|
||||
* The instance of the page that opened the modal. We use the instance instead of the needed attributes for these reasons:
|
||||
* - We want the user to be able to see the media file while the menu is being loaded, so we need to be able to update
|
||||
* the menu dynamically based on the data retrieved by the page that opened the modal.
|
||||
* - The onDidDismiss function takes a while to be called, making the app seem slow. This way we can directly call
|
||||
* the functions we need without having to wait for the modal to be dismissed.
|
||||
* @type {any}
|
||||
*/
|
||||
pageInstance: any;
|
||||
|
||||
constructor(params: NavParams, protected viewCtrl: ViewController) {
|
||||
this.pageInstance = params.get('page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
this.viewCtrl.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a certain page.
|
||||
*
|
||||
* @param {number} pageId The page ID to load.
|
||||
*/
|
||||
loadPage(pageId: number): void {
|
||||
this.pageInstance.changePage && this.pageInstance.changePage(pageId);
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<button *ngIf="displayMenu || mediaFile" ion-button icon-only [attr.aria-label]="'addon.mod_lesson.lessonmenu' | translate" (click)="menuModal.present()">
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<!-- Info messages. Only show the first one. -->
|
||||
<div class="core-info-card" icon-start *ngIf="lesson && messages && messages.length">
|
||||
<ion-icon name="information"></ion-icon>
|
||||
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="messages[0].message"></core-format-text>
|
||||
</div>
|
||||
|
||||
<div *ngIf="lesson" [ngClass]='{"addon-mod_lesson-slideshow": lesson.slideshow}' [ngStyle]="{'width': lessonWidth, 'height': lessonHeight}">
|
||||
|
||||
<core-timer *ngIf="endTime" [endTime]="endTime" (finished)="timeUp()" [timerText]="'addon.mod_lesson.timeremaining' | translate"></core-timer>
|
||||
|
||||
<!-- Retake and ongoing score. -->
|
||||
<ion-item text-wrap *ngIf="showRetake && !eolData && !processData">
|
||||
<p>{{ 'addon.mod_lesson.attempt' | translate:{$a: retake} }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="pageData && pageData.ongoingscore && !eolData && !processData" class="addon-mod_lesson-ongoingscore">
|
||||
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageData.ongoingscore"></core-format-text>
|
||||
</ion-item>
|
||||
|
||||
<!-- Page content. -->
|
||||
<ion-card *ngIf="!eolData && !processData">
|
||||
<!-- Content page. -->
|
||||
<ion-item text-wrap *ngIf="!question">
|
||||
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageContent"></core-format-text>
|
||||
</ion-item>
|
||||
|
||||
<!-- Question page. -->
|
||||
<form *ngIf="question" ion-list [formGroup]="questionForm">
|
||||
<ion-item-divider text-wrap color="light">
|
||||
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageContent"></core-format-text>
|
||||
</ion-item-divider>
|
||||
|
||||
<input *ngFor="let input of question.hiddenInputs" type="hidden" [name]="input.name" [value]="input.value" />
|
||||
|
||||
<!-- Render a different input depending on the type of the question. -->
|
||||
<ng-container [ngSwitch]="question.template">
|
||||
|
||||
<!-- Short answer. -->
|
||||
<ng-container *ngSwitchCase="'shortanswer'">
|
||||
<ion-input padding-left [type]="question.input.type" placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}" [id]="question.input.id" [formControlName]="question.input.name" autocorrect="off" [maxlength]="question.input.maxlength">
|
||||
</ion-input>
|
||||
</ng-container>
|
||||
|
||||
<!-- Essay. -->
|
||||
<ng-container *ngSwitchCase="'essay'">
|
||||
<ion-item *ngIf="question.textarea">
|
||||
<core-rich-text-editor item-content placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}" [control]="question.control"></core-rich-text-editor>
|
||||
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
|
||||
[component]="component" [componentId]="lesson.coursemodule" -->
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="!question.textarea && question.useranswer">
|
||||
<p class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</p>
|
||||
<p><core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="question.useranswer"></core-format-text></p>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- Multichoice. -->
|
||||
<ng-container *ngSwitchCase="'multichoice'">
|
||||
<!-- Single choice. -->
|
||||
<div *ngIf="!question.multi" radio-group [formControlName]="question.controlName">
|
||||
<ion-item text-wrap *ngFor="let option of question.options">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="option.text"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-radio [id]="option.id" [value]="option.value" [disabled]="option.disabled"></ion-radio>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<!-- Multiple choice. -->
|
||||
<ng-container *ngIf="question.multi">
|
||||
<ion-item text-wrap *ngFor="let option of question.options">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="option.text"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-checkbox [id]="option.id" [formControlName]="option.name" item-end></ion-checkbox>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Matching. -->
|
||||
<ng-container *ngSwitchCase="'matching'">
|
||||
<ion-item text-wrap *ngFor="let row of question.rows">
|
||||
<ion-grid item-content>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<p><core-format-text id="addon-mod_lesson-matching-{{row.id}}" [component]="component" [componentId]="lesson.coursemodule" [text]="row.text"></core-format-text></p>
|
||||
</ion-col>
|
||||
<ion-col>
|
||||
<ion-select [id]="row.id" [formControlName]="row.name" [attr.aria-labelledby]="'addon-mod_lesson-matching-' + row.id" interface="popover">
|
||||
<ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option>
|
||||
</ion-select>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ion-item>
|
||||
<button ion-button block (click)="submitQuestion()">{{ question.submitLabel }}</button>
|
||||
</ion-item>
|
||||
</form>
|
||||
</ion-card>
|
||||
|
||||
<!-- Page buttons and progress. -->
|
||||
<ion-list *ngIf="!eolData && !processData">
|
||||
<ion-item text-wrap *ngIf="pageButtons && pageButtons.length">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let button of pageButtons">
|
||||
<a ion-button block color="light" text-wrap [id]="button.id" (click)="buttonClicked(button.data)">{{ button.content }}</a>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="lesson && lesson.progressbar && !canManage && pageData">
|
||||
<p>{{ 'addon.mod_lesson.progresscompleted' | translate:{$a: pageData.progress} }}</p>
|
||||
<core-progress-bar [progress]="pageData.progress"></core-progress-bar>
|
||||
</ion-item>
|
||||
<div class="core-info-card" icon-start *ngIf="lesson && lesson.progressbar && canManage">
|
||||
<ion-icon name="information"></ion-icon>
|
||||
{{ 'addon.mod_lesson.progressbarteacherwarning2' | translate }}
|
||||
</div>
|
||||
</ion-list>
|
||||
|
||||
<!-- End of lesson reached. -->
|
||||
<ion-card *ngIf="eolData && !processData">
|
||||
<div class="core-warning-card" icon-start *ngIf="eolData.offline && eolData.offline.value">
|
||||
<ion-icon name="warning"></ion-icon>
|
||||
{{ 'addon.mod_lesson.finishretakeoffline' | translate }}
|
||||
</div>
|
||||
|
||||
<h3 padding *ngIf="eolData.gradelesson">{{ 'addon.mod_lesson.congratulations' | translate }}</h3>
|
||||
<ion-item text-wrap *ngIf="eolData.notenoughtimespent">
|
||||
<core-format-text [text]="eolData.notenoughtimespent.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.numberofpagesviewed">
|
||||
<core-format-text [text]="eolData.numberofpagesviewed.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.youshouldview">
|
||||
<core-format-text [text]="eolData.youshouldview.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.numberofcorrectanswers">
|
||||
<core-format-text [text]="eolData.numberofcorrectanswers.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.displayscorewithessays">
|
||||
<core-format-text [text]="eolData.displayscorewithessays.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="!eolData.displayscorewithessays && eolData.displayscorewithoutessays">
|
||||
<core-format-text [text]="eolData.displayscorewithoutessays.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.yourcurrentgradeisoutof">
|
||||
<core-format-text [text]="eolData.yourcurrentgradeisoutof.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.eolstudentoutoftimenoanswers">
|
||||
<core-format-text [text]="eolData.eolstudentoutoftimenoanswers.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.welldone">
|
||||
<core-format-text [text]="eolData.welldone.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="lesson.progressbar && eolData.progresscompleted">
|
||||
<p>{{ 'addon.mod_lesson.progresscompleted' | translate:{$a: eolData.progresscompleted.value} }}</p>
|
||||
<core-progress-bar [progress]="eolData.progresscompleted.value"></core-progress-bar>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.displayofgrade">
|
||||
<core-format-text [text]="eolData.displayofgrade.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.reviewlesson">
|
||||
<a ion-button block (click)="reviewLesson(eolData.reviewlesson.pageid)">
|
||||
<core-format-text [text]="'addon.mod_lesson.reviewlesson' | translate"></core-format-text>
|
||||
</a>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.modattemptsnoteacher">
|
||||
<core-format-text [text]="eolData.modattemptsnoteacher.message"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="eolData.activitylink && eolData.activitylink.value">
|
||||
<ng-container *ngIf="eolData.activitylink.value.formatted">
|
||||
<!-- Activity link was successfully formatted, render the button. -->
|
||||
<a ion-button block color="light" [href]="eolData.activitylink.value.href" core-link [capture]="true">
|
||||
<core-format-text [text]="eolData.activitylink.value.label"></core-format-text>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!eolData.activitylink.value.formatted">
|
||||
<!-- Activity link wasn't formatted, render the original link. -->
|
||||
<core-format-text [text]="eolData.activitylink.value.label"></core-format-text>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- Feedback returned when processing an action. -->
|
||||
<ion-list *ngIf="processData">
|
||||
<ion-item text-wrap *ngIf="processData.ongoingscore && !processData.reviewmode" >
|
||||
<core-format-text class="addon-mod_lesson-ongoingscore" [text]="processData.ongoingscore"></core-format-text>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="!processData.reviewmode || review">
|
||||
<p *ngIf="!processData.reviewmode">
|
||||
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="processData.feedback"></core-format-text>
|
||||
</p>
|
||||
<div *ngIf="review">
|
||||
<p>{{ 'addon.mod_lesson.gotoendoflesson' | translate }}</p>
|
||||
<p>{{ 'addon.mod_lesson.or' | translate }}</p>
|
||||
<p>{{ 'addon.mod_lesson.continuetonextpage' | translate }}</p>
|
||||
</div>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="review || (processData.buttons && processData.buttons.length)">
|
||||
<a ion-button block color="light" *ngIf="review" (click)="changePage(LESSON_EOL)">{{ 'addon.mod_lesson.finish' | translate }}</a>
|
||||
<a ion-button block color="light" *ngFor="let button of processData.buttons" (click)="changePage(button.pageId, true)">{{ button.label | translate }}</a>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,33 @@
|
|||
// (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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModLessonPlayerPage } from './player';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModLessonPlayerPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
IonicPageModule.forChild(AddonModLessonPlayerPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModLessonPlayerPageModule {}
|
|
@ -0,0 +1,11 @@
|
|||
page-addon-mod-lesson-player {
|
||||
.addon-mod_lesson-slideshow {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
ion-input[padding-left] input[padding-left] {
|
||||
// Applying padding-left to the ion-input applies it twice since it's replicated in the inner input.
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,635 @@
|
|||
// (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, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { AddonModLessonProvider } from '../../providers/lesson';
|
||||
import { AddonModLessonOfflineProvider } from '../../providers/lesson-offline';
|
||||
import { AddonModLessonSyncProvider } from '../../providers/lesson-sync';
|
||||
import { AddonModLessonHelperProvider } from '../../providers/helper';
|
||||
|
||||
/**
|
||||
* Page that allows attempting and reviewing a lesson.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-lesson-player' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-lesson-player',
|
||||
templateUrl: 'player.html',
|
||||
})
|
||||
export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
|
||||
@ViewChild(Content) content: Content;
|
||||
|
||||
component = AddonModLessonProvider.COMPONENT;
|
||||
LESSON_EOL = AddonModLessonProvider.LESSON_EOL;
|
||||
questionForm: FormGroup; // The FormGroup for question pages.
|
||||
title: string; // The page title.
|
||||
lesson: any; // The lesson object.
|
||||
currentPage: number; // Current page being viewed.
|
||||
review: boolean; // Whether the user is reviewing.
|
||||
messages: any[]; // Messages to display to the user.
|
||||
menuModal: Modal; // Modal to navigate through the pages.
|
||||
canManage: boolean; // Whether the user can manage the lesson.
|
||||
retake: number; // Current retake number.
|
||||
showRetake: boolean; // Whether the retake number needs to be displayed.
|
||||
lessonWidth: string; // Width of the lesson (if slideshow mode).
|
||||
lessonHeight: string; // Height of the lesson (if slideshow mode).
|
||||
endTime: number; // End time of the lesson if it's timed.
|
||||
pageData: any; // Current page data.
|
||||
pageContent: string; // Current page contents.
|
||||
pageButtons: any[]; // List of buttons of the current page.
|
||||
question: any; // Question of the current page (if it's a question page).
|
||||
eolData: any; // Data for EOL page (if current page is EOL).
|
||||
processData: any; // Data to display after processing a page.
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
displayMenu: boolean; // Whether the lesson menu should be displayed.
|
||||
originalData: any; // Original question data. It is used to check if data has changed.
|
||||
|
||||
protected courseId: number; // The course ID the lesson belongs to.
|
||||
protected lessonId: number; // Lesson ID.
|
||||
protected password: string; // Lesson password (if any).
|
||||
protected forceLeave = false; // If true, don't perform any check when leaving the view.
|
||||
protected offline: boolean; // Whether we are in offline mode.
|
||||
protected accessInfo: any; // Lesson access info.
|
||||
protected jumps: any; // All possible jumps.
|
||||
protected mediaFile: any; // Media file of the lesson.
|
||||
protected firstPageLoaded: boolean; // Whether the first page has been loaded.
|
||||
protected loadingMenu: boolean; // Whether the lesson menu is being loaded.
|
||||
protected lessonPages: any[]; // Lesson pages (for the lesson menu).
|
||||
|
||||
constructor(protected navParams: NavParams, logger: CoreLoggerProvider, protected translate: TranslateService,
|
||||
protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider,
|
||||
protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController,
|
||||
protected timeUtils: CoreTimeUtilsProvider, protected lessonProvider: AddonModLessonProvider,
|
||||
protected lessonHelper: AddonModLessonHelperProvider, protected lessonSync: AddonModLessonSyncProvider,
|
||||
protected lessonOfflineProvider: AddonModLessonOfflineProvider, protected cdr: ChangeDetectorRef,
|
||||
modalCtrl: ModalController, protected navCtrl: NavController, protected appProvider: CoreAppProvider,
|
||||
protected utils: CoreUtilsProvider, protected urlUtils: CoreUrlUtilsProvider, protected fb: FormBuilder) {
|
||||
|
||||
this.lessonId = navParams.get('lessonId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.password = navParams.get('password');
|
||||
this.review = !!navParams.get('review');
|
||||
this.currentPage = navParams.get('pageId');
|
||||
|
||||
// Block the lesson so it cannot be synced.
|
||||
this.syncProvider.blockOperation(this.component, this.lessonId);
|
||||
|
||||
// Create the navigation modal.
|
||||
this.menuModal = modalCtrl.create('AddonModLessonMenuModalPage', {
|
||||
page: this
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Fetch the Lesson data.
|
||||
this.fetchLessonData().then((success) => {
|
||||
if (success) {
|
||||
// Review data loaded or new retake started, remove any retake being finished in sync.
|
||||
this.lessonSync.deleteRetakeFinishedInSync(this.lessonId);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unblock the lesson so it can be synced.
|
||||
this.syncProvider.unblockOperation(this.component, this.lessonId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
ionViewCanLeave(): boolean | Promise<void> {
|
||||
if (this.forceLeave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.question && !this.eolData && !this.processData && this.originalData) {
|
||||
// Question shown. Check if there is any change.
|
||||
if (!this.utils.basicLeftCompare(this.questionForm.getRawValue(), this.originalData, 3)) {
|
||||
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* A button was clicked.
|
||||
*
|
||||
* @param {any} data Button data.
|
||||
*/
|
||||
buttonClicked(data: any): void {
|
||||
this.processPage(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a function and go offline if allowed and the call fails.
|
||||
*
|
||||
* @param {Function} func Function to call.
|
||||
* @param {any[]} args Arguments to pass to the function.
|
||||
* @param {number} offlineParamPos Position of the offline parameter in the args.
|
||||
* @param {number} [jumpsParamPos] Position of the jumps parameter in the args.
|
||||
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
|
||||
*/
|
||||
protected callFunction(func: Function, args: any[], offlineParamPos: number, jumpsParamPos?: number): Promise<any> {
|
||||
return func.apply(func, args).catch((error) => {
|
||||
if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson) &&
|
||||
!this.utils.isWebServiceError(error)) {
|
||||
// If it fails, go offline.
|
||||
this.offline = true;
|
||||
|
||||
// Get the possible jumps now.
|
||||
return this.lessonProvider.getPagesPossibleJumps(this.lesson.id, true).then((jumpList) => {
|
||||
this.jumps = jumpList;
|
||||
|
||||
// Call the function again with offline set to true and the new jumps.
|
||||
args[offlineParamPos] = true;
|
||||
if (typeof jumpsParamPos != 'undefined') {
|
||||
args[jumpsParamPos] = this.jumps;
|
||||
}
|
||||
|
||||
return func.apply(func, args);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page from menu or when continuing from a feedback page.
|
||||
*
|
||||
* @param {number} pageId Page to load.
|
||||
* @param {boolean} [ignoreCurrent] If true, allow loading current page.
|
||||
*/
|
||||
changePage(pageId: number, ignoreCurrent?: boolean): void {
|
||||
if (!ignoreCurrent && !this.eolData && this.currentPage == pageId) {
|
||||
// Page already loaded, stop.
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.messages = [];
|
||||
|
||||
this.loadPage(pageId).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error loading page');
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lesson data and load the page.
|
||||
*
|
||||
* @return {Promise<boolean>} Promise resolved with true if success, resolved with false otherwise.
|
||||
*/
|
||||
protected fetchLessonData(): Promise<boolean> {
|
||||
// Wait for any ongoing sync to finish. We won't sync a lesson while it's being played.
|
||||
return this.lessonSync.waitForSync(this.lessonId).then(() => {
|
||||
return this.lessonProvider.getLessonById(this.courseId, this.lessonId);
|
||||
}).then((lessonData) => {
|
||||
this.lesson = lessonData;
|
||||
this.title = this.lesson.name; // Temporary title.
|
||||
|
||||
// If lesson has offline data already, use offline mode.
|
||||
return this.lessonOfflineProvider.hasOfflineData(this.lessonId);
|
||||
}).then((offlineMode) => {
|
||||
this.offline = offlineMode;
|
||||
|
||||
if (!offlineMode && !this.appProvider.isOnline() && this.lessonProvider.isLessonOffline(this.lesson) && !this.review) {
|
||||
// Lesson doesn't have offline data, but it allows offline and the device is offline. Use offline mode.
|
||||
this.offline = true;
|
||||
}
|
||||
|
||||
return this.callFunction(this.lessonProvider.getAccessInformation.bind(this.lessonProvider),
|
||||
[this.lesson.id, this.offline, true], 1);
|
||||
}).then((info) => {
|
||||
const promises = [];
|
||||
|
||||
this.accessInfo = info;
|
||||
this.canManage = info.canmanage;
|
||||
this.retake = info.attemptscount;
|
||||
this.showRetake = !this.currentPage && this.retake > 0; // Only show it in first page if it isn't the first retake.
|
||||
|
||||
if (info.preventaccessreasons && info.preventaccessreasons.length) {
|
||||
// If it's a password protected lesson and we have the password, allow playing it.
|
||||
if (!this.password || info.preventaccessreasons.length > 1 || !this.lessonProvider.isPasswordProtected(info)) {
|
||||
// Lesson cannot be played, show message and go back.
|
||||
return Promise.reject(info.preventaccessreasons[0].message);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.review && this.navParams.get('retake') != info.attemptscount - 1) {
|
||||
// Reviewing a retake that isn't the last one. Error.
|
||||
return Promise.reject(this.translate.instant('addon.mod_lesson.errorreviewretakenotlast'));
|
||||
}
|
||||
|
||||
if (this.password) {
|
||||
// Lesson uses password, get the whole lesson object.
|
||||
promises.push(this.callFunction(this.lessonProvider.getLessonWithPassword.bind(this.lessonProvider),
|
||||
[this.lesson.id, this.password, true, this.offline, true], 3).then((lesson) => {
|
||||
this.lesson = lesson;
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.offline) {
|
||||
// Offline mode, get the list of possible jumps to allow navigation.
|
||||
promises.push(this.lessonProvider.getPagesPossibleJumps(this.lesson.id, true).then((jumpList) => {
|
||||
this.jumps = jumpList;
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
this.mediaFile = this.lesson.mediafiles && this.lesson.mediafiles[0];
|
||||
|
||||
this.lessonWidth = this.lesson.slideshow ? this.domUtils.formatPixelsSize(this.lesson.mediawidth) : '';
|
||||
this.lessonHeight = this.lesson.slideshow ? this.domUtils.formatPixelsSize(this.lesson.mediaheight) : '';
|
||||
|
||||
return this.launchRetake(this.currentPage);
|
||||
}).then(() => {
|
||||
return true;
|
||||
}).catch((error) => {
|
||||
// An error occurred.
|
||||
let promise;
|
||||
|
||||
if (this.review && this.navParams.get('retake') && this.utils.isWebServiceError(error)) {
|
||||
// The user cannot review the retake. Unmark the retake as being finished in sync.
|
||||
promise = this.lessonSync.deleteRetakeFinishedInSync(this.lessonId);
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
this.forceLeave = true;
|
||||
this.navCtrl.pop();
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the retake.
|
||||
*
|
||||
* @param {boolean} [outOfTime] Whether the retake is finished because the user ran out of time.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected finishRetake(outOfTime?: boolean): Promise<any> {
|
||||
let promise;
|
||||
|
||||
this.messages = [];
|
||||
|
||||
if (this.offline && this.appProvider.isOnline()) {
|
||||
// Offline mode but the app is online. Try to sync the data.
|
||||
promise = this.lessonSync.syncLesson(this.lesson.id, true, true).then((result) => {
|
||||
if (result.warnings && result.warnings.length) {
|
||||
const error = result.warnings[0];
|
||||
|
||||
// Some data was deleted. Check if the retake has changed.
|
||||
return this.lessonProvider.getAccessInformation(this.lesson.id).then((info) => {
|
||||
if (info.attemptscount != this.accessInfo.attemptscount) {
|
||||
// The retake has changed. Leave the view and show the error.
|
||||
this.forceLeave = true;
|
||||
this.navCtrl.pop();
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Retake hasn't changed, show the warning and finish the retake in offline.
|
||||
this.offline = false;
|
||||
this.domUtils.showErrorModal(error);
|
||||
});
|
||||
}
|
||||
|
||||
this.offline = false;
|
||||
}, () => {
|
||||
// Ignore errors.
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
// Now finish the retake.
|
||||
const args = [this.lesson, this.courseId, this.password, outOfTime, this.review, this.offline, this.accessInfo];
|
||||
|
||||
return this.callFunction(this.lessonProvider.finishRetake.bind(this.lessonProvider), args, 5);
|
||||
}).then((data) => {
|
||||
this.title = this.lesson.name;
|
||||
this.eolData = data.data;
|
||||
this.messages = this.messages.concat(data.messages);
|
||||
this.processData = undefined;
|
||||
|
||||
// Format activity link if present.
|
||||
if (this.eolData && this.eolData.activitylink) {
|
||||
this.eolData.activitylink.value = this.lessonHelper.formatActivityLink(this.eolData.activitylink.value);
|
||||
}
|
||||
|
||||
// Format review lesson if present.
|
||||
if (this.eolData && this.eolData.reviewlesson) {
|
||||
const params = this.urlUtils.extractUrlParams(this.eolData.reviewlesson.value);
|
||||
|
||||
if (!params || !params.pageid) {
|
||||
// No pageid in the URL, the user cannot review (probably didn't answer any question).
|
||||
delete this.eolData.reviewlesson;
|
||||
} else {
|
||||
this.eolData.reviewlesson.pageid = params.pageid;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Jump to a certain page after performing an action.
|
||||
*
|
||||
* @param {number} pageId The page to load.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected jumpToPage(pageId: number): Promise<any> {
|
||||
if (pageId === 0) {
|
||||
// Not a valid page, return to entry view.
|
||||
// This happens, for example, when the user clicks to go to previous page and there is no previous page.
|
||||
this.forceLeave = true;
|
||||
this.navCtrl.pop();
|
||||
|
||||
return Promise.resolve();
|
||||
} else if (pageId == AddonModLessonProvider.LESSON_EOL) {
|
||||
// End of lesson reached.
|
||||
return this.finishRetake();
|
||||
}
|
||||
|
||||
// Load new page.
|
||||
this.messages = [];
|
||||
|
||||
return this.loadPage(pageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start or continue a retake.
|
||||
*
|
||||
* @param {number} pageId The page to load.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected launchRetake(pageId: number): Promise<any> {
|
||||
let promise;
|
||||
|
||||
if (this.review) {
|
||||
// Review mode, no need to launch the retake.
|
||||
promise = Promise.resolve({});
|
||||
} else if (!this.offline) {
|
||||
// Not in offline mode, launch the retake.
|
||||
promise = this.lessonProvider.launchRetake(this.lesson.id, this.password, pageId);
|
||||
} else {
|
||||
// Check if there is a finished offline retake.
|
||||
promise = this.lessonOfflineProvider.hasFinishedRetake(this.lesson.id).then((finished) => {
|
||||
if (finished) {
|
||||
// Always show EOL page.
|
||||
pageId = AddonModLessonProvider.LESSON_EOL;
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then((data) => {
|
||||
this.currentPage = pageId || this.accessInfo.firstpageid;
|
||||
this.messages = data.messages || [];
|
||||
|
||||
if (this.lesson.timelimit && !this.accessInfo.canmanage) {
|
||||
// Get the last lesson timer.
|
||||
return this.lessonProvider.getTimers(this.lesson.id, false, true).then((timers) => {
|
||||
this.endTime = timers[timers.length - 1].starttime + this.lesson.timelimit;
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
return this.loadPage(this.currentPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the lesson menu.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected loadMenu(): Promise<any> {
|
||||
if (this.loadingMenu) {
|
||||
// Already loading.
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingMenu = true;
|
||||
|
||||
const args = [this.lessonId, this.password, this.offline, true];
|
||||
|
||||
return this.callFunction(this.lessonProvider.getPages.bind(this.lessonProvider), args, 2).then((pages) => {
|
||||
this.lessonPages = pages.map((entry) => {
|
||||
return entry.page;
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error loading menu.');
|
||||
}).finally(() => {
|
||||
this.loadingMenu = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a certain page.
|
||||
*
|
||||
* @param {number} pageId The page to load.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected loadPage(pageId: number): Promise<any> {
|
||||
if (pageId == AddonModLessonProvider.LESSON_EOL) {
|
||||
// End of lesson reached.
|
||||
return this.finishRetake();
|
||||
}
|
||||
|
||||
const args = [this.lesson, pageId, this.password, this.review, true, this.offline, true, this.accessInfo, this.jumps];
|
||||
|
||||
return this.callFunction(this.lessonProvider.getPageData.bind(this.lessonProvider), args, 5, 8).then((data) => {
|
||||
if (data.newpageid == AddonModLessonProvider.LESSON_EOL) {
|
||||
// End of lesson reached.
|
||||
return this.finishRetake();
|
||||
}
|
||||
|
||||
this.pageData = data;
|
||||
this.title = data.page.title;
|
||||
this.pageContent = this.lessonHelper.getPageContentsFromPageData(data);
|
||||
this.loaded = true;
|
||||
this.currentPage = pageId;
|
||||
this.messages = this.messages.concat(data.messages);
|
||||
|
||||
// Page loaded, hide EOL and feedback data if shown.
|
||||
this.eolData = this.processData = undefined;
|
||||
|
||||
if (this.lessonProvider.isQuestionPage(data.page.type)) {
|
||||
// Create an empty FormGroup without controls, they will be added in getQuestionFromPageData.
|
||||
this.questionForm = this.fb.group({});
|
||||
this.pageButtons = [];
|
||||
this.question = this.lessonHelper.getQuestionFromPageData(this.questionForm, data);
|
||||
this.originalData = this.questionForm.getRawValue(); // Use getRawValue to include disabled values.
|
||||
} else {
|
||||
this.pageButtons = this.lessonHelper.getPageButtonsFromHtml(data.pagecontent);
|
||||
this.question = undefined;
|
||||
this.originalData = undefined;
|
||||
}
|
||||
|
||||
if (data.displaymenu && !this.displayMenu) {
|
||||
// Load the menu.
|
||||
this.loadMenu();
|
||||
}
|
||||
this.displayMenu = !!data.displaymenu;
|
||||
|
||||
if (!this.firstPageLoaded) {
|
||||
this.firstPageLoaded = true;
|
||||
} else {
|
||||
this.showRetake = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a page, sending some data.
|
||||
*
|
||||
* @param {any} data The data to send.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected processPage(data: any): Promise<any> {
|
||||
this.loaded = false;
|
||||
|
||||
const args = [this.lesson, this.courseId, this.pageData, data, this.password, this.review, this.offline, this.accessInfo,
|
||||
this.jumps];
|
||||
|
||||
return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, 6, 8).then((result) => {
|
||||
if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson)) {
|
||||
// Lesson allows offline and the user changed some data in server. Update cached data.
|
||||
const retake = this.accessInfo.attemptscount;
|
||||
|
||||
if (this.lessonProvider.isQuestionPage(this.pageData.page.type)) {
|
||||
this.lessonProvider.getQuestionsAttemptsOnline(this.lessonId, retake, false, undefined, false, true);
|
||||
} else {
|
||||
this.lessonProvider.getContentPagesViewedOnline(this.lessonId, retake, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.nodefaultresponse || result.inmediatejump) {
|
||||
// Don't display feedback or force a redirect to a new page. Load the new page.
|
||||
return this.jumpToPage(result.newpageid);
|
||||
} else {
|
||||
|
||||
// Not inmediate jump, show the feedback.
|
||||
result.feedback = this.lessonHelper.removeQuestionFromFeedback(result.feedback);
|
||||
this.messages = result.messages;
|
||||
this.processData = result;
|
||||
this.processData.buttons = [];
|
||||
|
||||
if (this.lesson.review && !result.correctanswer && !result.noanswer && !result.isessayquestion &&
|
||||
!result.maxattemptsreached && !result.reviewmode) {
|
||||
// User can try again, show button to do so.
|
||||
this.processData.buttons.push({
|
||||
label: 'addon.mod_lesson.reviewquestionback',
|
||||
pageId: this.currentPage
|
||||
});
|
||||
}
|
||||
|
||||
// Button to continue.
|
||||
if (this.lesson.review && !result.correctanswer && !result.noanswer && !result.isessayquestion &&
|
||||
!result.maxattemptsreached) {
|
||||
this.processData.buttons.push({
|
||||
label: 'addon.mod_lesson.reviewquestioncontinue',
|
||||
pageId: result.newpageid
|
||||
});
|
||||
} else {
|
||||
this.processData.buttons.push({
|
||||
label: 'addon.mod_lesson.continue',
|
||||
pageId: result.newpageid
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error processing page');
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Review the lesson.
|
||||
*
|
||||
* @param {number} pageId Page to load.
|
||||
*/
|
||||
reviewLesson(pageId: number): void {
|
||||
this.loaded = false;
|
||||
this.review = true;
|
||||
this.offline = false; // Don't allow offline mode in review.
|
||||
|
||||
this.loadPage(pageId).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error loading page');
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a question.
|
||||
*/
|
||||
submitQuestion(): void {
|
||||
this.loaded = false;
|
||||
|
||||
// Use getRawValue to include disabled values.
|
||||
this.lessonHelper.prepareQuestionData(this.question, this.questionForm.getRawValue()).then((data) => {
|
||||
return this.processPage(data);
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Time up.
|
||||
*/
|
||||
timeUp(): void {
|
||||
// Time up called, hide the timer.
|
||||
this.endTime = undefined;
|
||||
this.loaded = false;
|
||||
|
||||
this.finishRetake(true).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error finishing attempt');
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,368 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { AddonModLessonProvider } from './lesson';
|
||||
|
||||
/**
|
||||
* Helper service that provides some features for quiz.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModLessonHelperProvider {
|
||||
|
||||
protected div = document.createElement('div'); // A div element to search in HTML code.
|
||||
|
||||
constructor(private domUtils: CoreDomUtilsProvider, private fb: FormBuilder, private translate: TranslateService,
|
||||
private textUtils: CoreTextUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* Given the HTML of next activity link, format it to extract the href and the text.
|
||||
*
|
||||
* @param {string} activityLink HTML of the activity link.
|
||||
* @return {{formatted: boolean, label: string, href: string}} Formatted data.
|
||||
*/
|
||||
formatActivityLink(activityLink: string): {formatted: boolean, label: string, href: string} {
|
||||
this.div.innerHTML = activityLink;
|
||||
const anchor = this.div.querySelector('a');
|
||||
if (!anchor) {
|
||||
// Anchor not found, return the original HTML.
|
||||
return {
|
||||
formatted: false,
|
||||
label: activityLink,
|
||||
href: ''
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
formatted: true,
|
||||
label: anchor.innerHTML,
|
||||
href: anchor.href
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the buttons to change pages.
|
||||
*
|
||||
* @param {string} html Page's HTML.
|
||||
* @return {any[]} List of buttons.
|
||||
*/
|
||||
getPageButtonsFromHtml(html: string): any[] {
|
||||
const buttons = [];
|
||||
|
||||
// Get the container of the buttons if it exists.
|
||||
this.div.innerHTML = html;
|
||||
let buttonsContainer = this.div.querySelector('.branchbuttoncontainer');
|
||||
|
||||
if (!buttonsContainer) {
|
||||
// Button container not found, might be a legacy lesson (from 1.9).
|
||||
if (!this.div.querySelector('form input[type="submit"]')) {
|
||||
// No buttons found.
|
||||
return buttons;
|
||||
}
|
||||
buttonsContainer = this.div;
|
||||
}
|
||||
|
||||
const forms = Array.from(buttonsContainer.querySelectorAll('form'));
|
||||
forms.forEach((form) => {
|
||||
const buttonSelector = 'input[type="submit"], button[type="submit"]',
|
||||
buttonEl = <HTMLInputElement | HTMLButtonElement> form.querySelector(buttonSelector),
|
||||
inputs = Array.from(form.querySelectorAll('input'));
|
||||
|
||||
if (!buttonEl || !inputs || !inputs.length) {
|
||||
// Button not found or no inputs, ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
const button = {
|
||||
id: buttonEl.id,
|
||||
title: buttonEl.title || buttonEl.value,
|
||||
content: buttonEl.tagName == 'INPUT' ? buttonEl.value : buttonEl.innerHTML.trim(),
|
||||
data: {}
|
||||
};
|
||||
|
||||
inputs.forEach((input) => {
|
||||
if (input.type != 'submit') {
|
||||
button.data[input.name] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
buttons.push(button);
|
||||
});
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a page data (result of getPageData), get the page contents.
|
||||
*
|
||||
* @param {any} data Page data.
|
||||
* @return {string} Page contents.
|
||||
*/
|
||||
getPageContentsFromPageData(data: any): string {
|
||||
// Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered.
|
||||
this.div.innerHTML = data.pagecontent;
|
||||
const contents = this.div.querySelector('.contents');
|
||||
|
||||
if (contents) {
|
||||
return contents.innerHTML.trim();
|
||||
}
|
||||
|
||||
// Cannot find contents element, return the page.contents (some elements like videos might not work).
|
||||
return data.page.contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a question and all the data required to render it from the page data (result of AddonModLessonProvider.getPageData).
|
||||
*
|
||||
* @param {FormGroup} questionForm The form group where to add the controls.
|
||||
* @param {any} pageData Page data (result of $mmaModLesson#getPageData).
|
||||
* @return {any} Question data.
|
||||
*/
|
||||
getQuestionFromPageData(questionForm: FormGroup, pageData: any): any {
|
||||
const question: any = {};
|
||||
|
||||
// Get the container of the question answers if it exists.
|
||||
this.div.innerHTML = pageData.pagecontent;
|
||||
const fieldContainer = this.div.querySelector('.fcontainer');
|
||||
|
||||
// Get hidden inputs and add their data to the form group.
|
||||
const hiddenInputs = <HTMLInputElement[]> Array.from(this.div.querySelectorAll('input[type="hidden"]'));
|
||||
hiddenInputs.forEach((input) => {
|
||||
questionForm.addControl(input.name, this.fb.control(input.value));
|
||||
});
|
||||
|
||||
// Get the submit button and extract its value.
|
||||
const submitButton = <HTMLInputElement> this.div.querySelector('input[type="submit"]');
|
||||
question.submitLabel = submitButton ? submitButton.value : this.translate.instant('addon.mod_lesson.submit');
|
||||
|
||||
if (!fieldContainer) {
|
||||
// Element not found, return.
|
||||
return question;
|
||||
}
|
||||
|
||||
let type;
|
||||
|
||||
switch (pageData.page.qtype) {
|
||||
case AddonModLessonProvider.LESSON_PAGE_TRUEFALSE:
|
||||
case AddonModLessonProvider.LESSON_PAGE_MULTICHOICE:
|
||||
question.template = 'multichoice';
|
||||
question.options = [];
|
||||
|
||||
// Get all the inputs. Search radio first.
|
||||
let inputs = <HTMLInputElement[]> Array.from(fieldContainer.querySelectorAll('input[type="radio"]'));
|
||||
if (!inputs || !inputs.length) {
|
||||
// Radio buttons not found, it might be a multi answer. Search for checkbox.
|
||||
question.multi = true;
|
||||
inputs = <HTMLInputElement[]> Array.from(fieldContainer.querySelectorAll('input[type="checkbox"]'));
|
||||
|
||||
if (!inputs || !inputs.length) {
|
||||
// No checkbox found either. Stop.
|
||||
return question;
|
||||
}
|
||||
}
|
||||
|
||||
let controlAdded = false;
|
||||
inputs.forEach((input) => {
|
||||
const option: any = {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
checked: !!input.checked,
|
||||
disabled: !!input.disabled
|
||||
},
|
||||
parent = input.parentElement;
|
||||
|
||||
if (option.checked || question.multi) {
|
||||
// Add the control.
|
||||
const value = question.multi ? {value: option.checked, disabled: option.disabled} : option.value;
|
||||
questionForm.addControl(option.name, this.fb.control(value));
|
||||
controlAdded = true;
|
||||
}
|
||||
|
||||
// Remove the input and use the rest of the parent contents as the label.
|
||||
input.remove();
|
||||
option.text = parent.innerHTML.trim();
|
||||
|
||||
question.options.push(option);
|
||||
});
|
||||
|
||||
if (!question.multi) {
|
||||
question.controlName = inputs[0].name; // All option have the same name in single choice.
|
||||
|
||||
if (!controlAdded) {
|
||||
// No checked option for single choice, add the control with an empty value.
|
||||
questionForm.addControl(question.controlName, this.fb.control(''));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case AddonModLessonProvider.LESSON_PAGE_NUMERICAL:
|
||||
type = 'number';
|
||||
case AddonModLessonProvider.LESSON_PAGE_SHORTANSWER:
|
||||
question.template = 'shortanswer';
|
||||
|
||||
// Get the input.
|
||||
const input = <HTMLInputElement> fieldContainer.querySelector('input[type="text"], input[type="number"]');
|
||||
if (!input) {
|
||||
return question;
|
||||
}
|
||||
|
||||
question.input = {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
maxlength: input.maxLength,
|
||||
type: type || 'text'
|
||||
};
|
||||
|
||||
// Init the control.
|
||||
questionForm.addControl(input.name, this.fb.control({value: input.value, disabled: input.readOnly}));
|
||||
break;
|
||||
|
||||
case AddonModLessonProvider.LESSON_PAGE_ESSAY:
|
||||
question.template = 'essay';
|
||||
|
||||
// Get the textarea.
|
||||
const textarea = fieldContainer.querySelector('textarea');
|
||||
|
||||
if (!textarea) {
|
||||
// Textarea not found, probably review mode.
|
||||
const answerEl = fieldContainer.querySelector('.reviewessay');
|
||||
if (!answerEl) {
|
||||
// Answer not found, stop.
|
||||
return question;
|
||||
}
|
||||
question.useranswer = answerEl.innerHTML;
|
||||
|
||||
} else {
|
||||
question.textarea = {
|
||||
id: textarea.id,
|
||||
name: textarea.name || 'answer[text]'
|
||||
};
|
||||
|
||||
// Init the control.
|
||||
question.control = this.fb.control('');
|
||||
questionForm.addControl(question.textarea.name, question.control);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case AddonModLessonProvider.LESSON_PAGE_MATCHING:
|
||||
question.template = 'matching';
|
||||
|
||||
const rows = Array.from(fieldContainer.querySelectorAll('.answeroption'));
|
||||
question.rows = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
const label = row.querySelector('label'),
|
||||
select = row.querySelector('select'),
|
||||
options = Array.from(row.querySelectorAll('option')),
|
||||
rowData: any = {};
|
||||
|
||||
if (!label || !select || !options || !options.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the row's text (label).
|
||||
rowData.text = label.innerHTML.trim();
|
||||
rowData.id = select.id;
|
||||
rowData.name = select.name;
|
||||
rowData.options = [];
|
||||
|
||||
// Treat each option.
|
||||
let controlAdded = false;
|
||||
options.forEach((option) => {
|
||||
if (typeof option.value == 'undefined') {
|
||||
// Option not valid, ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
const opt = {
|
||||
value: option.value,
|
||||
label: option.innerHTML.trim(),
|
||||
selected: option.selected
|
||||
};
|
||||
|
||||
if (opt.selected) {
|
||||
controlAdded = true;
|
||||
questionForm.addControl(rowData.name, this.fb.control({value: opt.value, disabled: !!select.disabled}));
|
||||
}
|
||||
|
||||
rowData.options.push(opt);
|
||||
});
|
||||
|
||||
if (!controlAdded) {
|
||||
// No selected option, add the control with an empty value.
|
||||
questionForm.addControl(rowData.name, this.fb.control({value: '', disabled: !!select.disabled}));
|
||||
}
|
||||
|
||||
question.rows.push(rowData);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
return question;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the question data to be sent to server.
|
||||
*
|
||||
* @param {any} question Question to prepare.
|
||||
* @param {any} data Data to prepare.
|
||||
* @return {Promise<any>} Promise resolved with the data to send when done.
|
||||
*/
|
||||
prepareQuestionData(question: any, data: any): Promise<any> {
|
||||
if (question.template == 'essay' && question.textarea) {
|
||||
// The answer might need formatting. Check if rich text editor is enabled or not.
|
||||
return this.domUtils.isRichTextEditorEnabled().then((enabled) => {
|
||||
if (!enabled) {
|
||||
// Rich text editor not enabled, add some HTML to the answer if needed.
|
||||
data[question.textarea.property] = this.textUtils.formatHtmlLines(data[question.textarea.property]);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
} else if (question.template == 'multichoice' && question.multi) {
|
||||
// Only send the options with value set to true.
|
||||
for (const name in data) {
|
||||
if (name.match(/answer\[\d+\]/) && data[name] == false) {
|
||||
delete data[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the feedback of a process page in HTML, remove the question text.
|
||||
*
|
||||
* @param {string} html Feedback's HTML.
|
||||
* @return {string} Feedback without the question text.
|
||||
*/
|
||||
removeQuestionFromFeedback(html: string): string {
|
||||
this.div.innerHTML = html;
|
||||
|
||||
// Remove the question text.
|
||||
this.domUtils.removeElement(this.div, '.generalbox:not(.feedback):not(.correctanswer)');
|
||||
|
||||
return this.div.innerHTML.trim();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue