864 lines
31 KiB
TypeScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, ElementRef } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { IonContent } from '@ionic/angular';
import { CoreError } from '@classes/errors/error';
import { CanLeave } from '@guards/can-leave';
import { CoreNetwork } from '@services/network';
import { CoreNavigator } from '@services/navigator';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreUrl } from '@singletons/url';
import { CoreObject } from '@singletons/object';
import { CoreWSExternalFile } from '@services/ws';
import { ModalController, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import {
AddonModLesson,
AddonModLessonEOLPageDataEntry,
AddonModLessonFinishRetakeResponse,
AddonModLessonGetAccessInformationWSResponse,
AddonModLessonGetPageDataWSResponse,
AddonModLessonGetPagesPageWSData,
AddonModLessonLaunchAttemptWSResponse,
AddonModLessonLessonWSData,
AddonModLessonMessageWSData,
AddonModLessonPageWSData,
AddonModLessonPossibleJumps,
AddonModLessonProcessPageOptions,
AddonModLessonProcessPageResponse,
} from '../../services/lesson';
import {
AddonModLessonActivityLink,
AddonModLessonHelper,
AddonModLessonPageButton,
AddonModLessonQuestion,
} from '../../services/lesson-helper';
import { AddonModLessonOffline } from '../../services/lesson-offline';
import { AddonModLessonSync } from '../../services/lesson-sync';
import { CoreFormFields, CoreForms } from '@singletons/form';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { ADDON_MOD_LESSON_COMPONENT, AddonModLessonJumpTo } from '../../constants';
import { CoreModals } from '@services/overlays/modals';
import { CorePromiseUtils } from '@singletons/promise-utils';
import { CoreWSError } from '@classes/errors/wserror';
import { CoreDom } from '@singletons/dom';
import { CoreAlerts } from '@services/overlays/alerts';
/**
* Page that allows attempting and reviewing a lesson.
*/
@Component({
selector: 'page-addon-mod-lesson-player',
templateUrl: 'player.html',
styleUrl: 'player.scss',
})
export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
@ViewChild(IonContent) content?: IonContent;
@ViewChild('questionFormEl') formElement?: ElementRef;
component = ADDON_MOD_LESSON_COMPONENT;
readonly LESSON_EOL = AddonModLessonJumpTo.EOL;
questionForm?: FormGroup; // The FormGroup for question pages.
title?: string; // The page title.
lesson?: AddonModLessonLessonWSData; // The lesson object.
currentPage?: number; // Current page being viewed.
review?: boolean; // Whether the user is reviewing.
messages: AddonModLessonMessageWSData[] = []; // Messages to display to the user.
canManage?: boolean; // Whether the user can manage the lesson.
retake?: number; // Current retake number.
showRetake?: boolean; // Whether the retake number needs to be displayed.
lessonWidth?: string; // Width of the lesson (if slideshow mode).
lessonHeight?: string; // Height of the lesson (if slideshow mode).
endTime?: number; // End time of the lesson if it's timed.
pageData?: AddonModLessonGetPageDataWSResponse; // Current page data.
pageContent?: string; // Current page contents.
pageButtons?: AddonModLessonPageButton[]; // List of buttons of the current page.
question?: AddonModLessonQuestion; // Question of the current page (if it's a question page).
eolData?: Record<string, AddonModLessonEOLPageDataEntry>; // Data for EOL page (if current page is EOL).
processData?: AddonModLessonProcessPageResponse; // Data to display after processing a page.
processDataButtons: ProcessDataButton[] = []; // Buttons to display after processing a page.
loaded?: boolean; // Whether data has been loaded.
displayMenu?: boolean; // Whether the lesson menu should be displayed.
originalData?: CoreFormFields; // Original question data. It is used to check if data has changed.
reviewPageId?: number; // Page to open if the user wants to review the attempt.
courseId!: number; // The course ID the lesson belongs to.
lessonPages?: AddonModLessonPageWSData[]; // Lesson pages (for the lesson menu).
loadingMenu?: boolean; // Whether the lesson menu is being loaded.
mediaFile?: CoreWSExternalFile; // Media file of the lesson.
activityLink?: AddonModLessonActivityLink; // Next activity link data.
cmId!: number; // Course module ID.
protected password?: string; // Lesson password (if any).
protected forceLeave = false; // If true, don't perform any check when leaving the view.
protected offline?: boolean; // Whether we are in offline mode.
protected accessInfo?: AddonModLessonGetAccessInformationWSResponse; // Lesson access info.
protected jumps?: AddonModLessonPossibleJumps; // All possible jumps.
protected firstPageLoaded?: boolean; // Whether the first page has been loaded.
protected retakeToReview?: number; // Retake to review.
protected menuShown = false; // Whether menu is shown.
constructor(
protected changeDetector: ChangeDetectorRef,
protected formBuilder: FormBuilder,
) {
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
try {
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.password = CoreNavigator.getRouteParam('password');
this.review = !!CoreNavigator.getRouteBooleanParam('review');
this.currentPage = CoreNavigator.getRouteNumberParam('pageId');
this.retakeToReview = CoreNavigator.getRouteNumberParam('retake');
} catch (error) {
CoreAlerts.showError(error);
CoreNavigator.back();
return;
}
try {
// Fetch the Lesson data.
const success = await this.fetchLessonData();
if (success) {
// Review data loaded or new retake started, remove any retake being finished in sync.
AddonModLessonSync.deleteRetakeFinishedInSync(this.lesson!.id);
}
} finally {
this.loaded = true;
}
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
if (this.lesson) {
// Unblock the lesson so it can be synced.
CoreSync.unblockOperation(this.component, this.lesson.id);
}
}
/**
* Check if we can leave the page or not.
*
* @returns Resolved if we can leave it, rejected if not.
*/
async canLeave(): Promise<boolean> {
if (this.forceLeave || !this.questionForm) {
return true;
}
if (this.question && !this.eolData && !this.processData && this.originalData) {
// Question shown. Check if there is any change.
if (!CoreObject.basicLeftCompare(this.questionForm.getRawValue(), this.originalData, 3)) {
await CoreAlerts.confirmLeaveWithChanges();
}
}
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
return true;
}
/**
* Runs when the page is about to leave and no longer be the active page.
*/
ionViewWillLeave(): void {
if (this.menuShown) {
ModalController.dismiss();
}
}
/**
* A button was clicked.
*
* @param data Button data.
*/
buttonClicked(data: Record<string, string>): void {
this.processPage(data);
}
/**
* Call a function and go offline if allowed and the call fails.
*
* @param func Function to call.
* @param options Options passed to the function.
* @returns Promise resolved in success, rejected otherwise.
*/
protected async callFunction<T>(func: () => Promise<T>, options: CommonOptions): Promise<T> {
try {
return await func();
} catch (error) {
if (this.offline || this.review || !AddonModLesson.isLessonOffline(this.lesson!)) {
// Already offline or not allowed.
throw error;
}
if (CoreWSError.isWebServiceError(error)) {
// WebService returned an error, cannot perform the action.
throw error;
}
// Go offline and retry.
this.offline = true;
// Get the possible jumps now.
this.jumps = await AddonModLesson.getPagesPossibleJumps(this.lesson!.id, {
cmId: this.cmId,
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
});
// Call the function again with offline mode and the new jumps.
options.readingStrategy = CoreSitesReadingStrategy.PREFER_CACHE;
options.jumps = this.jumps;
options.offline = true;
return func();
}
}
/**
* Change the page from menu or when continuing from a feedback page.
*
* @param pageId Page to load.
* @param ignoreCurrent If true, allow loading current page.
* @returns Promise resolved when done.
*/
async changePage(pageId: number, ignoreCurrent?: boolean): Promise<void> {
if (!ignoreCurrent && !this.eolData && this.currentPage == pageId) {
// Page already loaded, stop.
return;
}
this.loaded = true;
this.messages = [];
try {
await this.loadPage(pageId);
} catch (error) {
CoreAlerts.showError(error, { default: 'Error loading page' });
} finally {
this.loaded = true;
}
}
/**
* Get the lesson data and load the page.
*
* @returns Promise resolved with true if success, resolved with false otherwise.
*/
protected async fetchLessonData(): Promise<boolean> {
try {
this.lesson = await AddonModLesson.getLesson(this.courseId, this.cmId);
this.title = this.lesson.name; // Temporary title.
// Block the lesson so it cannot be synced.
CoreSync.blockOperation(this.component, this.lesson.id);
// Wait for any ongoing sync to finish. We won't sync a lesson while it's being played.
await AddonModLessonSync.waitForSync(this.lesson.id);
// If lesson has offline data already, use offline mode.
this.offline = await AddonModLessonOffline.hasOfflineData(this.lesson.id);
if (!this.offline && !CoreNetwork.isOnline() && AddonModLesson.isLessonOffline(this.lesson) && !this.review) {
// Lesson doesn't have offline data, but it allows offline and the device is offline. Use offline mode.
this.offline = true;
}
const lessonId = this.lesson.id;
const options = {
cmId: this.cmId,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
};
this.accessInfo = await this.callFunction<AddonModLessonGetAccessInformationWSResponse>(
() => AddonModLesson.getAccessInformation(lessonId, options),
options,
);
const promises: Promise<void>[] = [];
this.canManage = this.accessInfo.canmanage;
this.retake = this.accessInfo.attemptscount;
this.showRetake = !this.currentPage && this.retake > 0; // Only show it in first page if it isn't the first retake.
if (this.accessInfo.preventaccessreasons.length) {
// If it's a password protected lesson and we have the password, allow playing it.
const preventReason = AddonModLesson.getPreventAccessReason(this.accessInfo, !!this.password, this.review);
if (preventReason) {
// Lesson cannot be played, show message and go back.
throw new CoreError(preventReason.message);
}
}
if (this.review && this.retakeToReview != this.accessInfo.attemptscount - 1) {
// Reviewing a retake that isn't the last one. Error.
throw new CoreError(Translate.instant('addon.mod_lesson.errorreviewretakenotlast'));
}
if (this.password) {
// Lesson uses password, get the whole lesson object.
const options = {
password: this.password,
cmId: this.cmId,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
};
promises.push(this.callFunction<AddonModLessonLessonWSData>(
() => AddonModLesson.getLessonWithPassword(lessonId, options),
options,
).then((lesson) => {
this.lesson = lesson;
return;
}));
}
if (this.offline) {
// Offline mode, get the list of possible jumps to allow navigation.
promises.push(AddonModLesson.getPagesPossibleJumps(this.lesson.id, {
cmId: this.cmId,
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
}).then((jumpList) => {
this.jumps = jumpList;
return;
}));
}
await Promise.all(promises);
this.mediaFile = this.lesson.mediafiles?.[0];
this.lessonWidth = this.lesson.slideshow ? CoreDom.formatSizeUnits(this.lesson.mediawidth!) : '';
this.lessonHeight = this.lesson.slideshow ? CoreDom.formatSizeUnits(this.lesson.mediaheight!) : '';
await this.launchRetake(this.currentPage);
return true;
} catch (error) {
if (this.review && this.retakeToReview && CoreWSError.isWebServiceError(error)) {
// The user cannot review the retake. Unmark the retake as being finished in sync.
await AddonModLessonSync.deleteRetakeFinishedInSync(this.lesson!.id);
}
CoreAlerts.showError(error, { default: Translate.instant('core.course.errorgetmodule') });
this.forceLeave = true;
CoreNavigator.back();
return false;
}
}
/**
* Finish the retake.
*
* @param outOfTime Whether the retake is finished because the user ran out of time.
* @returns Promise resolved when done.
*/
protected async finishRetake(outOfTime?: boolean): Promise<void> {
if (!this.lesson) {
return;
}
const lesson = this.lesson;
this.messages = [];
if (this.offline && CoreNetwork.isOnline()) {
// Offline mode but the app is online. Try to sync the data.
const result = await CorePromiseUtils.ignoreErrors(
AddonModLessonSync.syncLesson(lesson.id, true, true),
);
if (result?.warnings?.length) {
// Some data was deleted. Check if the retake has changed.
const info = await AddonModLesson.getAccessInformation(lesson.id, {
cmId: this.cmId,
});
if (info.attemptscount != this.accessInfo!.attemptscount) {
// The retake has changed. Leave the view and show the error.
this.forceLeave = true;
CoreNavigator.back();
throw new CoreError(result.warnings[0]);
}
// Retake hasn't changed, show the warning and finish the retake in offline.
CoreAlerts.show({ message: result.warnings[0] });
}
this.offline = false;
}
// Now finish the retake.
const options = {
password: this.password,
outOfTime,
review: this.review,
offline: this.offline,
accessInfo: this.accessInfo,
};
const data = await this.callFunction<AddonModLessonFinishRetakeResponse>(
() => AddonModLesson.finishRetake(lesson, this.courseId, options),
options,
);
this.title = lesson.name;
this.eolData = data.data;
this.messages = this.messages.concat(data.messages);
this.processData = undefined;
this.endTime = undefined;
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'lesson' });
// Format activity link if present.
if (this.eolData.activitylink) {
this.activityLink = AddonModLessonHelper.formatActivityLink(<string> this.eolData.activitylink.value);
} else {
this.activityLink = undefined;
}
// Format review lesson if present.
if (this.eolData.reviewlesson) {
const params = CoreUrl.extractUrlParams(<string> this.eolData.reviewlesson.value);
if (!params || !params.pageid) {
// No pageid in the URL, the user cannot review (probably didn't answer any question).
delete this.eolData.reviewlesson;
} else {
this.reviewPageId = Number(params.pageid);
}
}
this.logPageLoaded(AddonModLessonJumpTo.EOL, Translate.instant('addon.mod_lesson.congratulations'));
}
/**
* Jump to a certain page after performing an action.
*
* @param pageId The page to load.
* @returns Promise resolved when done.
*/
protected async jumpToPage(pageId: number): Promise<void> {
if (pageId === 0) {
// Not a valid page, return to entry view.
// This happens, for example, when the user clicks to go to previous page and there is no previous page.
this.forceLeave = true;
CoreNavigator.back();
return;
} else if (pageId == AddonModLessonJumpTo.EOL) {
// End of lesson reached.
return this.finishRetake();
}
// Load new page.
this.messages = [];
return this.loadPage(pageId);
}
/**
* Start or continue a retake.
*
* @param pageId The page to load.
* @returns Promise resolved when done.
*/
protected async launchRetake(pageId?: number): Promise<void> {
let data: AddonModLessonLaunchAttemptWSResponse | undefined;
if (this.review) {
// Review mode, no need to launch the retake.
} else if (!this.offline) {
// Not in offline mode, launch the retake.
data = await AddonModLesson.launchRetake(this.lesson!.id, this.password, pageId);
} else {
// Check if there is a finished offline retake.
const finished = await AddonModLessonOffline.hasFinishedRetake(this.lesson!.id);
if (finished) {
// Always show EOL page.
pageId = AddonModLessonJumpTo.EOL;
}
}
this.currentPage = pageId || this.accessInfo!.firstpageid;
this.messages = data?.messages || [];
if (this.lesson!.timelimit && !this.accessInfo!.canmanage) {
// Get the last lesson timer.
const timers = await AddonModLesson.getTimers(this.lesson!.id, {
cmId: this.cmId,
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
});
this.endTime = timers[timers.length - 1].starttime + this.lesson!.timelimit;
}
return this.loadPage(this.currentPage);
}
/**
* Load the lesson menu.
*
* @returns Promise resolved when done.
*/
protected async loadMenu(): Promise<void> {
if (this.loadingMenu || !this.lesson) {
// Already loading.
return;
}
try {
this.loadingMenu = true;
const lessonId = this.lesson.id;
const options = {
password: this.password,
cmId: this.cmId,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
};
const pages = await this.callFunction<AddonModLessonGetPagesPageWSData[]>(
() => AddonModLesson.getPages(lessonId, options),
options,
);
this.lessonPages = pages.map((entry) => entry.page);
} catch (error) {
CoreAlerts.showError(error, { default: 'Error loading menu.' });
} finally {
this.loadingMenu = false;
}
}
/**
* Load a certain page.
*
* @param pageId The page to load.
* @returns Promise resolved when done.
*/
protected async loadPage(pageId: number): Promise<void> {
if (pageId == AddonModLessonJumpTo.EOL) {
// End of lesson reached.
return this.finishRetake();
} else if (!this.lesson) {
return;
}
const lesson = this.lesson;
const options = {
password: this.password,
review: this.review,
includeContents: true,
cmId: this.cmId,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
accessInfo: this.accessInfo,
jumps: this.jumps,
includeOfflineData: true,
};
const data = await this.callFunction<AddonModLessonGetPageDataWSResponse>(
() => AddonModLesson.getPageData(lesson, pageId, options),
options,
);
if (data.newpageid == AddonModLessonJumpTo.EOL) {
// End of lesson reached.
return this.finishRetake();
}
this.pageData = data;
this.title = data.page!.title;
this.pageContent = AddonModLessonHelper.getPageContentsFromPageData(data);
this.loaded = true;
this.currentPage = pageId;
this.messages = this.messages.concat(data.messages);
// Page loaded, hide EOL and feedback data if shown.
this.eolData = this.processData = undefined;
if (AddonModLesson.isQuestionPage(data.page!.type)) {
// Create an empty FormGroup without controls, they will be added in getQuestionFromPageData.
this.questionForm = this.formBuilder.group({});
this.pageButtons = [];
this.question = AddonModLessonHelper.getQuestionFromPageData(this.questionForm, data);
this.originalData = this.questionForm.getRawValue(); // Use getRawValue to include disabled values.
} else {
this.pageButtons = AddonModLessonHelper.getPageButtonsFromHtml(data.pagecontent || '');
this.question = undefined;
this.originalData = undefined;
}
// Don't display the navigation menu in review mode, using them displays errors.
if (data.displaymenu && !this.displayMenu && !this.review) {
// Load the menu.
this.loadMenu();
}
this.displayMenu = !this.review && !!data.displaymenu;
if (!this.firstPageLoaded) {
this.firstPageLoaded = true;
} else {
this.showRetake = false;
}
this.logPageLoaded(pageId, data.page?.title ?? '');
}
/**
* Log page loaded.
*
* @param pageId Page ID.
*/
protected logPageLoaded(pageId: number, title: string): void {
if (!this.lesson) {
return;
}
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: 'mod_lesson_get_page_data',
name: this.lesson.name + ': ' + title,
data: { id: this.lesson.id, pageid: pageId, category: 'lesson' },
url: `/mod/lesson/view.php?id=${this.lesson.id}&pageid=${pageId}`,
});
}
/**
* Log continue page.
*/
protected logContinuePageLoaded(): void {
if (!this.lesson) {
return;
}
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: 'mod_lesson_process_page',
name: this.lesson.name + ': ' + Translate.instant('addon.mod_lesson.continue'),
data: { id: this.lesson.id, category: 'lesson' },
url: '/mod/lesson/continue.php',
});
}
/**
* Process a page, sending some data.
*
* @param data The data to send.
* @param formSubmitted Whether a form was submitted.
* @returns Promise resolved when done.
*/
protected async processPage(data: CoreFormFields, formSubmitted?: boolean): Promise<void> {
if (!this.lesson || !this.pageData) {
return;
}
this.loaded = false;
const lesson = this.lesson;
const pageData = this.pageData;
const options: AddonModLessonProcessPageOptions = {
password: this.password,
review: this.review,
offline: this.offline,
accessInfo: this.accessInfo,
jumps: this.jumps,
};
try {
const result = await this.callFunction<AddonModLessonProcessPageResponse>(
() => AddonModLesson.processPage(
lesson,
this.courseId,
pageData,
data,
options,
),
options,
);
if (formSubmitted) {
CoreForms.triggerFormSubmittedEvent(
this.formElement,
result.sent,
CoreSites.getCurrentSiteId(),
);
}
if (!this.offline && !this.review && AddonModLesson.isLessonOffline(lesson)) {
// Lesson allows offline and the user changed some data in server. Update cached data.
const retake = this.accessInfo!.attemptscount;
const options = {
cmId: this.cmId,
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
};
// Update in background the list of content pages viewed or question attempts.
if (AddonModLesson.isQuestionPage(this.pageData?.page?.type || -1)) {
AddonModLesson.getQuestionsAttemptsOnline(lesson.id, retake, options);
} else {
AddonModLesson.getContentPagesViewedOnline(lesson.id, retake, options);
}
}
if (result.nodefaultresponse || result.inmediatejump) {
// Don't display feedback or force a redirect to a new page. Load the new page.
return await this.jumpToPage(result.newpageid);
}
// Not inmediate jump, show the feedback.
result.feedback = AddonModLessonHelper.removeQuestionFromFeedback(result.feedback);
this.messages = result.messages;
this.processData = result;
this.processDataButtons = [];
if (lesson.review && !result.correctanswer && !result.noanswer && !result.isessayquestion &&
!result.maxattemptsreached && !result.reviewmode) {
// User can try again, show button to do so.
this.processDataButtons.push({
label: 'addon.mod_lesson.reviewquestionback',
pageId: this.currentPage!,
});
}
// Button to continue.
if (lesson.review && !result.correctanswer && !result.noanswer && !result.isessayquestion &&
!result.maxattemptsreached) {
/* If both the "Yes, I'd like to try again" and "No, I just want to go on to the next question" point to the
same page then don't show the "No, I just want to go on to the next question" button. It's confusing. */
if (pageData.page?.id != result.newpageid) {
// Button to continue the lesson (the page to go is configured by the teacher).
this.processDataButtons.push({
label: 'addon.mod_lesson.reviewquestioncontinue',
pageId: result.newpageid,
});
}
} else {
this.processDataButtons.push({
label: 'addon.mod_lesson.continue',
pageId: result.newpageid,
});
}
this.logContinuePageLoaded();
} catch (error) {
CoreAlerts.showError(error, { default: 'Error processing page' });
} finally {
this.loaded = true;
}
}
/**
* Review the lesson.
*
* @param pageId Page to load.
*/
async reviewLesson(pageId: number): Promise<void> {
this.loaded = false;
this.review = true;
this.offline = false; // Don't allow offline mode in review.
try {
await this.loadPage(pageId);
} catch (error) {
CoreAlerts.showError(error, { default: 'Error loading page' });
} finally {
this.loaded = true;
}
}
/**
* Submit a question.
*
* @param e Event.
*/
submitQuestion(e: Event): void {
e.preventDefault();
e.stopPropagation();
this.loaded = false;
// Use getRawValue to include disabled values.
const data = AddonModLessonHelper.prepareQuestionData(this.question!, this.questionForm!.getRawValue());
this.processPage(data, true).finally(() => {
this.loaded = true;
});
}
/**
* Time up.
*/
async timeUp(): Promise<void> {
// Time up called, hide the timer.
this.endTime = undefined;
this.loaded = false;
try {
await this.finishRetake(true);
} catch (error) {
CoreAlerts.showError(error, { default: 'Error finishing attempt' });
} finally {
this.loaded = true;
}
}
/**
* Show the navigation modal.
*
* @returns Promise resolved when done.
*/
async showMenu(): Promise<void> {
this.menuShown = true;
const { AddonModLessonMenuModalPage } = await import('../../components/menu-modal/menu-modal');
await CoreModals.openSideModal({
component: AddonModLessonMenuModalPage,
componentProps: {
pageInstance: this,
},
});
this.menuShown = false;
}
}
/**
* Common options for functions called using callFunction.
*/
type CommonOptions = CoreSitesCommonWSOptions & {
jumps?: AddonModLessonPossibleJumps;
offline?: boolean;
};
/**
* Button displayed after processing a page.
*/
type ProcessDataButton = {
label: string;
pageId: number;
};