MOBILE-2348 quiz: Implement navigation modal

main
Dani Palou 2018-04-04 14:26:24 +02:00
parent 2411115a9c
commit 7fc6c6bd00
7 changed files with 164 additions and 13 deletions

View File

@ -48,6 +48,7 @@
"preview": "Preview",
"previewquiznow": "Preview quiz now",
"question": "Question",
"quiznavigation": "Quiz navigation",
"quizpassword": "Quiz password",
"reattemptquiz": "Re-attempt quiz",
"requirepasswordmessage": "To attempt this quiz you need to know the quiz password",

View File

@ -0,0 +1,41 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'addon.mod_quiz.quiznavigation' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content class="addon-mod_quiz-navigation-modal">
<nav>
<ion-list>
<!-- In player, show button to finish attempt. -->
<a ion-item text-wrap *ngIf="!pageInstance.isReview" (click)="loadPage(-1)">
{{ 'addon.mod_quiz.finishattemptdots' | translate }}
</a>
<!-- In review we can toggle between all questions in same page or one page at a time. -->
<a ion-item text-wrap *ngIf="pageInstance.isReview && pageInstance.numPages > 1" (click)="switchMode()">
<span *ngIf="!pageInstance.showAll">{{ 'addon.mod_quiz.showall' | translate }}</span>
<span *ngIf="pageInstance.showAll">{{ 'addon.mod_quiz.showeachpage' | translate }}</span>
</a>
<a ion-item text-wrap *ngFor="let question of pageInstance.navigation" class="{{question.stateClass}}" [ngClass]='{"addon-mod_quiz-selected": !pageInstance.showSummary && pageInstance.attempt.currentpage == question.page}' (click)="loadPage(question.page, question.slot)">
<span *ngIf="question.number">{{ 'core.question.questionno' | translate:{$a: question.number} }}</span>
<span *ngIf="!question.number">{{ 'core.question.information' | translate }}</span>
</a>
<!-- In player, show button to finish attempt. -->
<a ion-item text-wrap *ngIf="!pageInstance.isReview" (click)="loadPage(-1)">
{{ 'addon.mod_quiz.finishattemptdots' | translate }}
</a>
<!-- In review we can toggle between all questions in same page or one page at a time. -->
<a ion-item text-wrap *ngIf="pageInstance.isReview && pageInstance.numPages > 1" (click)="switchMode()">
<span ng-if="!pageInstance.showAll">{{ 'mma.mod_quiz.showall' | translate }}</span>
<span ng-if="pageInstance.showAll">{{ 'mma.mod_quiz.showeachpage' | translate }}</span>
</a>
</ion-list>
</nav>
</ion-content>

View File

@ -0,0 +1,29 @@
// (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 { AddonModQuizNavigationModalPage } from './navigation-modal';
import { TranslateModule } from '@ngx-translate/core';
@NgModule({
declarations: [
AddonModQuizNavigationModalPage
],
imports: [
IonicPageModule.forChild(AddonModQuizNavigationModalPage),
TranslateModule.forChild()
]
})
export class AddonModQuizNavigationModalPageModule {}

View File

@ -0,0 +1,5 @@
page-addon-mod-quiz-navigation-modal {
.addon-mod_quiz-selected, .item.addon-mod_quiz-selected {
background: $blue-light;
}
}

View File

@ -0,0 +1,66 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
/**
* Modal that renders the quiz navigation.
*/
@IonicPage({ segment: 'addon-mod-quiz-navigation-modal' })
@Component({
selector: 'page-addon-mod-quiz-navigation-modal',
templateUrl: 'navigation-modal.html',
})
export class AddonModQuizNavigationModalPage {
/**
* The instance of the page that opened the modal. We use the instance instead of the needed attributes for these reasons:
* - Some attributes can change dynamically, and we don't want to create the modal everytime the user opens it.
* - The onDidDismiss function takes a while to be called, making the app seem slow. This way we can directly call
* the functions we need without having to wait for the modal to be dismissed.
* @type {any}
*/
pageInstance: any;
constructor(params: NavParams, protected viewCtrl: ViewController) {
this.pageInstance = params.get('page');
}
/**
* Close modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
}
/**
* Load a certain page.
*
* @param {number} page The page to load.
* @param {number} [slot] Slot of the question to scroll to.
*/
loadPage(page: number, slot: number): void {
this.pageInstance.changePage && this.pageInstance.changePage(page, true, slot);
this.closeModal();
}
/**
* Switch mode in review.
*/
switchMode(): void {
this.pageInstance.switchMode && this.pageInstance.switchMode();
this.closeModal();
}
}

View File

@ -6,7 +6,9 @@
<button id="addon-mod_quiz-connection-error-button" ion-button icon-only [hidden]="!autoSaveError" (click)="showConnectionError($event)" [attr.aria-label]="'core.error' | translate">
<ion-icon name="alert"></ion-icon>
</button>
<!-- @todo <button menu-toggle="right" ng-if="toc && toc.length" class="button button-icon icon ion-bookmark" aria-label="{{ 'mma.mod_quiz.opentoc' | translate }}"></button> -->
<button *ngIf="navigation && navigation.length" ion-button icon-only [attr.aria-label]="'addon.mod_quiz.opentoc' | translate" (click)="navigationModal.present()">
<ion-icon name="bookmark"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
import { IonicPage, NavParams, Content, PopoverController } from 'ionic-angular';
import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreLoggerProvider } from '@providers/logger';
@ -47,7 +47,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
quizAborted: boolean; // Whether the quiz was aborted due to an error.
preflightData: any = {}; // Preflight data to attempt the quiz.
offline: boolean; // Whether the quiz is being attempted in offline mode.
toc: any[]; // TOC to navigate the questions.
navigation: any[]; // List of questions to navigate them.
questions: any[]; // Questions of the current page.
nextPage: number; // Next page.
previousPage: number; // Previous page.
@ -70,13 +70,15 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
protected timeUpCalled: boolean; // Whether the time up function has been called.
protected autoSave: AddonModQuizAutoSave; // Class to auto-save answers every certain time.
protected autoSaveErrorSubscription: Subscription; // To be notified when an error happens in auto-save.
protected navigationModal: Modal; // Modal to navigate through the questions.
constructor(navParams: NavParams, element: ElementRef, logger: CoreLoggerProvider, protected translate: TranslateService,
protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider,
protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController,
protected timeUtils: CoreTimeUtilsProvider, protected quizProvider: AddonModQuizProvider,
protected quizHelper: AddonModQuizHelperProvider, protected quizSync: AddonModQuizSyncProvider,
protected questionHelper: CoreQuestionHelperProvider, protected cdr: ChangeDetectorRef) {
protected questionHelper: CoreQuestionHelperProvider, protected cdr: ChangeDetectorRef,
protected modalCtrl: ModalController) {
this.quizId = navParams.get('quizId');
this.courseId = navParams.get('courseId');
@ -88,6 +90,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
// Create the auto save instance.
this.autoSave = new AddonModQuizAutoSave('addon-mod_quiz-player-form', '#addon-mod_quiz-connection-error-button',
logger, popoverCtrl, questionHelper, quizProvider);
// Create the navigation modal.
this.navigationModal = this.modalCtrl.create('AddonModQuizNavigationModalPage', {
page: this
});
}
/**
@ -164,10 +171,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
* Change the current page. If slot is supplied, try to scroll to that question.
*
* @param {number} page Page to load. -1 means summary.
* @param {boolean} [fromToc] Whether the page was selected using the TOC.
* @param {boolean} [fromModal] Whether the page was selected using the navigation modal.
* @param {number} [slot] Slot of the question to scroll to.
*/
changePage(page: number, fromToc?: boolean, slot?: number): void {
changePage(page: number, fromModal?: boolean, slot?: number): void {
if (page != -1 && (this.attempt.state == AddonModQuizProvider.ATTEMPT_OVERDUE || this.attempt.finishedOffline)) {
// We can't load a page if overdue or the local attempt is finished.
return;
@ -176,9 +183,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
this.scrollToQuestion(slot);
return;
} else if ((page == this.attempt.currentpage && !this.showSummary) || (fromToc && this.quiz.isSequential && page != -1)) {
} else if ((page == this.attempt.currentpage && !this.showSummary) || (fromModal && this.quiz.isSequential && page != -1)) {
// If the user is navigating to the current page we do nothing.
// Also, in sequential quizzes we don't allow navigating using the TOC except for finishing the quiz (summary).
// Also, in sequential quizzes we don't allow navigating using the modal except for finishing the quiz (summary).
return;
} else if (page === -1 && this.showSummary) {
// Summary already shown.
@ -417,14 +424,14 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
}
/**
* Load TOC to navigate the questions.
* Load data to navigate the questions using the navigation modal.
*
* @return {Promise<void>} Promise resolved when done.
*/
protected loadToc(): Promise<void> {
// We use the attempt summary to build the TOC because it contains all the questions.
protected loadNavigation(): Promise<void> {
// We use the attempt summary to build the navigation because it contains all the questions.
return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline).then((questions) => {
this.toc = questions;
this.navigation = questions;
});
}
@ -512,7 +519,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
this.attemptAccessInfo = info;
this.attempt = attempt;
return this.loadToc();
return this.loadNavigation();
}).then(() => {
if (this.attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !this.attempt.finishedOffline) {
// Attempt not overdue and not finished in offline, load page.