2
0
Fork 0

MOBILE-2350 scorm: Implement SCORM player

main
Dani Palou 2018-04-26 12:09:48 +02:00
parent 5793af108d
commit 8e577becc7
9 changed files with 803 additions and 5 deletions

View File

@ -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 {}

View File

@ -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>

View File

@ -0,0 +1,3 @@
addon-mod-scorm-toc-popover {
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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');
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}
});
}
}