forked from EVOgeek/Vmeda.Online
MOBILE-2350 scorm: Implement SCORM player
parent
5793af108d
commit
8e577becc7
|
@ -20,10 +20,12 @@ import { CoreComponentsModule } from '@components/components.module';
|
|||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||
import { AddonModScormIndexComponent } from './index/index';
|
||||
import { AddonModScormTocPopoverComponent } from './toc-popover/toc-popover';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModScormIndexComponent
|
||||
AddonModScormIndexComponent,
|
||||
AddonModScormTocPopoverComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -36,10 +38,12 @@ import { AddonModScormIndexComponent } from './index/index';
|
|||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModScormIndexComponent
|
||||
AddonModScormIndexComponent,
|
||||
AddonModScormTocPopoverComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModScormIndexComponent
|
||||
AddonModScormIndexComponent,
|
||||
AddonModScormTocPopoverComponent
|
||||
]
|
||||
})
|
||||
export class AddonModScormComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<ion-list>
|
||||
<ion-item text-wrap *ngIf="attemptToContinue">
|
||||
<p>{{ 'addon.mod_scorm.dataattemptshown' | translate:{number: attemptToContinue} }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-center *ngIf="isBrowse">
|
||||
<p>{{ 'addon.mod_scorm.mod_scorm.browsemode' }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-center *ngIf="isReview">
|
||||
<p>{{ 'addon.mod_scorm.mod_scorm.reviewmode' }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- List of SCOs. -->
|
||||
<ng-container *ngFor="let sco of toc">
|
||||
<a *ngIf="sco.isvisible" ion-item text-wrap [ngClass]="['core-padding-' + sco.level]" (click)="loadSco(sco)" [attr.disabled]="!sco.prereq || !sco.launch ? true : null" detail-none>
|
||||
<img [src]="sco.image.url" [alt]="sco.image.description" />
|
||||
<span>{{ sco.title }}</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
</ion-list>
|
|
@ -0,0 +1,3 @@
|
|||
addon-mod-scorm-toc-popover {
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// (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 { NavParams, ViewController } from 'ionic-angular';
|
||||
import { AddonModScormProvider } from '../../providers/scorm';
|
||||
|
||||
/**
|
||||
* Component to display the TOC of a SCORM.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-scorm-toc-popover',
|
||||
templateUrl: 'toc-popover.html'
|
||||
})
|
||||
export class AddonModScormTocPopoverComponent {
|
||||
toc: any[];
|
||||
isBrowse: boolean;
|
||||
isReview: boolean;
|
||||
attemptToContinue: number;
|
||||
|
||||
constructor(navParams: NavParams, private viewCtrl: ViewController) {
|
||||
this.toc = navParams.get('toc') || [];
|
||||
this.attemptToContinue = navParams.get('attemptToContinue');
|
||||
|
||||
const mode = navParams.get('mode');
|
||||
|
||||
this.isBrowse = mode === AddonModScormProvider.MODEBROWSE;
|
||||
this.isReview = mode === AddonModScormProvider.MODEREVIEW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when a SCO is clicked.
|
||||
*
|
||||
* @param {any} sco Clicked SCO.
|
||||
*/
|
||||
loadSco(sco: any): void {
|
||||
if (!sco.prereq || !sco.isvisible || !sco.launch) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewCtrl.dismiss(sco);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<button *ngIf="showToc && !loadingToc && toc && toc.length" ion-button icon-only (click)="openToc($event)">
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
</button>
|
||||
<ion-spinner *ngIf="showToc && loadingToc"></ion-spinner>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<core-navigation-bar [previous]="previousSco" [next]="nextSco" (action)="loadSco($event)"></core-navigation-bar>
|
||||
<core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scorm.popup ? scorm.width : undefined" [iframeHeight]="scorm.popup ? scorm.height : undefined"></core-iframe>
|
||||
<p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p>
|
||||
</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 { AddonModScormPlayerPage } from './player';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModScormPlayerPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
IonicPageModule.forChild(AddonModScormPlayerPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModScormPlayerPageModule {}
|
|
@ -0,0 +1,450 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { IonicPage, NavParams, PopoverController } from 'ionic-angular';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { AddonModScormProvider, AddonModScormAttemptCountResult } from '../../providers/scorm';
|
||||
import { AddonModScormHelperProvider } from '../../providers/helper';
|
||||
import { AddonModScormSyncProvider } from '../../providers/scorm-sync';
|
||||
import { AddonModScormDataModel12 } from '../../classes/data-model-12';
|
||||
import { AddonModScormTocPopoverComponent } from '../../components/toc-popover/toc-popover';
|
||||
|
||||
/**
|
||||
* Page that allows playing a SCORM.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-scorm-player' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-scorm-player',
|
||||
templateUrl: 'player.html',
|
||||
})
|
||||
export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||
|
||||
title: string; // Title.
|
||||
scorm: any; // The SCORM object.
|
||||
showToc: boolean; // Whether to show the table of contents (TOC).
|
||||
loadingToc = true; // Whether the TOC is being loaded.
|
||||
toc: any[]; // List of SCOs.
|
||||
loaded: boolean; // Whether the data has been loaded.
|
||||
previousSco: any; // Previous SCO.
|
||||
nextSco: any; // Next SCO.
|
||||
src: string; // Iframe src.
|
||||
errorMessage: string; // Error message.
|
||||
|
||||
protected siteId: string;
|
||||
protected mode: string; // Mode to play the SCORM.
|
||||
protected newAttempt: boolean; // Whether to start a new attempt.
|
||||
protected organizationId: string; // Organization ID to load.
|
||||
protected attempt: number; // The attempt number.
|
||||
protected offline = false; // Whether it's offline mode.
|
||||
protected userData: any; // User data.
|
||||
protected initialScoId: number; // Initial SCO ID to load.
|
||||
protected currentSco: any; // Current SCO.
|
||||
protected dataModel: AddonModScormDataModel12; // Data Model.
|
||||
protected attemptToContinue: number; // Attempt to continue (for the popover).
|
||||
|
||||
// Observers.
|
||||
protected tocObserver: any;
|
||||
protected launchNextObserver: any;
|
||||
protected launchPrevObserver: any;
|
||||
protected goOfflineObserver: any;
|
||||
|
||||
constructor(navParams: NavParams, protected popoverCtrl: PopoverController, protected eventsProvider: CoreEventsProvider,
|
||||
protected sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider,
|
||||
protected domUtils: CoreDomUtilsProvider, protected timeUtils: CoreTimeUtilsProvider,
|
||||
protected scormProvider: AddonModScormProvider, protected scormHelper: AddonModScormHelperProvider,
|
||||
protected scormSyncProvider: AddonModScormSyncProvider) {
|
||||
|
||||
this.scorm = navParams.get('scorm') || {};
|
||||
this.mode = navParams.get('mode') || AddonModScormProvider.MODENORMAL;
|
||||
this.newAttempt = !!navParams.get('newAttempt');
|
||||
this.organizationId = navParams.get('organizationId');
|
||||
this.initialScoId = navParams.get('scoId');
|
||||
this.siteId = this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// We use SCORM name at start, later we'll use the SCO title.
|
||||
this.title = this.scorm.name;
|
||||
|
||||
// Block the SCORM so it cannot be synchronized.
|
||||
this.syncProvider.blockOperation(AddonModScormProvider.COMPONENT, this.scorm.id, 'player');
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
|
||||
this.showToc = this.scormProvider.displayTocInPlayer(this.scorm);
|
||||
|
||||
if (this.scorm.popup) {
|
||||
// If we receive a value <= 100 we need to assume it's a percentage.
|
||||
if (this.scorm.width <= 100) {
|
||||
this.scorm.width = this.scorm.width + '%';
|
||||
}
|
||||
if (this.scorm.height <= 100) {
|
||||
this.scorm.height = this.scorm.height + '%';
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the SCORM data.
|
||||
this.fetchData().then(() => {
|
||||
if (this.currentSco) {
|
||||
// Set start time if it's a new attempt.
|
||||
const promise = this.newAttempt ? this.setStartTime(this.currentSco.id) : Promise.resolve();
|
||||
|
||||
return promise.catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
|
||||
}).finally(() => {
|
||||
// Load SCO.
|
||||
this.loadSco(this.currentSco);
|
||||
});
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
|
||||
// Listen for events to update the TOC, navigate through SCOs and go offline.
|
||||
this.tocObserver = this.eventsProvider.on(AddonModScormProvider.UPDATE_TOC_EVENT, (data) => {
|
||||
if (data.scormId === this.scorm.id) {
|
||||
if (this.offline) {
|
||||
// Wait a bit to make sure data is stored.
|
||||
setTimeout(this.refreshToc.bind(this), 100);
|
||||
} else {
|
||||
this.refreshToc();
|
||||
}
|
||||
}
|
||||
}, this.siteId);
|
||||
|
||||
this.launchNextObserver = this.eventsProvider.on(AddonModScormProvider.LAUNCH_NEXT_SCO_EVENT, (data) => {
|
||||
if (data.scormId === this.scorm.id && this.nextSco) {
|
||||
this.loadSco(this.nextSco);
|
||||
}
|
||||
}, this.siteId);
|
||||
|
||||
this.launchPrevObserver = this.eventsProvider.on(AddonModScormProvider.LAUNCH_PREV_SCO_EVENT, (data) => {
|
||||
if (data.scormId === this.scorm.id && this.previousSco) {
|
||||
this.loadSco(this.previousSco);
|
||||
}
|
||||
}, this.siteId);
|
||||
|
||||
this.goOfflineObserver = this.eventsProvider.on(AddonModScormProvider.GO_OFFLINE_EVENT, (data) => {
|
||||
if (data.scormId === this.scorm.id && !this.offline) {
|
||||
this.offline = true;
|
||||
|
||||
// Wait a bit to prevent collisions between this store and SCORM API's store.
|
||||
setTimeout(() => {
|
||||
this.scormHelper.convertAttemptToOffline(this.scorm, this.attempt).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.error', true);
|
||||
}).then(() => {
|
||||
this.refreshToc();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the next and previous SCO.
|
||||
*
|
||||
* @param {number} scoId Current SCO ID.
|
||||
*/
|
||||
protected calculateNextAndPreviousSco(scoId: number): void {
|
||||
this.previousSco = this.scormHelper.getPreviousScoFromToc(this.toc, scoId);
|
||||
this.nextSco = this.scormHelper.getNextScoFromToc(this.toc, scoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the attempt to use, the mode (normal/preview) and if it's offline or online.
|
||||
*
|
||||
* @param {AddonModScormAttemptCountResult} attemptsData Attempts count.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected determineAttemptAndMode(attemptsData: AddonModScormAttemptCountResult): Promise<any> {
|
||||
let result;
|
||||
|
||||
return this.scormHelper.determineAttemptToContinue(this.scorm, attemptsData).then((data) => {
|
||||
this.attempt = data.number;
|
||||
this.offline = data.offline;
|
||||
|
||||
if (this.attempt != attemptsData.lastAttempt.number) {
|
||||
this.attemptToContinue = this.attempt;
|
||||
}
|
||||
|
||||
// Check if current attempt is incomplete.
|
||||
if (this.attempt > 0) {
|
||||
return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, this.offline);
|
||||
} else {
|
||||
// User doesn't have attempts. Last attempt is not incomplete (since he doesn't have any).
|
||||
return false;
|
||||
}
|
||||
}).then((incomplete) => {
|
||||
// Determine mode and attempt to use.
|
||||
result = this.scormProvider.determineAttemptAndMode(this.scorm, this.mode, this.attempt, this.newAttempt, incomplete);
|
||||
|
||||
if (result.attempt > this.attempt) {
|
||||
// We're creating a new attempt.
|
||||
if (this.offline) {
|
||||
// Last attempt was offline, so we'll create a new offline attempt.
|
||||
return this.scormHelper.createOfflineAttempt(this.scorm, result.attempt, attemptsData.online.length);
|
||||
} else {
|
||||
// Last attempt was online, verify that we can create a new online attempt. We ignore cache.
|
||||
return this.scormProvider.getScormUserData(this.scorm.id, result.attempt, undefined, false, true).catch(() => {
|
||||
// Cannot communicate with the server, create an offline attempt.
|
||||
this.offline = true;
|
||||
|
||||
return this.scormHelper.createOfflineAttempt(this.scorm, result.attempt, attemptsData.online.length);
|
||||
});
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
this.mode = result.mode;
|
||||
this.newAttempt = result.newAttempt;
|
||||
this.attempt = result.attempt;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data needed to play the SCORM.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchData(): Promise<any> {
|
||||
// Wait for any ongoing sync to finish. We won't sync a SCORM while it's being played.
|
||||
return this.scormSyncProvider.waitForSync(this.scorm.id).then(() => {
|
||||
// Get attempts data.
|
||||
return this.scormProvider.getAttemptCount(this.scorm.id).then((attemptsData) => {
|
||||
return this.determineAttemptAndMode(attemptsData).then(() => {
|
||||
// Fetch TOC and get user data.
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.fetchToc());
|
||||
promises.push(this.scormProvider.getScormUserData(this.scorm.id, this.attempt, undefined, this.offline)
|
||||
.then((data) => {
|
||||
this.userData = data;
|
||||
}));
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the TOC.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchToc(): Promise<any> {
|
||||
this.loadingToc = true;
|
||||
|
||||
// We need to check incomplete again: attempt number or status might have changed.
|
||||
return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, this.offline).then((incomplete) => {
|
||||
this.scorm.incomplete = incomplete;
|
||||
|
||||
// Get TOC.
|
||||
return this.scormProvider.getOrganizationToc(this.scorm.id, this.attempt, this.organizationId, this.offline);
|
||||
}).then((toc) => {
|
||||
this.toc = this.scormProvider.formatTocToArray(toc);
|
||||
|
||||
// Get images for each SCO.
|
||||
this.toc.forEach((sco) => {
|
||||
sco.image = this.scormProvider.getScoStatusIcon(sco, this.scorm.incomplete);
|
||||
});
|
||||
|
||||
// Determine current SCO if we received an ID..
|
||||
if (this.initialScoId > 0) {
|
||||
// SCO set by parameter, get it from TOC.
|
||||
this.currentSco = this.scormHelper.getScoFromToc(this.toc, this.initialScoId);
|
||||
}
|
||||
|
||||
if (!this.currentSco) {
|
||||
// No SCO defined. Get the first valid one.
|
||||
return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, this.toc, this.organizationId, this.offline)
|
||||
.then((sco) => {
|
||||
|
||||
if (sco) {
|
||||
this.currentSco = sco;
|
||||
} else {
|
||||
// We couldn't find a SCO to load: they're all inactive or without launch URL.
|
||||
this.errorMessage = 'addon.mod_scorm.errornovalidsco';
|
||||
}
|
||||
});
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loadingToc = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page will leave.
|
||||
*/
|
||||
ionViewWillLeave(): void {
|
||||
// Empty src when leaving the state so unload event is triggered in the iframe.
|
||||
this.src = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a SCO.
|
||||
*
|
||||
* @param {any} sco The SCO to load.
|
||||
*/
|
||||
protected loadSco(sco: any): void {
|
||||
if (!this.dataModel) {
|
||||
// Create the model.
|
||||
this.dataModel = new AddonModScormDataModel12(this.eventsProvider, this.scormProvider, this.siteId, this.scorm, sco.id,
|
||||
this.attempt, this.userData, this.mode, this.offline);
|
||||
|
||||
// Add the model to the window so the SCORM can access it.
|
||||
(<any> window).API = this.dataModel;
|
||||
} else {
|
||||
// Load the SCO in the existing model.
|
||||
this.dataModel.loadSco(sco.id);
|
||||
}
|
||||
|
||||
this.currentSco = sco;
|
||||
this.title = sco.title || this.scorm.name; // Try to use SCO title.
|
||||
|
||||
this.calculateNextAndPreviousSco(sco.id);
|
||||
|
||||
// Load the SCO source.
|
||||
this.scormProvider.getScoSrc(this.scorm, sco).then((src) => {
|
||||
if (src == this.src) {
|
||||
// Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed.
|
||||
this.src = '';
|
||||
|
||||
setTimeout(() => {
|
||||
this.src = src;
|
||||
});
|
||||
} else {
|
||||
this.src = src;
|
||||
}
|
||||
});
|
||||
|
||||
if (sco.scormtype == 'asset') {
|
||||
// Mark the asset as completed.
|
||||
const tracks = [{
|
||||
element: 'cmi.core.lesson_status',
|
||||
value: 'completed'
|
||||
}];
|
||||
|
||||
this.scormProvider.saveTracks(sco.id, this.attempt, tracks, this.scorm, this.offline).catch(() => {
|
||||
// Error saving data. We'll go offline if we're online and the asset is not marked as completed already.
|
||||
if (!this.offline) {
|
||||
return this.scormProvider.getScormUserData(this.scorm.id, this.attempt, undefined, false).then((data) => {
|
||||
if (!data[sco.id] || data[sco.id].userdata['cmi.core.lesson_status'] != 'completed') {
|
||||
// Go offline.
|
||||
return this.scormHelper.convertAttemptToOffline(this.scorm, this.attempt).then(() => {
|
||||
this.offline = true;
|
||||
this.dataModel.setOffline(true);
|
||||
|
||||
return this.scormProvider.saveTracks(sco.id, this.attempt, tracks, this.scorm, true);
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.error', true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
// Refresh TOC, some prerequisites might have changed.
|
||||
this.refreshToc();
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger SCO launch event.
|
||||
this.scormProvider.logLaunchSco(this.scorm.id, sco.id).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the TOC.
|
||||
*
|
||||
* @param {MouseEvent} event Event.
|
||||
*/
|
||||
openToc(event: MouseEvent): void {
|
||||
const popover = this.popoverCtrl.create(AddonModScormTocPopoverComponent, {
|
||||
toc: this.toc,
|
||||
attemptToContinue: this.attemptToContinue,
|
||||
mode: this.mode
|
||||
});
|
||||
|
||||
// If the popover sends back a SCO, load it.
|
||||
popover.onDidDismiss((sco) => {
|
||||
if (sco) {
|
||||
this.loadSco(sco);
|
||||
}
|
||||
});
|
||||
|
||||
popover.present({
|
||||
ev: event
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the TOC.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected refreshToc(): Promise<any> {
|
||||
return this.scormProvider.invalidateAllScormData(this.scorm.id).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.fetchToc();
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set SCORM start time.
|
||||
*
|
||||
* @param {number} scoId SCO ID.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected setStartTime(scoId: number): Promise<any> {
|
||||
const tracks = [{
|
||||
element: 'x.start.time',
|
||||
value: this.timeUtils.timestamp()
|
||||
}];
|
||||
|
||||
return this.scormProvider.saveTracks(scoId, this.attempt, tracks, this.scorm, this.offline).then(() => {
|
||||
if (!this.offline) {
|
||||
// New online attempt created, update cached data about online attempts.
|
||||
this.scormProvider.getAttemptCount(this.scorm.id, false, true).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Stop listening for events.
|
||||
this.tocObserver && this.tocObserver.off();
|
||||
this.launchNextObserver && this.launchNextObserver.off();
|
||||
this.launchPrevObserver && this.launchPrevObserver.off();
|
||||
this.goOfflineObserver && this.goOfflineObserver.off();
|
||||
|
||||
// Unblock the SCORM so it can be synced.
|
||||
this.syncProvider.unblockOperation(AddonModScormProvider.COMPONENT, this.scorm.id, 'player');
|
||||
}
|
||||
}
|
|
@ -13,8 +13,12 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm';
|
||||
import { AddonModScormOfflineProvider } from './scorm-offline';
|
||||
|
||||
/**
|
||||
* Helper service that provides some features for SCORM.
|
||||
|
@ -22,9 +26,13 @@ import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm'
|
|||
@Injectable()
|
||||
export class AddonModScormHelperProvider {
|
||||
|
||||
protected div = document.createElement('div'); // A div element to search in HTML code.
|
||||
// List of elements we want to ignore when copying attempts (they're calculated).
|
||||
protected elementsToIgnore = ['status', 'score_raw', 'total_time', 'session_time', 'student_id', 'student_name', 'credit',
|
||||
'mode', 'entry'];
|
||||
|
||||
constructor(private domUtils: CoreDomUtilsProvider, private scormProvider: AddonModScormProvider) { }
|
||||
constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService,
|
||||
private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
|
||||
private scormProvider: AddonModScormProvider, private scormOfflineProvider: AddonModScormOfflineProvider) { }
|
||||
|
||||
/**
|
||||
* Show a confirm dialog if needed. If SCORM doesn't have size, try to calculate it.
|
||||
|
@ -58,6 +66,93 @@ export class AddonModScormHelperProvider {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new offline attempt based on an existing online attempt.
|
||||
*
|
||||
* @param {any} scorm SCORM.
|
||||
* @param {number} attempt Number of the online attempt.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the attempt is created.
|
||||
*/
|
||||
convertAttemptToOffline(scorm: any, attempt: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Get data from the online attempt.
|
||||
return this.scormProvider.getScormUserData(scorm.id, attempt, undefined, false, false, siteId).then((onlineData) => {
|
||||
// The SCORM API might have written some data to the offline attempt already.
|
||||
// We don't want to override it with cached online data.
|
||||
return this.scormOfflineProvider.getScormUserData(scorm.id, attempt, undefined, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then((offlineData) => {
|
||||
const dataToStore = this.utils.clone(onlineData);
|
||||
|
||||
// Filter the data to copy.
|
||||
for (const scoId in dataToStore) {
|
||||
const sco = dataToStore[scoId];
|
||||
|
||||
// Delete calculated data.
|
||||
this.elementsToIgnore.forEach((el) => {
|
||||
delete sco.userdata[el];
|
||||
});
|
||||
|
||||
// Don't override offline data.
|
||||
if (offlineData && offlineData[sco.scoid] && offlineData[sco.scoid].userdata) {
|
||||
const scoUserData = {};
|
||||
|
||||
for (const element in sco.userdata) {
|
||||
if (!offlineData[sco.scoid].userdata[element]) {
|
||||
// This element is not stored in offline, we can save it.
|
||||
scoUserData[element] = sco.userdata[element];
|
||||
}
|
||||
}
|
||||
|
||||
sco.userdata = scoUserData;
|
||||
}
|
||||
}
|
||||
|
||||
return this.scormOfflineProvider.createNewAttempt(scorm, attempt, dataToStore, onlineData, siteId);
|
||||
});
|
||||
}).catch(() => {
|
||||
// Shouldn't happen.
|
||||
return Promise.reject(this.translate.instant('addon.mod_scorm.errorcreateofflineattempt'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new offline attempt.
|
||||
*
|
||||
* @param {any} scorm SCORM.
|
||||
* @param {number} newAttempt Number of the new attempt.
|
||||
* @param {number} lastOnline Number of the last online attempt.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the attempt is created.
|
||||
*/
|
||||
createOfflineAttempt(scorm: any, newAttempt: number, lastOnline: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Try to get data from online attempts.
|
||||
return this.searchOnlineAttemptUserData(scorm.id, lastOnline, siteId).then((userData) => {
|
||||
// We're creating a new attempt, remove all the user data that is not needed for a new attempt.
|
||||
for (const scoId in userData) {
|
||||
const sco = userData[scoId],
|
||||
filtered = {};
|
||||
|
||||
for (const element in sco.userdata) {
|
||||
if (element.indexOf('.') == -1 && this.elementsToIgnore.indexOf(element) == -1) {
|
||||
// The element doesn't use a dot notation, probably SCO data.
|
||||
filtered[element] = sco.userdata[element];
|
||||
}
|
||||
}
|
||||
|
||||
sco.userdata = filtered;
|
||||
}
|
||||
|
||||
return this.scormOfflineProvider.createNewAttempt(scorm, newAttempt, userData, undefined, siteId);
|
||||
}).catch(() => {
|
||||
return Promise.reject(this.translate.instant('addon.mod_scorm.errorcreateofflineattempt'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the attempt to continue/review. It will be:
|
||||
* - The last incomplete online attempt if it hasn't been continued in offline and all offline attempts are complete.
|
||||
|
@ -97,6 +192,41 @@ export class AddonModScormHelperProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first SCO to load in a SCORM. If a non-empty TOC is provided, it will be the first valid SCO in the TOC.
|
||||
* Otherwise, it will be the first valid SCO returned by $mmaModScorm#getScos.
|
||||
*
|
||||
* @param {number} scormId Scorm ID.
|
||||
* @param {number} attempt Attempt number.
|
||||
* @param {any[]} [toc] SCORM's TOC.
|
||||
* @param {string} [organization] Organization to use.
|
||||
* @param {boolean} [offline] Whether the attempt is offline.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with the first SCO.
|
||||
*/
|
||||
getFirstSco(scormId: number, attempt: number, toc?: any[], organization?: string, offline?: boolean, siteId?: string)
|
||||
: Promise<any> {
|
||||
|
||||
let promise;
|
||||
if (toc && toc.length) {
|
||||
promise = Promise.resolve(toc);
|
||||
} else {
|
||||
// SCORM doesn't have a TOC. Get all the scos.
|
||||
promise = this.scormProvider.getScosWithData(scormId, attempt, organization, offline, false, siteId);
|
||||
}
|
||||
|
||||
return promise.then((scos) => {
|
||||
// Search the first valid SCO.
|
||||
for (let i = 0; i < scos.length; i++) {
|
||||
const sco = scos[i];
|
||||
|
||||
if (sco.isvisible && sco.prereq && sco.launch) {
|
||||
return sco;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last attempt (number and whether it's offline).
|
||||
* It'll be the highest number as long as it doesn't surpass the max number of attempts.
|
||||
|
@ -118,4 +248,83 @@ export class AddonModScormHelperProvider {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a TOC in array format and a scoId, return the next available SCO.
|
||||
*
|
||||
* @param {any[]} toc SCORM's TOC.
|
||||
* @param {number} scoId SCO ID.
|
||||
* @return {any} Next SCO.
|
||||
*/
|
||||
getNextScoFromToc(toc: any, scoId: number): any {
|
||||
for (let i = 0; i < toc.length; i++) {
|
||||
if (toc[i].id == scoId) {
|
||||
// We found the current SCO. Now let's search the next visible SCO with fulfilled prerequisites.
|
||||
for (let j = i + 1; j < toc.length; j++) {
|
||||
if (toc[j].isvisible && toc[j].prereq && toc[j].launch) {
|
||||
return toc[j];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a TOC in array format and a scoId, return the previous available SCO.
|
||||
*
|
||||
* @param {any[]} toc SCORM's TOC.
|
||||
* @param {number} scoId SCO ID.
|
||||
* @return {any} Previous SCO.
|
||||
*/
|
||||
getPreviousScoFromToc(toc: any, scoId: number): any {
|
||||
for (let i = 0; i < toc.length; i++) {
|
||||
if (toc[i].id == scoId) {
|
||||
// We found the current SCO. Now let's search the previous visible SCO with fulfilled prerequisites.
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (toc[j].isvisible && toc[j].prereq && toc[j].launch) {
|
||||
return toc[j];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a TOC in array format and a scoId, return the SCO.
|
||||
*
|
||||
* @param {any[]} toc SCORM's TOC.
|
||||
* @param {number} scoId SCO ID.
|
||||
* @return {any} SCO.
|
||||
*/
|
||||
getScoFromToc(toc: any[], scoId: number): any {
|
||||
for (let i = 0; i < toc.length; i++) {
|
||||
if (toc[i].id == scoId) {
|
||||
return toc[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches user data for an online attempt. If the data can't be retrieved, re-try with the previous online attempt.
|
||||
*
|
||||
* @param {number} scormId SCORM ID.
|
||||
* @param {number} attempt Online attempt to get the data.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with user data.
|
||||
*/
|
||||
searchOnlineAttemptUserData(scormId: number, attempt: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, false, siteId).catch(() => {
|
||||
if (attempt > 0) {
|
||||
// We couldn't retrieve the data. Try again with the previous online attempt.
|
||||
return this.searchOnlineAttemptUserData(scormId, attempt - 1, siteId);
|
||||
} else {
|
||||
// No more attempts to try. Reject
|
||||
return Promise.reject(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,6 +139,13 @@ export class CoreIframeComponent implements OnInit, OnChanges {
|
|||
winAndDoc = this.getContentWindowAndDocument(element);
|
||||
this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document);
|
||||
this.treatLinks(element, winAndDoc.document);
|
||||
|
||||
if (winAndDoc.window) {
|
||||
// Send a resize events to the iframe so it calculates the right size if needed.
|
||||
setTimeout(() => {
|
||||
winAndDoc.window.dispatchEvent(new Event('resize'));
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue