MOBILE-2350 scorm: Implement SCORM player
This commit is contained in:
		
							parent
							
								
									5793af108d
								
							
						
					
					
						commit
						8e577becc7
					
				@ -20,10 +20,12 @@ import { CoreComponentsModule } from '@components/components.module';
 | 
				
			|||||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
					import { CoreDirectivesModule } from '@directives/directives.module';
 | 
				
			||||||
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
 | 
					import { CoreCourseComponentsModule } from '@core/course/components/components.module';
 | 
				
			||||||
import { AddonModScormIndexComponent } from './index/index';
 | 
					import { AddonModScormIndexComponent } from './index/index';
 | 
				
			||||||
 | 
					import { AddonModScormTocPopoverComponent } from './toc-popover/toc-popover';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@NgModule({
 | 
					@NgModule({
 | 
				
			||||||
    declarations: [
 | 
					    declarations: [
 | 
				
			||||||
        AddonModScormIndexComponent
 | 
					        AddonModScormIndexComponent,
 | 
				
			||||||
 | 
					        AddonModScormTocPopoverComponent
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    imports: [
 | 
					    imports: [
 | 
				
			||||||
        CommonModule,
 | 
					        CommonModule,
 | 
				
			||||||
@ -36,10 +38,12 @@ import { AddonModScormIndexComponent } from './index/index';
 | 
				
			|||||||
    providers: [
 | 
					    providers: [
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    exports: [
 | 
					    exports: [
 | 
				
			||||||
        AddonModScormIndexComponent
 | 
					        AddonModScormIndexComponent,
 | 
				
			||||||
 | 
					        AddonModScormTocPopoverComponent
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    entryComponents: [
 | 
					    entryComponents: [
 | 
				
			||||||
        AddonModScormIndexComponent
 | 
					        AddonModScormIndexComponent,
 | 
				
			||||||
 | 
					        AddonModScormTocPopoverComponent
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AddonModScormComponentsModule {}
 | 
					export class AddonModScormComponentsModule {}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								src/addon/mod/scorm/components/toc-popover/toc-popover.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/addon/mod/scorm/components/toc-popover/toc-popover.html
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					addon-mod-scorm-toc-popover {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								src/addon/mod/scorm/components/toc-popover/toc-popover.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/addon/mod/scorm/components/toc-popover/toc-popover.ts
									
									
									
									
									
										Normal 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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/addon/mod/scorm/pages/player/player.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/addon/mod/scorm/pages/player/player.html
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/addon/mod/scorm/pages/player/player.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/addon/mod/scorm/pages/player/player.module.ts
									
									
									
									
									
										Normal 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 {}
 | 
				
			||||||
							
								
								
									
										450
									
								
								src/addon/mod/scorm/pages/player/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										450
									
								
								src/addon/mod/scorm/pages/player/player.ts
									
									
									
									
									
										Normal 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');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -13,8 +13,12 @@
 | 
				
			|||||||
// limitations under the License.
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Injectable } from '@angular/core';
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
 | 
					import { TranslateService } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { CoreSitesProvider } from '@providers/sites';
 | 
				
			||||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
 | 
					import { CoreDomUtilsProvider } from '@providers/utils/dom';
 | 
				
			||||||
 | 
					import { CoreUtilsProvider } from '@providers/utils/utils';
 | 
				
			||||||
import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm';
 | 
					import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm';
 | 
				
			||||||
 | 
					import { AddonModScormOfflineProvider } from './scorm-offline';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Helper service that provides some features for SCORM.
 | 
					 * Helper service that provides some features for SCORM.
 | 
				
			||||||
@ -22,9 +26,13 @@ import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm'
 | 
				
			|||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AddonModScormHelperProvider {
 | 
					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.
 | 
					     * 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:
 | 
					     * 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.
 | 
					     * - 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).
 | 
					     * 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.
 | 
					     * 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);
 | 
					                winAndDoc = this.getContentWindowAndDocument(element);
 | 
				
			||||||
                this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document);
 | 
					                this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document);
 | 
				
			||||||
                this.treatLinks(element, 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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user