forked from CIT/Vmeda.Online
		
	Merge pull request #3536 from crazyserver/linting
[4.2] Linting and small improvements release
This commit is contained in:
		
						commit
						a812ee3625
					
				@ -27,7 +27,7 @@ import {
 | 
			
		||||
import { AddonCalendarOffline } from './calendar-offline';
 | 
			
		||||
import { AddonCalendarHelper } from './calendar-helper';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import moment from 'moment-timezone';
 | 
			
		||||
 | 
			
		||||
@ -301,13 +301,11 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
 | 
			
		||||
 | 
			
		||||
export const AddonCalendarSync = makeSingleton(AddonCalendarSyncProvider);
 | 
			
		||||
 | 
			
		||||
export type AddonCalendarSyncEvents = {
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
export type AddonCalendarSyncEvents = CoreSyncResult & {
 | 
			
		||||
    events: AddonCalendarEvent[];
 | 
			
		||||
    offlineIdMap: Record<number, number>; // Map offline ID with online ID for created events.
 | 
			
		||||
    deleted: number[];
 | 
			
		||||
    toinvalidate: AddonCalendarSyncInvalidateEvent[];
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
    source?: string; // Added on pages.
 | 
			
		||||
    moment?: moment.Moment; // Added on day page.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -270,9 +270,10 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (this.userId) {
 | 
			
		||||
                    const userId = this.userId;
 | 
			
		||||
                    // Get the member info. Invalidate first to make sure we get the latest status.
 | 
			
		||||
                    promises.push(AddonMessages.invalidateMemberInfo(this.userId).then(async () => {
 | 
			
		||||
                        this.otherMember = await AddonMessages.getMemberInfo(this.userId!);
 | 
			
		||||
                        this.otherMember = await AddonMessages.getMemberInfo(userId);
 | 
			
		||||
 | 
			
		||||
                        if (!exists && this.otherMember) {
 | 
			
		||||
                            this.conversationImage = this.otherMember.profileimageurl;
 | 
			
		||||
@ -288,6 +289,8 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
 | 
			
		||||
 | 
			
		||||
            } else {
 | 
			
		||||
                if (this.userId) {
 | 
			
		||||
                    const userId = this.userId;
 | 
			
		||||
 | 
			
		||||
                    // Fake the user member info.
 | 
			
		||||
                    promises.push(CoreUser.getProfile(this.userId).then(async (user) => {
 | 
			
		||||
                        this.otherMember = {
 | 
			
		||||
@ -305,8 +308,8 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
 | 
			
		||||
                            canmessage: true,
 | 
			
		||||
                            requirescontact: false,
 | 
			
		||||
                        };
 | 
			
		||||
                        this.otherMember.isblocked = await AddonMessages.isBlocked(this.userId!);
 | 
			
		||||
                        this.otherMember.iscontact = await AddonMessages.isContact(this.userId!);
 | 
			
		||||
                        this.otherMember.isblocked = await AddonMessages.isBlocked(userId);
 | 
			
		||||
                        this.otherMember.iscontact = await AddonMessages.isContact(userId);
 | 
			
		||||
                        this.blockIcon = this.otherMember.isblocked ? 'fas-user-check' : 'fas-user-lock';
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
@ -80,17 +80,17 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
 | 
			
		||||
            AddonMessagesProvider.NEW_MESSAGE_EVENT,
 | 
			
		||||
            (data) => {
 | 
			
		||||
                if (data.userId && this.discussions) {
 | 
			
		||||
                    const discussion = this.discussions.find((disc) => disc.message!.user == data.userId);
 | 
			
		||||
                    const discussion = this.discussions.find((disc) => disc.message?.user === data.userId);
 | 
			
		||||
 | 
			
		||||
                    if (discussion === undefined) {
 | 
			
		||||
                        this.loaded = false;
 | 
			
		||||
                        this.refreshData().finally(() => {
 | 
			
		||||
                            this.loaded = true;
 | 
			
		||||
                        });
 | 
			
		||||
                    } else {
 | 
			
		||||
                    // An existing discussion has a new message, update the last message.
 | 
			
		||||
                        discussion.message!.message = data.message;
 | 
			
		||||
                        discussion.message!.timecreated = data.timecreated;
 | 
			
		||||
                    } else if (discussion.message) {
 | 
			
		||||
                        // An existing discussion has a new message, update the last message.
 | 
			
		||||
                        discussion.message.message = data.message;
 | 
			
		||||
                        discussion.message.timecreated = data.timecreated;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
@ -102,10 +102,10 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
 | 
			
		||||
            AddonMessagesProvider.READ_CHANGED_EVENT,
 | 
			
		||||
            (data) => {
 | 
			
		||||
                if (data.userId && this.discussions) {
 | 
			
		||||
                    const discussion = this.discussions.find((disc) => disc.message!.user == data.userId);
 | 
			
		||||
                    const discussion = this.discussions.find((disc) => disc.message?.user === data.userId);
 | 
			
		||||
 | 
			
		||||
                    if (discussion !== undefined) {
 | 
			
		||||
                    // A discussion has been read reset counter.
 | 
			
		||||
                        // A discussion has been read reset counter.
 | 
			
		||||
                        discussion.unread = false;
 | 
			
		||||
 | 
			
		||||
                        // Conversations changed, invalidate them and refresh unread counts.
 | 
			
		||||
@ -138,7 +138,7 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component loaded.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.route.queryParams.subscribe(async (params) => {
 | 
			
		||||
@ -150,9 +150,9 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        await this.fetchData();
 | 
			
		||||
 | 
			
		||||
        if (!this.discussionUserId && this.discussions.length > 0 && CoreScreen.isTablet) {
 | 
			
		||||
        if (!this.discussionUserId && this.discussions.length > 0 && CoreScreen.isTablet && this.discussions[0].message) {
 | 
			
		||||
            // Take first and load it.
 | 
			
		||||
            await this.gotoDiscussion(this.discussions[0].message!.user);
 | 
			
		||||
            await this.gotoDiscussion(this.discussions[0].message.user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Treat deep link now that the conversation route has been loaded if needed.
 | 
			
		||||
@ -287,7 +287,7 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component destroyed.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.newMessagesObserver?.off();
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
import { Component, Optional, OnDestroy, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreSite } from '@classes/site';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
@ -314,11 +315,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result?: AddonModAssignSyncResult): boolean {
 | 
			
		||||
        if (!result) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModAssignSyncResult): boolean {
 | 
			
		||||
        if (result.updated) {
 | 
			
		||||
            this.submissionComponent?.invalidateAndRefresh(false);
 | 
			
		||||
        }
 | 
			
		||||
@ -384,9 +381,9 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(): Promise<AddonModAssignSyncResult | void> {
 | 
			
		||||
    protected async sync(): Promise<AddonModAssignSyncResult> {
 | 
			
		||||
        if (!this.assign) {
 | 
			
		||||
            return;
 | 
			
		||||
            throw new CoreError('Cannot sync without a assign.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return AddonModAssignSync.syncAssign(this.assign.id);
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ import {
 | 
			
		||||
    AddonModAssignSubmissionsDBRecordFormatted,
 | 
			
		||||
    AddonModAssignSubmissionsGradingDBRecordFormatted,
 | 
			
		||||
} from './assign-offline';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
@ -530,9 +530,7 @@ export const AddonModAssignSync = makeSingleton(AddonModAssignSyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a assign sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModAssignSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
export type AddonModAssignSyncResult = CoreSyncResult & {
 | 
			
		||||
    courseId?: number; // Course the assign belongs to (if known).
 | 
			
		||||
    gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, Optional, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
@ -454,22 +455,14 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected sync(): Promise<AddonModChoiceSyncResult> {
 | 
			
		||||
        return AddonModChoiceSync.syncChoice(this.choice!.id, this.userId);
 | 
			
		||||
    }
 | 
			
		||||
        if (!this.choice) {
 | 
			
		||||
            throw new CoreError('Cannot sync without a choice.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns Whether it succeed or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModChoiceSyncResult): boolean {
 | 
			
		||||
        return result.updated;
 | 
			
		||||
        return AddonModChoiceSync.syncChoice(this.choice.id, this.userId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
 | 
			
		||||
import { CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
@ -217,10 +218,7 @@ export const AddonModChoiceSync = makeSingleton(AddonModChoiceSyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a choice sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModChoiceSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
};
 | 
			
		||||
export type AddonModChoiceSyncResult = CoreSyncResult;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNCED event.
 | 
			
		||||
 | 
			
		||||
@ -77,15 +77,14 @@ export class AddonModChoiceProvider {
 | 
			
		||||
        choiceId: number,
 | 
			
		||||
        name: string,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        responses?: number[],
 | 
			
		||||
        responses: number[] = [],
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<boolean> {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
        responses = responses || [];
 | 
			
		||||
 | 
			
		||||
        // Convenience function to store a message to be synchronized later.
 | 
			
		||||
        const storeOffline = async (): Promise<boolean> => {
 | 
			
		||||
            await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses!, true, siteId);
 | 
			
		||||
            await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses, true, siteId);
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -519,24 +519,12 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected sync(): Promise<AddonModDataSyncResult> {
 | 
			
		||||
        return AddonModDataPrefetchHandler.sync(this.module, this.courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns If suceed or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModDataSyncResult): boolean {
 | 
			
		||||
        return result.updated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ import { CoreRatingSync } from '@features/rating/services/rating-sync';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreFileEntry } from '@services/file-helper';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate, makeSingleton } from '@singletons';
 | 
			
		||||
@ -477,10 +477,7 @@ export type AddonModDataSyncEntryResult = {
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a database sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModDataSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
};
 | 
			
		||||
export type AddonModDataSyncResult = CoreSyncResult;
 | 
			
		||||
 | 
			
		||||
export type AddonModDataAutoSyncData = {
 | 
			
		||||
    dataId: number;
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, Input, Optional, ViewChild, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreTabsComponent } from '@components/tabs/tabs';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
@ -477,14 +478,11 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected sync(): Promise<AddonModFeedbackSyncResult> {
 | 
			
		||||
        return AddonModFeedbackSync.syncFeedback(this.feedback!.id);
 | 
			
		||||
    }
 | 
			
		||||
        if (!this.feedback) {
 | 
			
		||||
            throw new CoreError('Cannot sync without a feedback.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModFeedbackSyncResult): boolean {
 | 
			
		||||
        return result.updated;
 | 
			
		||||
        return AddonModFeedbackSync.syncFeedback(this.feedback.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/c
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
@ -292,10 +292,7 @@ export const AddonModFeedbackSync = makeSingleton(AddonModFeedbackSyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a feedback sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModFeedbackSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
};
 | 
			
		||||
export type AddonModFeedbackSyncResult = CoreSyncResult;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNCED event.
 | 
			
		||||
 | 
			
		||||
@ -500,24 +500,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected sync(): Promise<AddonModForumSyncResult> {
 | 
			
		||||
        return AddonModForumPrefetchHandler.sync(this.module, this.courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns Whether it succeed or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModForumSyncResult): boolean {
 | 
			
		||||
        return result.updated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compares sync event data with current data to check if refresh content is needed.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -62,7 +62,7 @@ export class AddonModForumHelperProvider {
 | 
			
		||||
        message: string,
 | 
			
		||||
        attachments?: CoreFileEntry[],
 | 
			
		||||
        options?: AddonModForumDiscussionOptions,
 | 
			
		||||
        groupIds?: number[],
 | 
			
		||||
        groupIds: number[] = [],
 | 
			
		||||
        timeCreated?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<number[] | null> {
 | 
			
		||||
@ -76,7 +76,7 @@ export class AddonModForumHelperProvider {
 | 
			
		||||
        // Convenience function to store a message to be synchronized later.
 | 
			
		||||
        const storeOffline = async (): Promise<void> => {
 | 
			
		||||
            // Multiple groups, the discussion is being posted to all groups.
 | 
			
		||||
            const groupId = groupIds!.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds![0];
 | 
			
		||||
            const groupId = groupIds.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds[0];
 | 
			
		||||
 | 
			
		||||
            if (offlineAttachments && options) {
 | 
			
		||||
                options.attachmentsid = offlineAttachments;
 | 
			
		||||
@ -182,7 +182,7 @@ export class AddonModForumHelperProvider {
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @returns Promise resolved with the object converted to Online.
 | 
			
		||||
     */
 | 
			
		||||
    convertOfflineReplyToOnline(offlineReply: AddonModForumOfflineReply, siteId?: string): Promise<AddonModForumPost> {
 | 
			
		||||
    async convertOfflineReplyToOnline(offlineReply: AddonModForumOfflineReply, siteId?: string): Promise<AddonModForumPost> {
 | 
			
		||||
        const reply: AddonModForumPost = {
 | 
			
		||||
            id: -offlineReply.timecreated,
 | 
			
		||||
            discussionid: offlineReply.discussionid,
 | 
			
		||||
@ -236,11 +236,11 @@ export class AddonModForumHelperProvider {
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return Promise.all(promises).then(() => {
 | 
			
		||||
            reply.attachment = reply.attachments!.length > 0 ? 1 : 0;
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
            return reply;
 | 
			
		||||
        });
 | 
			
		||||
        reply.attachment = reply.attachments?.length ? 1 : 0;
 | 
			
		||||
 | 
			
		||||
        return reply;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ import { CoreRatingSync } from '@features/rating/services/rating-sync';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
@ -221,7 +221,7 @@ export class AddonModForumSyncProvider extends CoreCourseActivitySyncBaseProvide
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Sync offline logs.
 | 
			
		||||
        const syncDiscussions = async (): Promise<{ warnings: string[]; updated: boolean }> => {
 | 
			
		||||
        const syncDiscussions = async (): Promise<AddonModForumSyncResult> => {
 | 
			
		||||
            await CoreUtils.ignoreErrors(
 | 
			
		||||
                CoreCourseLogHelper.syncActivity(AddonModForumProvider.COMPONENT, forumId, siteId),
 | 
			
		||||
            );
 | 
			
		||||
@ -643,10 +643,7 @@ export const AddonModForumSync = makeSingleton(AddonModForumSyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Result of forum sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModForumSyncResult = {
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
};
 | 
			
		||||
export type AddonModForumSyncResult = CoreSyncResult;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNCED event.
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } fro
 | 
			
		||||
import { CoreUser } from '@features/user/services/user';
 | 
			
		||||
import { CoreGroups, CoreGroupsProvider } from '@services/groups';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { AddonModForumSync } from '../forum-sync';
 | 
			
		||||
import { AddonModForumSync, AddonModForumSyncResult } from '../forum-sync';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreCourses } from '@features/courses/services/courses';
 | 
			
		||||
 | 
			
		||||
@ -341,11 +341,3 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonModForumPrefetchHandler = makeSingleton(AddonModForumPrefetchHandlerService);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a forum sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModForumSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -215,24 +215,12 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected sync(): Promise<AddonModGlossarySyncResult> {
 | 
			
		||||
        return AddonModGlossaryPrefetchHandler.sync(this.module, this.courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns Whether it succeed or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModGlossarySyncResult): boolean {
 | 
			
		||||
        return result.updated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compares sync event data with current data to check if refresh content is needed.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreRatingSync } from '@features/rating/services/rating-sync';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
@ -344,10 +344,7 @@ export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a glossary sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModGlossarySyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
};
 | 
			
		||||
export type AddonModGlossarySyncResult = CoreSyncResult;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNCED event.
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
 | 
			
		||||
import { CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreXAPIOffline } from '@features/xapi/services/offline';
 | 
			
		||||
import { CoreXAPI } from '@features/xapi/services/xapi';
 | 
			
		||||
@ -197,10 +198,7 @@ export const AddonModH5PActivitySync = makeSingleton(AddonModH5PActivitySyncProv
 | 
			
		||||
/**
 | 
			
		||||
 * Sync result.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivitySyncResult = {
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
};
 | 
			
		||||
export type AddonModH5PActivitySyncResult = CoreSyncResult;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNC event.
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,7 @@ import {
 | 
			
		||||
} from '../../services/lesson-sync';
 | 
			
		||||
import { AddonModLessonModuleHandlerService } from '../../services/handlers/module';
 | 
			
		||||
import { CoreTime } from '@singletons/time';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays a lesson entry page.
 | 
			
		||||
@ -270,10 +271,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns If suceed or not.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModLessonSyncResult): boolean {
 | 
			
		||||
        if (result.updated || this.dataSent) {
 | 
			
		||||
@ -637,12 +635,14 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(): Promise<AddonModLessonSyncResult> {
 | 
			
		||||
        const result = await AddonModLessonSync.syncLesson(this.lesson!.id, true);
 | 
			
		||||
        if (!this.lesson) {
 | 
			
		||||
            throw new CoreError('Cannot sync without a lesson.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const result = await AddonModLessonSync.syncLesson(this.lesson.id, true);
 | 
			
		||||
 | 
			
		||||
        if (!result.updated && this.dataSent && this.isPrefetched()) {
 | 
			
		||||
            // The user sent data to server, but not in the sync process. Check if we need to fetch data.
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreUrlUtils } from '@services/utils/url';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
@ -495,9 +495,7 @@ export const AddonModLessonSync = makeSingleton(AddonModLessonSyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a lesson sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModLessonSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
export type AddonModLessonSyncResult = CoreSyncResult & {
 | 
			
		||||
    courseId?: number; // Course the lesson belongs to (if known).
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -418,10 +418,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns If suceed or not.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModQuizSyncResult): boolean {
 | 
			
		||||
        if (result.attemptFinished) {
 | 
			
		||||
@ -553,9 +550,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(): Promise<AddonModQuizSyncResult> {
 | 
			
		||||
        if (!this.candidateQuiz) {
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ import { CoreQuestion, CoreQuestionQuestionParsed } from '@features/question/ser
 | 
			
		||||
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
@ -482,10 +482,8 @@ export const AddonModQuizSync = makeSingleton(AddonModQuizSyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a quiz sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModQuizSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
export type AddonModQuizSyncResult = CoreSyncResult & {
 | 
			
		||||
    attemptFinished: boolean; // Whether an attempt was finished in the site due to the sync.
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -99,7 +99,7 @@ export class AddonModQuizProvider {
 | 
			
		||||
     * @returns Grade to display.
 | 
			
		||||
     */
 | 
			
		||||
    formatGrade(grade?: number | null, decimals?: number): string {
 | 
			
		||||
        if (grade === undefined || grade == -1 || grade === null || isNaN(grade)) {
 | 
			
		||||
        if (grade === undefined || grade === -1 || grade === null || isNaN(grade)) {
 | 
			
		||||
            return Translate.instant('addon.mod_quiz.notyetgraded');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1800,7 +1800,7 @@ export class AddonModQuizProvider {
 | 
			
		||||
    ): string | undefined {
 | 
			
		||||
        let grade: number | undefined;
 | 
			
		||||
 | 
			
		||||
        const rawGradeNum = typeof rawGrade == 'string' ? parseFloat(rawGrade) : rawGrade;
 | 
			
		||||
        const rawGradeNum = typeof rawGrade === 'string' ? parseFloat(rawGrade) : rawGrade;
 | 
			
		||||
        if (rawGradeNum !== undefined && rawGradeNum !== null && !isNaN(rawGradeNum)) {
 | 
			
		||||
            if (quiz.sumgrades && quiz.sumgrades >= 0.000005) {
 | 
			
		||||
                grade = rawGradeNum * (quiz.grade ?? 0) / quiz.sumgrades;
 | 
			
		||||
 | 
			
		||||
@ -143,7 +143,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
 | 
			
		||||
            this.displayDescription = false;
 | 
			
		||||
 | 
			
		||||
            this.warning = downloadResult.failed
 | 
			
		||||
                ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!)
 | 
			
		||||
                ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error ?? '')
 | 
			
		||||
                : '';
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
@ -61,8 +61,8 @@ export class AddonModResourcePrefetchHandlerService extends CoreCourseResourcePr
 | 
			
		||||
    async downloadOrPrefetch(module: CoreCourseModuleData, courseId: number, prefetch?: boolean): Promise<void> {
 | 
			
		||||
        let dirPath: string | undefined;
 | 
			
		||||
 | 
			
		||||
        if (AddonModResourceHelper.isDisplayedInIframe(module)) {
 | 
			
		||||
            dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url!);
 | 
			
		||||
        if (AddonModResourceHelper.isDisplayedInIframe(module) && module.url !== undefined) {
 | 
			
		||||
            dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<unknown>[] = [];
 | 
			
		||||
 | 
			
		||||
@ -62,7 +62,7 @@ export class AddonModResourceHelperProvider {
 | 
			
		||||
     * @returns Promise resolved with the iframe src.
 | 
			
		||||
     */
 | 
			
		||||
    async getIframeSrc(module: CoreCourseModuleData): Promise<string> {
 | 
			
		||||
        if (!module.contents?.length) {
 | 
			
		||||
        if (!module.contents?.length || module.url === undefined) {
 | 
			
		||||
            throw new CoreError('No contents available in module');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -74,7 +74,7 @@ export class AddonModResourceHelperProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const dirPath = await CoreFilepool.getPackageDirUrlByUrl(CoreSites.getCurrentSiteId(), module.url!);
 | 
			
		||||
            const dirPath = await CoreFilepool.getPackageDirUrlByUrl(CoreSites.getCurrentSiteId(), module.url);
 | 
			
		||||
 | 
			
		||||
            // This URL is going to be injected in an iframe, we need trustAsResourceUrl to make it work in a browser.
 | 
			
		||||
            return CorePath.concatenatePaths(dirPath, mainFilePath);
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { Component, Input, OnInit, Optional } from '@angular/core';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
@ -59,7 +60,10 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
    moduleName = 'scorm';
 | 
			
		||||
 | 
			
		||||
    scorm?: AddonModScormScorm; // The SCORM object.
 | 
			
		||||
    currentOrganization: Partial<AddonModScormOrganization> = {}; // Selected organization.
 | 
			
		||||
    currentOrganization: Partial<AddonModScormOrganization> & { identifier: string} = {
 | 
			
		||||
        identifier: '',
 | 
			
		||||
    }; // Selected organization.
 | 
			
		||||
 | 
			
		||||
    startNewAttempt = false;
 | 
			
		||||
    errorMessage?: string; // Error message.
 | 
			
		||||
    syncTime?: string; // Last sync time.
 | 
			
		||||
@ -70,7 +74,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
    percentage?: string; // Download/unzip percentage.
 | 
			
		||||
    showPercentage = false; // Whether to show the percentage.
 | 
			
		||||
    progressMessage?: string; // Message about download/unzip.
 | 
			
		||||
    organizations?: AddonModScormOrganization[]; // List of organizations.
 | 
			
		||||
    organizations: AddonModScormOrganization[] = []; // List of organizations.
 | 
			
		||||
    loadingToc = false; // Whether the TOC is being loaded.
 | 
			
		||||
    toc?: AddonModScormTOCScoWithIcon[]; // Table of contents (structure).
 | 
			
		||||
    accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
 | 
			
		||||
@ -128,7 +132,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModScormPrefetchHandler.download(this.module, this.courseId, undefined, (data) => {
 | 
			
		||||
                if (!data) {
 | 
			
		||||
                if (!data || !this.scorm) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -137,8 +141,8 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
 | 
			
		||||
                if (data.downloading) {
 | 
			
		||||
                    // Downloading package.
 | 
			
		||||
                    if (this.scorm!.packagesize && data.progress) {
 | 
			
		||||
                        const percentageNumber = Number(data.progress.loaded / this.scorm!.packagesize) * 100;
 | 
			
		||||
                    if (this.scorm.packagesize && data.progress) {
 | 
			
		||||
                        const percentageNumber = Number(data.progress.loaded / this.scorm.packagesize) * 100;
 | 
			
		||||
                        this.percentage = percentageNumber.toFixed(1);
 | 
			
		||||
                        this.showPercentage = percentageNumber >= 0 && percentageNumber <= 100;
 | 
			
		||||
                    }
 | 
			
		||||
@ -198,7 +202,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
                    (
 | 
			
		||||
                        this.accessInfo.canskipview && !this.accessInfo.canviewreport &&
 | 
			
		||||
                        (this.scorm.skipview ?? 0) >= AddonModScormProvider.SKIPVIEW_FIRST &&
 | 
			
		||||
                        (this.scorm.skipview == AddonModScormProvider.SKIPVIEW_ALWAYS || this.lastAttempt == 0)
 | 
			
		||||
                        (this.scorm.skipview === AddonModScormProvider.SKIPVIEW_ALWAYS || this.lastAttempt === 0)
 | 
			
		||||
                    )
 | 
			
		||||
                );
 | 
			
		||||
        }
 | 
			
		||||
@ -221,7 +225,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
        this.lastAttempt = attempt.num;
 | 
			
		||||
        this.lastIsOffline = attempt.offline;
 | 
			
		||||
 | 
			
		||||
        if (this.lastAttempt != this.attempts.lastAttempt.num) {
 | 
			
		||||
        if (this.lastAttempt !== this.attempts.lastAttempt.num) {
 | 
			
		||||
            this.attemptToContinue = this.lastAttempt;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.attemptToContinue = undefined;
 | 
			
		||||
@ -237,7 +241,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
        this.gradeMethodReadable = AddonModScorm.getScormGradeMethod(scorm);
 | 
			
		||||
        this.attemptsLeft = AddonModScorm.countAttemptsLeft(scorm, this.attempts.lastAttempt.num);
 | 
			
		||||
 | 
			
		||||
        if (scorm.forcenewattempt == AddonModScormProvider.SCORM_FORCEATTEMPT_ALWAYS ||
 | 
			
		||||
        if (scorm.forcenewattempt === AddonModScormProvider.SCORM_FORCEATTEMPT_ALWAYS ||
 | 
			
		||||
                (scorm.forcenewattempt && !this.incomplete)) {
 | 
			
		||||
            this.startNewAttempt = true;
 | 
			
		||||
        }
 | 
			
		||||
@ -272,13 +276,9 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
    protected async fetchStructure(scorm: AddonModScormScorm): Promise<void> {
 | 
			
		||||
        this.organizations = await AddonModScorm.getOrganizations(scorm.id, { cmId: this.module.id });
 | 
			
		||||
 | 
			
		||||
        if (!this.currentOrganization.identifier) {
 | 
			
		||||
        if (this.currentOrganization.identifier === '' && this.organizations[0]?.identifier) {
 | 
			
		||||
            // Load first organization (if any).
 | 
			
		||||
            if (this.organizations.length) {
 | 
			
		||||
                this.currentOrganization.identifier = this.organizations[0].identifier;
 | 
			
		||||
            } else {
 | 
			
		||||
                this.currentOrganization.identifier = '';
 | 
			
		||||
            }
 | 
			
		||||
            this.currentOrganization.identifier = this.organizations[0].identifier;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.loadOrganizationToc(scorm, this.currentOrganization.identifier);
 | 
			
		||||
@ -297,7 +297,11 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
        offline: boolean,
 | 
			
		||||
        attempts: Record<number, AddonModScormAttemptGrade>,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const grade = await AddonModScorm.getAttemptGrade(this.scorm!, attempt, offline);
 | 
			
		||||
        if (!this.scorm) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const grade = await AddonModScorm.getAttemptGrade(this.scorm, attempt, offline);
 | 
			
		||||
 | 
			
		||||
        attempts[attempt] = {
 | 
			
		||||
            num: attempt,
 | 
			
		||||
@ -361,10 +365,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns If suceed or not.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModScormSyncResult): boolean {
 | 
			
		||||
        if (result.updated || this.dataSent) {
 | 
			
		||||
@ -374,7 +375,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
 | 
			
		||||
        this.dataSent = false;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
        return result.updated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -438,8 +439,12 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
     * Load a organization's TOC.
 | 
			
		||||
     */
 | 
			
		||||
    async loadOrganization(): Promise<void> {
 | 
			
		||||
        if (!this.scorm) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.loadOrganizationToc(this.scorm!, this.currentOrganization.identifier!);
 | 
			
		||||
            await this.loadOrganizationToc(this.scorm, this.currentOrganization.identifier);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
 | 
			
		||||
        }
 | 
			
		||||
@ -453,7 +458,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadOrganizationToc(scorm: AddonModScormScorm, organizationId: string): Promise<void> {
 | 
			
		||||
        if (!scorm.displaycoursestructure) {
 | 
			
		||||
        if (!scorm.displaycoursestructure || this.lastAttempt === undefined) {
 | 
			
		||||
            // TOC is not displayed, no need to load it.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -461,18 +466,17 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
        this.loadingToc = true;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            this.toc = await AddonModScormHelper.getToc(scorm.id, this.lastAttempt!, this.incomplete, {
 | 
			
		||||
            this.toc = await AddonModScormHelper.getToc(scorm.id, this.lastAttempt, this.incomplete, {
 | 
			
		||||
                organization: organizationId,
 | 
			
		||||
                offline: this.lastIsOffline,
 | 
			
		||||
                cmId: this.module.id,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Search organization title.
 | 
			
		||||
            this.organizations!.forEach((org) => {
 | 
			
		||||
                if (org.identifier == organizationId) {
 | 
			
		||||
                    this.currentOrganization.title = org.title;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            const organization = this.organizations.find((org) => org.identifier === organizationId);
 | 
			
		||||
            if (organization) {
 | 
			
		||||
                this.currentOrganization.title = organization.title;
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loadingToc = false;
 | 
			
		||||
        }
 | 
			
		||||
@ -486,20 +490,18 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
     * @param scoId SCO that needs to be loaded when the SCORM is opened. If not defined, load first SCO.
 | 
			
		||||
     */
 | 
			
		||||
    async open(event?: Event, preview: boolean = false, scoId?: number): Promise<void> {
 | 
			
		||||
        if (event) {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
            event.stopPropagation();
 | 
			
		||||
        }
 | 
			
		||||
        event?.preventDefault();
 | 
			
		||||
        event?.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        if (this.downloading) {
 | 
			
		||||
        if (this.downloading || !this.scorm) {
 | 
			
		||||
            // Scope is being downloaded, abort.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const isOutdated = this.currentStatus == CoreConstants.OUTDATED;
 | 
			
		||||
        const scorm = this.scorm!;
 | 
			
		||||
        const isOutdated = this.currentStatus === CoreConstants.OUTDATED;
 | 
			
		||||
        const scorm = this.scorm;
 | 
			
		||||
 | 
			
		||||
        if (!isOutdated && this.currentStatus != CoreConstants.NOT_DOWNLOADED) {
 | 
			
		||||
        if (!isOutdated && this.currentStatus !== CoreConstants.NOT_DOWNLOADED) {
 | 
			
		||||
            // Already downloaded, open it.
 | 
			
		||||
            this.openScorm(scoId, preview);
 | 
			
		||||
 | 
			
		||||
@ -552,7 +554,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
        this.dataSentObserver?.off();
 | 
			
		||||
 | 
			
		||||
        this.dataSentObserver = CoreEvents.on(AddonModScormProvider.DATA_SENT_EVENT, (data) => {
 | 
			
		||||
            if (data.scormId === this.scorm!.id) {
 | 
			
		||||
            if (data.scormId === this.scorm?.id) {
 | 
			
		||||
                this.dataSent = true;
 | 
			
		||||
 | 
			
		||||
                if (this.module.completiondata && CoreCourse.isIncompleteAutomaticCompletion(this.module.completiondata)) {
 | 
			
		||||
@ -581,14 +583,14 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
     */
 | 
			
		||||
    protected async showStatus(status: string): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        if (status == CoreConstants.OUTDATED && this.scorm) {
 | 
			
		||||
        if (status === CoreConstants.OUTDATED && this.scorm) {
 | 
			
		||||
            // Only show the outdated message if the file should be downloaded.
 | 
			
		||||
            const download = await AddonModScorm.shouldDownloadMainFile(this.scorm, true);
 | 
			
		||||
 | 
			
		||||
            this.statusMessage = download ? 'addon.mod_scorm.scormstatusoutdated' : '';
 | 
			
		||||
        } else if (status == CoreConstants.NOT_DOWNLOADED) {
 | 
			
		||||
        } else if (status === CoreConstants.NOT_DOWNLOADED) {
 | 
			
		||||
            this.statusMessage = 'addon.mod_scorm.scormstatusnotdownloaded';
 | 
			
		||||
        } else if (status == CoreConstants.DOWNLOADING) {
 | 
			
		||||
        } else if (status === CoreConstants.DOWNLOADING) {
 | 
			
		||||
            if (!this.downloading) {
 | 
			
		||||
                // It's being downloaded right now but the view isn't tracking it. "Restore" the download.
 | 
			
		||||
                this.downloadScormPackage();
 | 
			
		||||
@ -605,14 +607,18 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(retries = 0): Promise<AddonModScormSyncResult> {
 | 
			
		||||
        if (CoreSync.isBlocked(AddonModScormProvider.COMPONENT, this.scorm!.id) && retries < 5) {
 | 
			
		||||
        if (!this.scorm) {
 | 
			
		||||
            throw new CoreError('Cannot sync without a scorm.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (CoreSync.isBlocked(AddonModScormProvider.COMPONENT, this.scorm.id) && retries < 5) {
 | 
			
		||||
            // Sync is currently blocked, this can happen when SCORM player is left. Retry in a bit.
 | 
			
		||||
            await CoreUtils.wait(400);
 | 
			
		||||
 | 
			
		||||
            return this.sync(retries + 1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const result = await AddonModScormSync.syncScorm(this.scorm!);
 | 
			
		||||
        const result = await AddonModScormSync.syncScorm(this.scorm);
 | 
			
		||||
 | 
			
		||||
        if (!result.updated && this.dataSent) {
 | 
			
		||||
            // The user sent data to server, but not in the sync process. Check if we need to fetch data.
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/act
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
@ -841,18 +841,14 @@ export const AddonModScormSync = makeSingleton(AddonModScormSyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a SCORM sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModScormSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
export type AddonModScormSyncResult = CoreSyncResult & {
 | 
			
		||||
    attemptFinished: boolean; // Whether an attempt was finished in the site due to the sync,
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the site.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Auto sync event data.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModScormAutoSyncEventData = {
 | 
			
		||||
export type AddonModScormAutoSyncEventData = CoreSyncResult & {
 | 
			
		||||
    scormId: number;
 | 
			
		||||
    attemptFinished: boolean;
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, Optional } from '@angular/core';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreIonLoadingElement } from '@classes/ion-loading';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
@ -117,8 +118,8 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
 | 
			
		||||
        if (sync) {
 | 
			
		||||
            // Try to synchronize the survey.
 | 
			
		||||
            const answersSent = await this.syncActivity(showErrors);
 | 
			
		||||
            if (answersSent) {
 | 
			
		||||
            const updated = await this.syncActivity(showErrors);
 | 
			
		||||
            if (updated) {
 | 
			
		||||
                // Answers were sent, update the survey.
 | 
			
		||||
                this.survey = await AddonModSurvey.getSurvey(this.courseId, this.module.id);
 | 
			
		||||
            }
 | 
			
		||||
@ -130,17 +131,18 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
            : await AddonModSurveyOffline.hasAnswers(this.survey.id);
 | 
			
		||||
 | 
			
		||||
        if (!this.survey.surveydone && !this.hasOffline) {
 | 
			
		||||
            await this.fetchQuestions();
 | 
			
		||||
            await this.fetchQuestions(this.survey.id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience function to get survey questions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param surveyId Survey Id.
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchQuestions(): Promise<void> {
 | 
			
		||||
        const questions = await AddonModSurvey.getQuestions(this.survey!.id, { cmId: this.module.id });
 | 
			
		||||
    protected async fetchQuestions(surveyId: number): Promise<void> {
 | 
			
		||||
        const questions = await AddonModSurvey.getQuestions(surveyId, { cmId: this.module.id });
 | 
			
		||||
 | 
			
		||||
        this.questions = AddonModSurveyHelper.formatQuestions(questions);
 | 
			
		||||
 | 
			
		||||
@ -183,6 +185,10 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
     * Save options selected.
 | 
			
		||||
     */
 | 
			
		||||
    async submit(): Promise<void> {
 | 
			
		||||
        if (!this.survey) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let modal: CoreIonLoadingElement | undefined;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
@ -198,7 +204,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const online = await AddonModSurvey.submitAnswers(this.survey!.id, this.survey!.name, this.courseId, answers);
 | 
			
		||||
            const online = await AddonModSurvey.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers);
 | 
			
		||||
 | 
			
		||||
            CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: this.moduleName });
 | 
			
		||||
 | 
			
		||||
@ -231,22 +237,14 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected sync(): Promise<AddonModSurveySyncResult> {
 | 
			
		||||
        return AddonModSurveySync.syncSurvey(this.survey!.id, this.currentUserId);
 | 
			
		||||
    }
 | 
			
		||||
    protected async sync(): Promise<AddonModSurveySyncResult> {
 | 
			
		||||
        if (!this.survey) {
 | 
			
		||||
            throw new CoreError('Cannot sync without a survey.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns If suceed or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModSurveySyncResult): boolean {
 | 
			
		||||
        return result.answersSent;
 | 
			
		||||
        return AddonModSurveySync.syncSurvey(this.survey.id, this.currentUserId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
 | 
			
		||||
import { CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
@ -80,7 +81,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
                ? this.syncSurvey(entry.surveyid, entry.userid, siteId)
 | 
			
		||||
                : this.syncSurveyIfNeeded(entry.surveyid, entry.userid, siteId));
 | 
			
		||||
 | 
			
		||||
            if (result && result.answersSent) {
 | 
			
		||||
            if (result && result.updated) {
 | 
			
		||||
                // Sync successful, send event.
 | 
			
		||||
                CoreEvents.trigger(AddonModSurveySyncProvider.AUTO_SYNCED, {
 | 
			
		||||
                    surveyId: entry.surveyid,
 | 
			
		||||
@ -150,7 +151,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
    protected async performSyncSurvey(surveyId: number, userId: number, siteId: string): Promise<AddonModSurveySyncResult> {
 | 
			
		||||
        const result: AddonModSurveySyncResult = {
 | 
			
		||||
            warnings: [],
 | 
			
		||||
            answersSent: false,
 | 
			
		||||
            updated: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Sync offline logs.
 | 
			
		||||
@ -179,7 +180,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
            try {
 | 
			
		||||
                await AddonModSurvey.submitAnswersOnline(surveyId, data.answers, siteId);
 | 
			
		||||
 | 
			
		||||
                result.answersSent = true;
 | 
			
		||||
                result.updated = true;
 | 
			
		||||
 | 
			
		||||
                // Answers sent, delete them.
 | 
			
		||||
                await AddonModSurveyOffline.deleteSurveyAnswers(surveyId, siteId, userId);
 | 
			
		||||
@ -190,7 +191,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // The WebService has thrown an error, this means that answers cannot be submitted. Delete them.
 | 
			
		||||
                result.answersSent = true;
 | 
			
		||||
                result.updated = true;
 | 
			
		||||
 | 
			
		||||
                await AddonModSurveyOffline.deleteSurveyAnswers(surveyId, siteId, userId);
 | 
			
		||||
 | 
			
		||||
@ -236,9 +237,7 @@ declare module '@singletons/events' {
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a assign sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModSurveySyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    answersSent: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
export type AddonModSurveySyncResult = CoreSyncResult & {
 | 
			
		||||
    courseId?: number; // Course the survey belongs to (if known).
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -189,7 +189,11 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
 | 
			
		||||
     */
 | 
			
		||||
    go(): void {
 | 
			
		||||
        this.logView();
 | 
			
		||||
        AddonModUrlHelper.open(this.url!);
 | 
			
		||||
        if (!this.url) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        AddonModUrlHelper.open(this.url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -707,12 +707,9 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns If suceed or not.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModWikiSyncWikiResult | undefined): boolean {
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModWikiSyncWikiResult): boolean {
 | 
			
		||||
        if (!result) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
@ -843,13 +840,11 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(): Promise<AddonModWikiSyncWikiResult | undefined> {
 | 
			
		||||
    protected async sync(): Promise<AddonModWikiSyncWikiResult> {
 | 
			
		||||
        if (!this.wiki) {
 | 
			
		||||
            return;
 | 
			
		||||
            throw new CoreError('Cannot sync without a wiki.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return AddonModWikiSync.syncWiki(this.wiki.id, this.courseId, this.wiki.coursemodule);
 | 
			
		||||
 | 
			
		||||
@ -45,8 +45,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
    cmId?: number; // Course module ID.
 | 
			
		||||
    courseId?: number; // Course the wiki belongs to.
 | 
			
		||||
    title?: string; // Title to display.
 | 
			
		||||
    pageForm?: FormGroup; // The form group.
 | 
			
		||||
    contentControl?: FormControl; // The FormControl for the page content.
 | 
			
		||||
    pageForm: FormGroup; // The form group.
 | 
			
		||||
    contentControl: FormControl; // The FormControl for the page content.
 | 
			
		||||
    canEditTitle = false; // Whether title can be edited.
 | 
			
		||||
    loaded = false; // Whether the data has been loaded.
 | 
			
		||||
    component = AddonModWikiProvider.COMPONENT; // Component to link the files to.
 | 
			
		||||
@ -71,7 +71,10 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected formBuilder: FormBuilder,
 | 
			
		||||
    ) { }
 | 
			
		||||
    ) {
 | 
			
		||||
        this.contentControl = this.formBuilder.control('');
 | 
			
		||||
        this.pageForm = this.formBuilder.group({});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
@ -96,10 +99,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
        this.blockId = AddonModWikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId);
 | 
			
		||||
 | 
			
		||||
        // Create the form group and its controls.
 | 
			
		||||
        this.contentControl = this.formBuilder.control('');
 | 
			
		||||
        this.pageForm = this.formBuilder.group({
 | 
			
		||||
            title: pageTitle,
 | 
			
		||||
        });
 | 
			
		||||
        this.pageForm.addControl('title', this.formBuilder.control(pageTitle));
 | 
			
		||||
        this.pageForm.addControl('text', this.contentControl);
 | 
			
		||||
 | 
			
		||||
        // Block the wiki so it cannot be synced.
 | 
			
		||||
@ -121,7 +121,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
            if (success && !this.isDestroyed) {
 | 
			
		||||
                // Block the subwiki now that we have blockId for sure.
 | 
			
		||||
                const newBlockId = AddonModWikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId);
 | 
			
		||||
                if (newBlockId != this.blockId) {
 | 
			
		||||
                if (newBlockId !== this.blockId) {
 | 
			
		||||
                    CoreSync.unblockOperation(this.component, this.blockId);
 | 
			
		||||
                    this.blockId = newBlockId;
 | 
			
		||||
                    CoreSync.blockOperation(this.component, this.blockId);
 | 
			
		||||
@ -143,7 +143,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Wait for sync to be over (if any).
 | 
			
		||||
            const syncResult = await AddonModWikiSync.waitForSync(this.blockId!);
 | 
			
		||||
            const syncResult = this.blockId ? await AddonModWikiSync.waitForSync(this.blockId) : undefined;
 | 
			
		||||
 | 
			
		||||
            if (this.pageId) {
 | 
			
		||||
                // Editing a page that already exists.
 | 
			
		||||
@ -154,7 +154,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
                // Get page contents to obtain title and editing permission
 | 
			
		||||
                const pageContents = await AddonModWiki.getPageContents(this.pageId, { cmId: this.cmId });
 | 
			
		||||
 | 
			
		||||
                this.pageForm!.controls.title.setValue(pageContents.title); // Set the title in the form group.
 | 
			
		||||
                this.pageForm.controls.title.setValue(pageContents.title); // Set the title in the form group.
 | 
			
		||||
                this.wikiId = pageContents.wikiid;
 | 
			
		||||
                this.subwikiId = pageContents.subwikiid;
 | 
			
		||||
                this.title = Translate.instant('addon.mod_wiki.editingpage', { $a: pageContents.title });
 | 
			
		||||
@ -177,7 +177,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
                // Get the original page contents, treating file URLs if needed.
 | 
			
		||||
                const content = CoreTextUtils.replacePluginfileUrls(editContents.content || '', this.subwikiFiles);
 | 
			
		||||
 | 
			
		||||
                this.contentControl!.setValue(content);
 | 
			
		||||
                this.contentControl.setValue(content);
 | 
			
		||||
                this.originalContent = content;
 | 
			
		||||
                this.version = editContents.version;
 | 
			
		||||
 | 
			
		||||
@ -188,7 +188,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
                    }, AddonModWikiProvider.RENEW_LOCK_TIME);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                const pageTitle = this.pageForm!.controls.title.value;
 | 
			
		||||
                const pageTitle = this.pageForm.controls.title.value;
 | 
			
		||||
                this.editing = false;
 | 
			
		||||
                canEdit = !!this.blockId; // If no blockId, the user cannot edit the page.
 | 
			
		||||
 | 
			
		||||
@ -222,7 +222,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
 | 
			
		||||
                    if (page) {
 | 
			
		||||
                        // Load offline content.
 | 
			
		||||
                        this.contentControl!.setValue(page.cachedcontent);
 | 
			
		||||
                        this.contentControl.setValue(page.cachedcontent);
 | 
			
		||||
                        this.originalContent = page.cachedcontent;
 | 
			
		||||
                        this.editOffline = true;
 | 
			
		||||
                    } else {
 | 
			
		||||
@ -286,13 +286,17 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
     * @param title Page title.
 | 
			
		||||
     */
 | 
			
		||||
    protected goToPage(title: string): void {
 | 
			
		||||
        if (!this.wikiId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Not the firstpage.
 | 
			
		||||
        AddonModWiki.setEditedPageData({
 | 
			
		||||
            cmId: this.cmId,
 | 
			
		||||
            courseId: this.courseId,
 | 
			
		||||
            pageId: this.pageId,
 | 
			
		||||
            pageTitle: title,
 | 
			
		||||
            wikiId: this.wikiId!,
 | 
			
		||||
            wikiId: this.wikiId,
 | 
			
		||||
            subwikiId: this.subwikiId,
 | 
			
		||||
            userId: this.userId,
 | 
			
		||||
            groupId: this.groupId,
 | 
			
		||||
@ -307,7 +311,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
     * @returns Whether data has changed.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasDataChanged(): boolean {
 | 
			
		||||
        const values = this.pageForm!.value;
 | 
			
		||||
        const values = this.pageForm.value;
 | 
			
		||||
 | 
			
		||||
        return !(this.originalContent == values.text || (!this.editing && !values.text && !values.title));
 | 
			
		||||
    }
 | 
			
		||||
@ -348,7 +352,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async save(): Promise<void> {
 | 
			
		||||
        const values = this.pageForm!.value;
 | 
			
		||||
        const values = this.pageForm.value;
 | 
			
		||||
        const title = values.title;
 | 
			
		||||
        let text = values.text;
 | 
			
		||||
 | 
			
		||||
@ -358,14 +362,14 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
        text = CoreTextUtils.formatHtmlLines(text);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (this.editing) {
 | 
			
		||||
            if (this.editing && this.pageId) {
 | 
			
		||||
                // Edit existing page.
 | 
			
		||||
                await AddonModWiki.editPage(this.pageId!, text, this.section);
 | 
			
		||||
                await AddonModWiki.editPage(this.pageId, text, this.section);
 | 
			
		||||
 | 
			
		||||
                CoreForms.triggerFormSubmittedEvent(this.formElement, true, CoreSites.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
                // Invalidate page since it changed.
 | 
			
		||||
                await AddonModWiki.invalidatePage(this.pageId!);
 | 
			
		||||
                await AddonModWiki.invalidatePage(this.pageId);
 | 
			
		||||
 | 
			
		||||
                return this.goToPage(title);
 | 
			
		||||
            }
 | 
			
		||||
@ -451,7 +455,11 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
     * Renew lock and control versions.
 | 
			
		||||
     */
 | 
			
		||||
    protected async renewLock(): Promise<void> {
 | 
			
		||||
        const response = await AddonModWiki.getPageForEditing(this.pageId!, this.section, true);
 | 
			
		||||
        if (!this.pageId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const response = await AddonModWiki.getPageForEditing(this.pageId, this.section, true);
 | 
			
		||||
 | 
			
		||||
        if (response.version && this.version != response.version) {
 | 
			
		||||
            this.wrongVersionLock = true;
 | 
			
		||||
 | 
			
		||||
@ -103,6 +103,10 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    const route = CoreNavigator.getCurrentRoute({ pageComponent: AddonModWikiIndexPage });
 | 
			
		||||
                    if (!route) {
 | 
			
		||||
                        // Current view isn't wiki index.
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    const subwikiId = parseInt(params.swid, 10);
 | 
			
		||||
                    const wikiId = parseInt(params.wid, 10);
 | 
			
		||||
                    let path = AddonModWikiModuleHandlerService.PAGE_NAME;
 | 
			
		||||
@ -112,7 +116,7 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
 | 
			
		||||
 | 
			
		||||
                    if (isSameWiki) {
 | 
			
		||||
                        // User is seeing the wiki, we can get the module from the wiki params.
 | 
			
		||||
                        path = path + `/${route!.snapshot.params.courseId}/${route!.snapshot.params.cmId}/edit`;
 | 
			
		||||
                        path = path + `/${route.snapshot.params.courseId}/${route.snapshot.params.cmId}/edit`;
 | 
			
		||||
                    } else if (wikiId) {
 | 
			
		||||
                        // The URL specifies which wiki it belongs to. Get the module.
 | 
			
		||||
                        const module = await CoreCourse.getModuleBasicInfoByInstance(
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
@ -345,9 +345,7 @@ export const AddonModWikiSync = makeSingleton(AddonModWikiSyncProvider);
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a subwiki sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModWikiSyncSubwikiResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether data was updated in the site.
 | 
			
		||||
export type AddonModWikiSyncSubwikiResult = CoreSyncResult & {
 | 
			
		||||
    created: AddonModWikiCreatedPage[]; // List of created pages.
 | 
			
		||||
    discarded: AddonModWikiDiscardedPage[]; // List of discarded pages.
 | 
			
		||||
};
 | 
			
		||||
@ -355,9 +353,7 @@ export type AddonModWikiSyncSubwikiResult = {
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a wiki sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModWikiSyncWikiResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether data was updated in the site.
 | 
			
		||||
export type AddonModWikiSyncWikiResult = CoreSyncResult & {
 | 
			
		||||
    subwikis: {
 | 
			
		||||
        [subwikiId: number]: { // List of subwikis.
 | 
			
		||||
            created: AddonModWikiCreatedPage[];
 | 
			
		||||
 | 
			
		||||
@ -162,6 +162,10 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
 | 
			
		||||
            cmId: this.workshop.coursemodule,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!this.data.assessment.form) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.edit) {
 | 
			
		||||
            try {
 | 
			
		||||
                const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId);
 | 
			
		||||
@ -176,7 +180,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Override assessment plugins values.
 | 
			
		||||
                this.data.assessment.form!.current = AddonModWorkshop.parseFields(
 | 
			
		||||
                this.data.assessment.form.current = AddonModWorkshop.parseFields(
 | 
			
		||||
                    CoreUtils.objectToArrayOfObjects(offlineData, 'name', 'value'),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
@ -221,7 +225,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
 | 
			
		||||
        try {
 | 
			
		||||
            this.data.selectedValues = await AddonWorkshopAssessmentStrategyDelegate.getOriginalValues(
 | 
			
		||||
                this.strategy,
 | 
			
		||||
                this.data.assessment.form!,
 | 
			
		||||
                this.data.assessment.form,
 | 
			
		||||
                this.workshop.id,
 | 
			
		||||
            );
 | 
			
		||||
        } finally {
 | 
			
		||||
@ -245,7 +249,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
 | 
			
		||||
     * @returns True if data has changed.
 | 
			
		||||
     */
 | 
			
		||||
    hasDataChanged(): boolean {
 | 
			
		||||
        if (!this.assessmentStrategyLoaded) {
 | 
			
		||||
        if (!this.assessmentStrategyLoaded || !this.workshop.strategy) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -269,7 +273,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return AddonWorkshopAssessmentStrategyDelegate.hasDataChanged(
 | 
			
		||||
            this.workshop.strategy!,
 | 
			
		||||
            this.workshop.strategy,
 | 
			
		||||
            this.originalData.selectedValues,
 | 
			
		||||
            this.data.selectedValues,
 | 
			
		||||
        );
 | 
			
		||||
@ -281,6 +285,10 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
 | 
			
		||||
     * @returns Promise resolved when done, rejected if assessment could not be saved.
 | 
			
		||||
     */
 | 
			
		||||
    async saveAssessment(): Promise<void> {
 | 
			
		||||
        if (!this.data.assessment?.form) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const files = CoreFileSession.getFiles(
 | 
			
		||||
            AddonModWorkshopProvider.COMPONENT,
 | 
			
		||||
            this.workshop.id + '_' + this.assessmentId,
 | 
			
		||||
@ -328,7 +336,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
 | 
			
		||||
                    this.workshop,
 | 
			
		||||
                    this.data.selectedValues,
 | 
			
		||||
                    text,
 | 
			
		||||
                    this.data.assessment!.form!,
 | 
			
		||||
                    this.data.assessment.form,
 | 
			
		||||
                    attachmentsId,
 | 
			
		||||
                );
 | 
			
		||||
            } catch (errors) {
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
import { Component, Input, OnDestroy, OnInit, Optional } from '@angular/core';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
@ -264,7 +265,12 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
     * @returns Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async gotoSubmissionsPage(page: number): Promise<void> {
 | 
			
		||||
        const report = await AddonModWorkshop.getGradesReport(this.workshop!.id, {
 | 
			
		||||
        if (!this.workshop) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const workshop = this.workshop;
 | 
			
		||||
 | 
			
		||||
        const report = await AddonModWorkshop.getGradesReport(workshop.id, {
 | 
			
		||||
            groupId: this.group,
 | 
			
		||||
            page,
 | 
			
		||||
            cmId: this.module.id,
 | 
			
		||||
@ -284,7 +290,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
        await Promise.all(grades.map(async (grade) => {
 | 
			
		||||
            const submission: AddonModWorkshopSubmissionDataWithOfflineData = {
 | 
			
		||||
                id: grade.submissionid,
 | 
			
		||||
                workshopid: this.workshop!.id,
 | 
			
		||||
                workshopid: workshop.id,
 | 
			
		||||
                example: false,
 | 
			
		||||
                authorid: grade.userid,
 | 
			
		||||
                timecreated: grade.submissionmodified,
 | 
			
		||||
@ -303,7 +309,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
                reviewerof: this.parseReviewer(grade.reviewerof),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_ASSESSMENT) {
 | 
			
		||||
            if (workshop.phase == AddonModWorkshopPhase.PHASE_ASSESSMENT) {
 | 
			
		||||
                submission.reviewedbydone = grade.reviewedby?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
 | 
			
		||||
                submission.reviewerofdone = grade.reviewerof?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
 | 
			
		||||
                submission.reviewedbycount = grade.reviewedby?.length || 0;
 | 
			
		||||
@ -313,7 +319,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
            const offlineData = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
 | 
			
		||||
 | 
			
		||||
            if (offlineData !== undefined) {
 | 
			
		||||
                this.grades!.push(offlineData);
 | 
			
		||||
                this.grades.push(offlineData);
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
@ -358,8 +364,12 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
     * Go to submit page.
 | 
			
		||||
     */
 | 
			
		||||
    gotoSubmit(): void {
 | 
			
		||||
        if (this.canSubmit && ((this.access!.creatingsubmissionallowed && !this.submission) ||
 | 
			
		||||
                (this.access!.modifyingsubmissionallowed && this.submission))) {
 | 
			
		||||
        if (!this.canSubmit || !this.access) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ((this.access.creatingsubmissionallowed && !this.submission) ||
 | 
			
		||||
            (this.access.modifyingsubmissionallowed && this.submission)) {
 | 
			
		||||
            const params: Params = {
 | 
			
		||||
                module: this.module,
 | 
			
		||||
                access: this.access,
 | 
			
		||||
@ -378,20 +388,22 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
     * View Phase info.
 | 
			
		||||
     */
 | 
			
		||||
    async viewPhaseInfo(): Promise<void> {
 | 
			
		||||
        if (this.phases) {
 | 
			
		||||
            const modalData = await CoreDomUtils.openModal<boolean>({
 | 
			
		||||
                component: AddonModWorkshopPhaseInfoComponent,
 | 
			
		||||
                componentProps: {
 | 
			
		||||
                    phases: CoreUtils.objectToArray(this.phases),
 | 
			
		||||
                    workshopPhase: this.workshop!.phase,
 | 
			
		||||
                    externalUrl: this.module.url,
 | 
			
		||||
                    showSubmit: this.showSubmit,
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        if (!this.phases || !this.workshop) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            if (modalData === true) {
 | 
			
		||||
                this.gotoSubmit();
 | 
			
		||||
            }
 | 
			
		||||
        const modalData = await CoreDomUtils.openModal<boolean>({
 | 
			
		||||
            component: AddonModWorkshopPhaseInfoComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                phases: CoreUtils.objectToArray(this.phases),
 | 
			
		||||
                workshopPhase: this.workshop.phase,
 | 
			
		||||
                externalUrl: this.module.url,
 | 
			
		||||
                showSubmit: this.showSubmit,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (modalData === true) {
 | 
			
		||||
            this.gotoSubmit();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -413,26 +425,32 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async setPhaseInfo(): Promise<void> {
 | 
			
		||||
        if (!this.phases || !this.workshop || !this.access) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.submission = undefined;
 | 
			
		||||
        this.canAssess = false;
 | 
			
		||||
        this.assessments = [];
 | 
			
		||||
        this.userGrades = undefined;
 | 
			
		||||
        this.publishedSubmissions = [];
 | 
			
		||||
 | 
			
		||||
        const workshop = this.workshop;
 | 
			
		||||
 | 
			
		||||
        this.canSubmit = AddonModWorkshopHelper.canSubmit(
 | 
			
		||||
            this.workshop!,
 | 
			
		||||
            this.access!,
 | 
			
		||||
            this.phases![AddonModWorkshopPhase.PHASE_SUBMISSION].tasks,
 | 
			
		||||
            this.workshop,
 | 
			
		||||
            this.access,
 | 
			
		||||
            this.phases[AddonModWorkshopPhase.PHASE_SUBMISSION].tasks,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.showSubmit = this.canSubmit &&
 | 
			
		||||
            ((this.access!.creatingsubmissionallowed && !this.submission) ||
 | 
			
		||||
                (this.access!.modifyingsubmissionallowed && !!this.submission));
 | 
			
		||||
            ((this.access.creatingsubmissionallowed && !this.submission) ||
 | 
			
		||||
                (this.access.modifyingsubmissionallowed && !!this.submission));
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        if (this.canSubmit) {
 | 
			
		||||
            promises.push(AddonModWorkshopHelper.getUserSubmission(this.workshop!.id, { cmId: this.module.id })
 | 
			
		||||
            promises.push(AddonModWorkshopHelper.getUserSubmission(this.workshop.id, { cmId: this.module.id })
 | 
			
		||||
                .then(async (submission) => {
 | 
			
		||||
                    this.submission = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
 | 
			
		||||
 | 
			
		||||
@ -440,27 +458,27 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
                }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.access!.canviewallsubmissions && this.workshop!.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
 | 
			
		||||
        if (this.access.canviewallsubmissions && this.workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
 | 
			
		||||
            promises.push(this.gotoSubmissionsPage(this.page));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let assessPromise = Promise.resolve();
 | 
			
		||||
 | 
			
		||||
        if (this.workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT) {
 | 
			
		||||
            this.canAssess = AddonModWorkshopHelper.canAssess(this.workshop!, this.access!);
 | 
			
		||||
        if (this.workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT) {
 | 
			
		||||
            this.canAssess = AddonModWorkshopHelper.canAssess(this.workshop, this.access);
 | 
			
		||||
 | 
			
		||||
            if (this.canAssess) {
 | 
			
		||||
                assessPromise = AddonModWorkshopHelper.getReviewerAssessments(this.workshop!.id, {
 | 
			
		||||
                assessPromise = AddonModWorkshopHelper.getReviewerAssessments(this.workshop.id, {
 | 
			
		||||
                    cmId: this.module.id,
 | 
			
		||||
                }).then(async (assessments) => {
 | 
			
		||||
                    await Promise.all(assessments.map(async (assessment) => {
 | 
			
		||||
                        assessment.strategy = this.workshop!.strategy;
 | 
			
		||||
                        assessment.strategy = workshop.strategy;
 | 
			
		||||
                        if (!this.hasOffline) {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        try {
 | 
			
		||||
                            const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop!.id, assessment.id);
 | 
			
		||||
                            const offlineAssessment = await AddonModWorkshopOffline.getAssessment(workshop.id, assessment.id);
 | 
			
		||||
 | 
			
		||||
                            assessment.offline = true;
 | 
			
		||||
                            assessment.timemodified = Math.floor(offlineAssessment.timemodified / 1000);
 | 
			
		||||
@ -477,27 +495,23 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
 | 
			
		||||
            promises.push(AddonModWorkshop.getGrades(this.workshop!.id, { cmId: this.module.id }).then((grades) => {
 | 
			
		||||
        if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED) {
 | 
			
		||||
            promises.push(AddonModWorkshop.getGrades(this.workshop.id, { cmId: this.module.id }).then((grades) => {
 | 
			
		||||
                this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : undefined;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            if (this.access!.canviewpublishedsubmissions) {
 | 
			
		||||
            if (this.access.canviewpublishedsubmissions) {
 | 
			
		||||
                promises.push(assessPromise.then(async () => {
 | 
			
		||||
                    const submissions: AddonModWorkshopSubmissionDataWithOfflineData[] =
 | 
			
		||||
                        await AddonModWorkshop.getSubmissions(this.workshop!.id, { cmId: this.module.id });
 | 
			
		||||
                        await AddonModWorkshop.getSubmissions(workshop.id, { cmId: this.module.id });
 | 
			
		||||
 | 
			
		||||
                    this.publishedSubmissions = submissions.filter((submission) => {
 | 
			
		||||
                        if (submission.published) {
 | 
			
		||||
                            submission.reviewedby = [];
 | 
			
		||||
 | 
			
		||||
                            this.assessments.forEach((assessment) => {
 | 
			
		||||
                                if (assessment.submissionid == submission.id) {
 | 
			
		||||
                                    submission.reviewedby!.push(AddonModWorkshopHelper.realGradeValue(this.workshop!, assessment));
 | 
			
		||||
                                }
 | 
			
		||||
                            });
 | 
			
		||||
                            submission.reviewedby =
 | 
			
		||||
                                this.assessments.filter((assessment) => assessment.submissionid === submission.id)
 | 
			
		||||
                                    .map((assessment => AddonModWorkshopHelper.realGradeValue(workshop, assessment)));
 | 
			
		||||
 | 
			
		||||
                            return true;
 | 
			
		||||
                        }
 | 
			
		||||
@ -514,22 +528,14 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected sync(): Promise<AddonModWorkshopSyncResult> {
 | 
			
		||||
        return AddonModWorkshopSync.syncWorkshop(this.workshop!.id);
 | 
			
		||||
    }
 | 
			
		||||
        if (!this.workshop) {
 | 
			
		||||
            throw new CoreError('Cannot sync without a workshop.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns If suceed or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModWorkshopSyncResult): boolean {
 | 
			
		||||
        return result.updated;
 | 
			
		||||
        return AddonModWorkshopSync.syncWorkshop(this.workshop.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -385,11 +385,14 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca
 | 
			
		||||
                    );
 | 
			
		||||
                    newSubmissionId = false;
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (!submissionId) {
 | 
			
		||||
                        throw new CoreError('Submission cannot be updated without a submissionId');
 | 
			
		||||
                    }
 | 
			
		||||
                    // Try to send it to server.
 | 
			
		||||
                    // Don't allow offline if there are attachments since they were uploaded fine.
 | 
			
		||||
                    newSubmissionId = await AddonModWorkshop.updateSubmission(
 | 
			
		||||
                        this.workshopId,
 | 
			
		||||
                        submissionId!,
 | 
			
		||||
                        submissionId,
 | 
			
		||||
                        this.courseId,
 | 
			
		||||
                        inputData.title,
 | 
			
		||||
                        inputData.content,
 | 
			
		||||
 | 
			
		||||
@ -254,7 +254,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }));
 | 
			
		||||
            } else if (this.currentUserId == this.userId && this.assessmentId) {
 | 
			
		||||
            } else if (this.currentUserId === this.userId && this.assessmentId) {
 | 
			
		||||
                // Get new data, different that came from stateParams.
 | 
			
		||||
                promises.push(AddonModWorkshop.getAssessment(this.workshopId, this.assessmentId, {
 | 
			
		||||
                    cmId: this.module.id,
 | 
			
		||||
@ -268,7 +268,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }));
 | 
			
		||||
            } else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.userId == this.currentUserId) {
 | 
			
		||||
            } else if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED && this.userId === this.currentUserId) {
 | 
			
		||||
                const assessments = await AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, {
 | 
			
		||||
                    cmId: this.module.id,
 | 
			
		||||
                });
 | 
			
		||||
@ -276,7 +276,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
                this.submissionInfo.reviewedby = assessments.map((assessment) => this.parseAssessment(assessment));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
 | 
			
		||||
            if (this.canAddFeedback || this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED) {
 | 
			
		||||
                this.evaluate = {
 | 
			
		||||
                    published: this.submission.published,
 | 
			
		||||
                    text: this.submission.feedbackauthor || '',
 | 
			
		||||
@ -284,54 +284,13 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.canAddFeedback) {
 | 
			
		||||
 | 
			
		||||
                if (!this.isDestroyed) {
 | 
			
		||||
                    // Block the workshop.
 | 
			
		||||
                    CoreSync.blockOperation(this.component, this.workshopId);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
 | 
			
		||||
 | 
			
		||||
                promises.push(CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1)
 | 
			
		||||
                    .then(async (grades) => {
 | 
			
		||||
                        this.evaluationGrades = grades;
 | 
			
		||||
 | 
			
		||||
                        this.evaluate!.grade = {
 | 
			
		||||
                            label: CoreGradesHelper.getGradeLabelFromValue(grades, this.submissionInfo.gradeover) ||
 | 
			
		||||
                            defaultGrade,
 | 
			
		||||
                            value: this.submissionInfo.gradeover || -1,
 | 
			
		||||
                        };
 | 
			
		||||
 | 
			
		||||
                        try {
 | 
			
		||||
                            const offlineSubmission =
 | 
			
		||||
                                await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId);
 | 
			
		||||
 | 
			
		||||
                            this.hasOffline = true;
 | 
			
		||||
                            this.evaluate!.published = offlineSubmission.published;
 | 
			
		||||
                            this.evaluate!.text = offlineSubmission.feedbacktext;
 | 
			
		||||
                            this.evaluate!.grade = {
 | 
			
		||||
                                label: CoreGradesHelper.getGradeLabelFromValue(
 | 
			
		||||
                                    grades,
 | 
			
		||||
                                    parseInt(offlineSubmission.gradeover, 10),
 | 
			
		||||
                                ) || defaultGrade,
 | 
			
		||||
                                value: offlineSubmission.gradeover || -1,
 | 
			
		||||
                            };
 | 
			
		||||
                        } catch {
 | 
			
		||||
                            // Ignore errors.
 | 
			
		||||
                            this.hasOffline = false;
 | 
			
		||||
                        } finally {
 | 
			
		||||
                            this.originalEvaluation.published = this.evaluate!.published;
 | 
			
		||||
                            this.originalEvaluation.text = this.evaluate!.text;
 | 
			
		||||
                            this.originalEvaluation.grade = this.evaluate!.grade.value;
 | 
			
		||||
 | 
			
		||||
                            this.feedbackForm.controls['published'].setValue(this.evaluate!.published);
 | 
			
		||||
                            this.feedbackForm.controls['grade'].setValue(this.evaluate!.grade.value);
 | 
			
		||||
                            this.feedbackForm.controls['text'].setValue(this.evaluate!.text);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    }));
 | 
			
		||||
            } else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby &&
 | 
			
		||||
                promises.push(this.fillEvaluationsGrades());
 | 
			
		||||
            } else if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby &&
 | 
			
		||||
                    this.evaluate && this.evaluate.text) {
 | 
			
		||||
                promises.push(CoreUser.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => {
 | 
			
		||||
                    this.evaluateByProfile = profile;
 | 
			
		||||
@ -362,6 +321,49 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fill evaluation grade info.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fillEvaluationsGrades(): Promise<void> {
 | 
			
		||||
        const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
 | 
			
		||||
 | 
			
		||||
        this.evaluationGrades = await CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1);
 | 
			
		||||
 | 
			
		||||
        if (!this.evaluate) {
 | 
			
		||||
            // Should not happen.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.evaluate.grade = {
 | 
			
		||||
            label: CoreGradesHelper.getGradeLabelFromValue(this.evaluationGrades, this.submissionInfo.gradeover) || defaultGrade,
 | 
			
		||||
            value: this.submissionInfo.gradeover || -1,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const offlineSubmission = await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId);
 | 
			
		||||
 | 
			
		||||
            this.hasOffline = true;
 | 
			
		||||
            this.evaluate.published = offlineSubmission.published;
 | 
			
		||||
            this.evaluate.text = offlineSubmission.feedbacktext;
 | 
			
		||||
            this.evaluate.grade = {
 | 
			
		||||
                label: CoreGradesHelper.getGradeLabelFromValue(this.evaluationGrades, parseInt(offlineSubmission.gradeover, 10)) ||
 | 
			
		||||
                    defaultGrade,
 | 
			
		||||
                value: offlineSubmission.gradeover || -1,
 | 
			
		||||
            };
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
            this.hasOffline = false;
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.originalEvaluation.published = this.evaluate.published;
 | 
			
		||||
            this.originalEvaluation.text = this.evaluate.text;
 | 
			
		||||
            this.originalEvaluation.grade = this.evaluate.grade.value;
 | 
			
		||||
 | 
			
		||||
            this.feedbackForm.controls['published'].setValue(this.evaluate.published);
 | 
			
		||||
            this.feedbackForm.controls['grade'].setValue(this.evaluate.grade.value);
 | 
			
		||||
            this.feedbackForm.controls['text'].setValue(this.evaluate.text);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse assessment to be shown.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -71,7 +71,7 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
 | 
			
		||||
    ): Promise<{ workshop?: AddonModWorkshopData; groups: CoreGroup[]; files: CoreWSFile[]}> {
 | 
			
		||||
        let groups: CoreGroup[] = [];
 | 
			
		||||
        let files: CoreWSFile[] = [];
 | 
			
		||||
        let workshop: AddonModWorkshopData | undefined;
 | 
			
		||||
        let workshop: AddonModWorkshopData;
 | 
			
		||||
        let access: AddonModWorkshopGetWorkshopAccessInformationWSResponse | undefined;
 | 
			
		||||
 | 
			
		||||
        const modOptions = {
 | 
			
		||||
@ -79,11 +79,25 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
 | 
			
		||||
            ...options, // Include all options.
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
            const userId = site.getUserId();
 | 
			
		||||
            const workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
 | 
			
		||||
        const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
        options.siteId = options.siteId ?? site.getId();
 | 
			
		||||
        const userId = site.getUserId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
 | 
			
		||||
        }  catch (error) {
 | 
			
		||||
            if (options.omitFail) {
 | 
			
		||||
                // Any error, return the info we have.
 | 
			
		||||
                return {
 | 
			
		||||
                    groups: [],
 | 
			
		||||
                    files: [],
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            throw error;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            files = this.getIntroFilesFromInstance(module, workshop);
 | 
			
		||||
            files = files.concat(workshop.instructauthorsfiles || []).concat(workshop.instructreviewersfiles || []);
 | 
			
		||||
 | 
			
		||||
@ -124,7 +138,7 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
 | 
			
		||||
                    await Promise.all(submissions.map(async (submission) => {
 | 
			
		||||
                        files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
 | 
			
		||||
 | 
			
		||||
                        const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop!.id, submission.id, {
 | 
			
		||||
                        const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop.id, submission.id, {
 | 
			
		||||
                            cmId: module.id,
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
@ -133,9 +147,9 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
 | 
			
		||||
                                .concat(assessment.feedbackcontentfiles);
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        if (workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
 | 
			
		||||
                        if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
 | 
			
		||||
                            await Promise.all(assessments.map((assessment) =>
 | 
			
		||||
                                AddonModWorkshopHelper.getReviewerAssessmentById(workshop!.id, assessment.id)));
 | 
			
		||||
                                AddonModWorkshopHelper.getReviewerAssessmentById(workshop.id, assessment.id)));
 | 
			
		||||
                        }
 | 
			
		||||
                    }));
 | 
			
		||||
 | 
			
		||||
@ -267,7 +281,12 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
 | 
			
		||||
 | 
			
		||||
        // Prefetch the workshop data.
 | 
			
		||||
        const info = await this.getWorkshopInfoHelper(module, courseId, commonOptions);
 | 
			
		||||
        const workshop = info.workshop!;
 | 
			
		||||
        if (!info.workshop) {
 | 
			
		||||
            // It would throw an exception so it would not happen.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const workshop = info.workshop;
 | 
			
		||||
        const promises: Promise<unknown>[] = [];
 | 
			
		||||
        const assessmentIds: number[] = [];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -181,7 +181,7 @@ export class AddonModWorkshopHelperProvider {
 | 
			
		||||
            assessment = await AddonModWorkshop.getAssessment(workshopId, assessmentId, options);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            const assessments = await AddonModWorkshop.getReviewerAssessments(workshopId, options);
 | 
			
		||||
            assessment = assessments.find((assessment_1) => assessment_1.id == assessmentId);
 | 
			
		||||
            assessment = assessments.find((ass) => ass.id === assessmentId);
 | 
			
		||||
 | 
			
		||||
            if (!assessment) {
 | 
			
		||||
                throw error;
 | 
			
		||||
@ -266,9 +266,7 @@ export class AddonModWorkshopHelperProvider {
 | 
			
		||||
     * Upload or store some files for a submission, depending if the user is offline or not.
 | 
			
		||||
     *
 | 
			
		||||
     * @param workshopId Workshop ID.
 | 
			
		||||
     * @param submissionId If not editing, it will refer to timecreated.
 | 
			
		||||
     * @param files List of files.
 | 
			
		||||
     * @param editing If the submission is being edited or added otherwise.
 | 
			
		||||
     * @param offline True if files sould be stored for offline, false to upload them.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @returns Promise resolved if success.
 | 
			
		||||
@ -451,30 +449,26 @@ export class AddonModWorkshopHelperProvider {
 | 
			
		||||
     * @returns Promise resolved with the files.
 | 
			
		||||
     */
 | 
			
		||||
    async applyOfflineData(
 | 
			
		||||
        submission?: AddonModWorkshopSubmissionDataWithOfflineData,
 | 
			
		||||
        submission: AddonModWorkshopSubmissionDataWithOfflineData = {
 | 
			
		||||
            id: 0,
 | 
			
		||||
            workshopid: 0,
 | 
			
		||||
            title: '',
 | 
			
		||||
            content: '',
 | 
			
		||||
            timemodified: 0,
 | 
			
		||||
            example: false,
 | 
			
		||||
            authorid: 0,
 | 
			
		||||
            timecreated: 0,
 | 
			
		||||
            contenttrust: 0,
 | 
			
		||||
            attachment: 0,
 | 
			
		||||
            published: false,
 | 
			
		||||
            late: 0,
 | 
			
		||||
        },
 | 
			
		||||
        actions: AddonModWorkshopOfflineSubmission[] = [],
 | 
			
		||||
    ): Promise<AddonModWorkshopSubmissionDataWithOfflineData | undefined> {
 | 
			
		||||
        if (actions.length == 0) {
 | 
			
		||||
        if (actions.length === 0) {
 | 
			
		||||
            return submission;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (submission === undefined) {
 | 
			
		||||
            submission = {
 | 
			
		||||
                id: 0,
 | 
			
		||||
                workshopid: 0,
 | 
			
		||||
                title: '',
 | 
			
		||||
                content: '',
 | 
			
		||||
                timemodified: 0,
 | 
			
		||||
                example: false,
 | 
			
		||||
                authorid: 0,
 | 
			
		||||
                timecreated: 0,
 | 
			
		||||
                contenttrust: 0,
 | 
			
		||||
                attachment: 0,
 | 
			
		||||
                published: false,
 | 
			
		||||
                late: 0,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let attachmentsId: CoreFileUploaderStoreFilesResult | undefined;
 | 
			
		||||
        const workshopId = actions[0].workshopid;
 | 
			
		||||
 | 
			
		||||
@ -482,17 +476,17 @@ export class AddonModWorkshopHelperProvider {
 | 
			
		||||
            switch (action.action) {
 | 
			
		||||
                case AddonModWorkshopAction.ADD:
 | 
			
		||||
                case AddonModWorkshopAction.UPDATE:
 | 
			
		||||
                    submission!.title = action.title;
 | 
			
		||||
                    submission!.content = action.content;
 | 
			
		||||
                    submission!.title = action.title;
 | 
			
		||||
                    submission!.courseid = action.courseid;
 | 
			
		||||
                    submission!.submissionmodified = action.timemodified / 1000;
 | 
			
		||||
                    submission!.offline = true;
 | 
			
		||||
                    submission.title = action.title;
 | 
			
		||||
                    submission.content = action.content;
 | 
			
		||||
                    submission.title = action.title;
 | 
			
		||||
                    submission.courseid = action.courseid;
 | 
			
		||||
                    submission.submissionmodified = action.timemodified / 1000;
 | 
			
		||||
                    submission.offline = true;
 | 
			
		||||
                    attachmentsId = action.attachmentsid as CoreFileUploaderStoreFilesResult;
 | 
			
		||||
                    break;
 | 
			
		||||
                case AddonModWorkshopAction.DELETE:
 | 
			
		||||
                    submission!.deleted = true;
 | 
			
		||||
                    submission!.submissionmodified = action.timemodified / 1000;
 | 
			
		||||
                    submission.deleted = true;
 | 
			
		||||
                    submission.submissionmodified = action.timemodified / 1000;
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
            }
 | 
			
		||||
@ -534,7 +528,8 @@ export class AddonModWorkshopHelperProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const data =
 | 
			
		||||
            (await AddonWorkshopAssessmentStrategyDelegate.prepareAssessmentData(workshop.strategy!, selectedValues, form)) || {};
 | 
			
		||||
            (await AddonWorkshopAssessmentStrategyDelegate.prepareAssessmentData(workshop.strategy ?? '', selectedValues, form)) ||
 | 
			
		||||
            {};
 | 
			
		||||
        data.feedbackauthor = feedbackText;
 | 
			
		||||
        data.feedbackauthorattachmentsid = attachmentsId;
 | 
			
		||||
        data.nodims = form.dimenssionscount;
 | 
			
		||||
@ -551,16 +546,16 @@ export class AddonModWorkshopHelperProvider {
 | 
			
		||||
     * @returns Real grade formatted.
 | 
			
		||||
     */
 | 
			
		||||
    protected realGradeValueHelper(value?: number | string, max = 0, decimals = 0): string | undefined {
 | 
			
		||||
        if (typeof value == 'string') {
 | 
			
		||||
        if (typeof value === 'string') {
 | 
			
		||||
            // Already treated.
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (value == null || value === undefined) {
 | 
			
		||||
        if (value === null || value === undefined) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (max == 0) {
 | 
			
		||||
        if (max === 0) {
 | 
			
		||||
            return '0';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/service
 | 
			
		||||
import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreFileEntry } from '@services/file-helper';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreSync, CoreSyncResult } from '@services/sync';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate, makeSingleton } from '@singletons';
 | 
			
		||||
@ -639,7 +639,4 @@ export type AddonModWorkshopAutoSyncData = {
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AddonModWorkshopSyncResult = {
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
};
 | 
			
		||||
export type AddonModWorkshopSyncResult = CoreSyncResult;
 | 
			
		||||
 | 
			
		||||
@ -610,13 +610,16 @@ export class AddonModWorkshopProvider {
 | 
			
		||||
        grades: AddonModWorkshopGradesData[],
 | 
			
		||||
        options: AddonModWorkshopGetGradesReportOptions = {},
 | 
			
		||||
    ): Promise<AddonModWorkshopGradesData[]> {
 | 
			
		||||
        options.page = options.page ?? 0;
 | 
			
		||||
        options.perPage = options.perPage ?? AddonModWorkshopProvider.PER_PAGE;
 | 
			
		||||
 | 
			
		||||
        const report = await this.getGradesReport(workshopId, options);
 | 
			
		||||
 | 
			
		||||
        Array.prototype.push.apply(grades, report.grades);
 | 
			
		||||
        const canLoadMore = ((options.page! + 1) * options.perPage!) < report.totalcount;
 | 
			
		||||
        const canLoadMore = ((options.page + 1) * options.perPage) < report.totalcount;
 | 
			
		||||
 | 
			
		||||
        if (canLoadMore) {
 | 
			
		||||
            options.page!++;
 | 
			
		||||
            options.page++;
 | 
			
		||||
 | 
			
		||||
            return this.fetchGradeReportsRecursive(workshopId, grades, options);
 | 
			
		||||
        }
 | 
			
		||||
@ -778,7 +781,11 @@ export class AddonModWorkshopProvider {
 | 
			
		||||
        // Other errors ocurring.
 | 
			
		||||
        CoreWS.throwOnFailedStatus(response, 'Add submission failed');
 | 
			
		||||
 | 
			
		||||
        return response.submissionid!;
 | 
			
		||||
        if (!response.submissionid) {
 | 
			
		||||
            throw new CoreError('Add submission failed, no submission id was returned');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response.submissionid;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -1,57 +1,57 @@
 | 
			
		||||
<ion-list class="addon-qtype-calculated-container" *ngIf="calcQuestion && (calcQuestion.text || calcQuestion.text === '')">
 | 
			
		||||
<ion-list class="addon-qtype-calculated-container" *ngIf="question && (question.text || question.text === '')">
 | 
			
		||||
    <ion-item class="ion-text-wrap">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="calcQuestion.text" [contextLevel]="contextLevel"
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
 | 
			
		||||
                [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
 | 
			
		||||
    <!-- Display unit options before the answer input. -->
 | 
			
		||||
    <ng-container *ngIf="calcQuestion.options && calcQuestion.options.length && calcQuestion.optionsFirst">
 | 
			
		||||
    <ng-container *ngIf="question.options && question.options.length && question.optionsFirst">
 | 
			
		||||
        <ng-container *ngTemplateOutlet="radioUnits"></ng-container>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <ion-item *ngIf="calcQuestion.input" class="ion-text-wrap core-{{calcQuestion.input.correctIconColor}}-item">
 | 
			
		||||
    <ion-item *ngIf="question.input" class="ion-text-wrap core-{{question.input.correctIconColor}}-item">
 | 
			
		||||
        <ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
 | 
			
		||||
 | 
			
		||||
        <div class="flex-row">
 | 
			
		||||
            <!-- Display unit select before the answer input. -->
 | 
			
		||||
            <ng-container *ngIf="calcQuestion.select && calcQuestion.selectFirst">
 | 
			
		||||
            <ng-container *ngIf="question.select && question.selectFirst">
 | 
			
		||||
                <ng-container *ngTemplateOutlet="selectUnits"></ng-container>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
            <!-- Input to enter the answer. -->
 | 
			
		||||
            <ion-input type="text" [attr.name]="calcQuestion.input.name"
 | 
			
		||||
                [placeholder]="calcQuestion.input.readOnly ? '' : 'core.question.answer' | translate" [value]="calcQuestion.input.value"
 | 
			
		||||
                [disabled]="calcQuestion.input.readOnly" autocorrect="off">
 | 
			
		||||
            <ion-input type="text" [attr.name]="question.input.name"
 | 
			
		||||
                [placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate" [value]="question.input.value"
 | 
			
		||||
                [disabled]="question.input.readOnly" autocorrect="off">
 | 
			
		||||
            </ion-input>
 | 
			
		||||
 | 
			
		||||
            <!-- Display unit select after the answer input. -->
 | 
			
		||||
            <ng-container *ngIf="calcQuestion.select && !calcQuestion.selectFirst">
 | 
			
		||||
            <ng-container *ngIf="question.select && !question.selectFirst">
 | 
			
		||||
                <ng-container *ngTemplateOutlet="selectUnits"></ng-container>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ion-icon *ngIf="calcQuestion.input.correctIcon" class="core-correct-icon ion-align-self-center" slot="end"
 | 
			
		||||
            [name]="calcQuestion.input.correctIcon" [color]="[calcQuestion.input.correctIconColor]">
 | 
			
		||||
        <ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon ion-align-self-center" slot="end"
 | 
			
		||||
            [name]="question.input.correctIcon" [color]="[question.input.correctIconColor]">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
 | 
			
		||||
    <!-- Display unit options after the answer input. -->
 | 
			
		||||
    <ng-container *ngIf="calcQuestion.options && calcQuestion.options.length && !calcQuestion.optionsFirst">
 | 
			
		||||
    <ng-container *ngIf="question.options && question.options.length && !question.optionsFirst">
 | 
			
		||||
        <ng-container *ngTemplateOutlet="radioUnits"></ng-container>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
</ion-list>
 | 
			
		||||
 | 
			
		||||
<!-- Template for units entered using a select. -->
 | 
			
		||||
<ng-template #selectUnits>
 | 
			
		||||
    <label *ngIf="calcQuestion!.select!.accessibilityLabel" class="accesshide" for="{{calcQuestion!.select!.id}}">
 | 
			
		||||
        {{ calcQuestion!.select!.accessibilityLabel }}
 | 
			
		||||
    <label *ngIf="question!.select!.accessibilityLabel" class="accesshide" for="{{question!.select!.id}}">
 | 
			
		||||
        {{ question!.select!.accessibilityLabel }}
 | 
			
		||||
    </label>
 | 
			
		||||
    <ion-select id="{{calcQuestion!.select!.id}}" [name]="calcQuestion!.select!.name" [(ngModel)]="calcQuestion!.select!.selected"
 | 
			
		||||
        interface="action-sheet" [disabled]="calcQuestion!.select!.disabled" [slot]="calcQuestion?.selectFirst ? 'start' : 'end'"
 | 
			
		||||
    <ion-select id="{{question!.select!.id}}" [name]="question!.select!.name" [(ngModel)]="question!.select!.selected"
 | 
			
		||||
        interface="action-sheet" [disabled]="question!.select!.disabled" [slot]="question?.selectFirst ? 'start' : 'end'"
 | 
			
		||||
        [interfaceOptions]="{header: 'addon.mod_quiz.unit' | translate}">
 | 
			
		||||
        <ion-select-option *ngFor="let option of calcQuestion!.select!.options" [value]="option.value">
 | 
			
		||||
        <ion-select-option *ngFor="let option of question!.select!.options" [value]="option.value">
 | 
			
		||||
            {{option.label}}
 | 
			
		||||
        </ion-select-option>
 | 
			
		||||
    </ion-select>
 | 
			
		||||
@ -59,15 +59,15 @@
 | 
			
		||||
 | 
			
		||||
<!-- Template for units entered using radio buttons. -->
 | 
			
		||||
<ng-template #radioUnits>
 | 
			
		||||
    <ion-radio-group [(ngModel)]="calcQuestion!.unit" [name]="calcQuestion!.optionsName">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngFor="let option of calcQuestion!.options">
 | 
			
		||||
    <ion-radio-group [(ngModel)]="question!.unit" [name]="question!.optionsName">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngFor="let option of question!.options">
 | 
			
		||||
            <ion-label>{{ option.text }}</ion-label>
 | 
			
		||||
            <ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || calcQuestion!.input?.readOnly"
 | 
			
		||||
                [color]="calcQuestion!.input?.correctIconColor">
 | 
			
		||||
            <ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || question!.input?.readOnly"
 | 
			
		||||
                [color]="question!.input?.correctIconColor">
 | 
			
		||||
            </ion-radio>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
 | 
			
		||||
        <input type="hidden" [ngModel]="calcQuestion!.unit" [attr.name]="calcQuestion!.optionsName">
 | 
			
		||||
        <input type="hidden" [ngModel]="question!.unit" [attr.name]="question!.optionsName">
 | 
			
		||||
    </ion-radio-group>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
 | 
			
		||||
@ -23,9 +23,7 @@ import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@feat
 | 
			
		||||
    selector: 'addon-qtype-calculated',
 | 
			
		||||
    templateUrl: 'addon-qtype-calculated.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    calcQuestion?: AddonModQuizCalculatedQuestion;
 | 
			
		||||
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent<AddonModQuizCalculatedQuestion> {
 | 
			
		||||
 | 
			
		||||
    constructor(elementRef: ElementRef) {
 | 
			
		||||
        super('AddonQtypeCalculatedComponent', elementRef);
 | 
			
		||||
@ -34,10 +32,8 @@ export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent imp
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        this.initCalculatedComponent();
 | 
			
		||||
 | 
			
		||||
        this.calcQuestion = this.question;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -708,9 +708,15 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
 | 
			
		||||
        this.topNode = this.container.querySelector<HTMLElement>('.addon-qtype-ddimageortext-container');
 | 
			
		||||
        this.dragItemsArea = this.topNode?.querySelector<HTMLElement>('div.draghomes') || null;
 | 
			
		||||
 | 
			
		||||
        if (!this.topNode) {
 | 
			
		||||
            this.logger.error('ddimageortext container not found');
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.dragItemsArea) {
 | 
			
		||||
            // On 3.9+ dragitems were removed.
 | 
			
		||||
            const dragItems = this.topNode!.querySelector('div.dragitems');
 | 
			
		||||
            const dragItems = this.topNode.querySelector('div.dragitems');
 | 
			
		||||
 | 
			
		||||
            if (dragItems) {
 | 
			
		||||
                // Remove empty div.dragitems.
 | 
			
		||||
@ -718,10 +724,10 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 3.6+ site, transform HTML so it has the same structure as in Moodle 3.5.
 | 
			
		||||
            const ddArea = this.topNode!.querySelector('div.ddarea');
 | 
			
		||||
            const ddArea = this.topNode.querySelector('div.ddarea');
 | 
			
		||||
            if (ddArea) {
 | 
			
		||||
                // Move div.dropzones to div.ddarea.
 | 
			
		||||
                const dropZones = this.topNode!.querySelector('div.dropzones');
 | 
			
		||||
                const dropZones = this.topNode.querySelector('div.dropzones');
 | 
			
		||||
                if (dropZones) {
 | 
			
		||||
                    ddArea.appendChild(dropZones);
 | 
			
		||||
                }
 | 
			
		||||
@ -738,7 +744,7 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
 | 
			
		||||
                draghome.classList.add(`dragitemhomes${index}`);
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            this.dragItemsArea = this.topNode!.querySelector<HTMLElement>('div.dragitems');
 | 
			
		||||
            this.dragItemsArea = this.topNode.querySelector<HTMLElement>('div.dragitems');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -797,14 +803,15 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
 | 
			
		||||
    getClassnameNumericSuffix(node: HTMLElement, prefix: string): number | undefined {
 | 
			
		||||
        if (node.classList && node.classList.length) {
 | 
			
		||||
            const patt1 = new RegExp(`^${prefix}([0-9])+$`);
 | 
			
		||||
            const patt2 = new RegExp('([0-9])+$');
 | 
			
		||||
 | 
			
		||||
            for (let index = 0; index < node.classList.length; index++) {
 | 
			
		||||
                if (patt1.test(node.classList[index])) {
 | 
			
		||||
                    const match = patt2.exec(node.classList[index]);
 | 
			
		||||
            const classFound = Array.from(node.classList)
 | 
			
		||||
                .find((className) => patt1.test(className));
 | 
			
		||||
 | 
			
		||||
                    return Number(match![0]);
 | 
			
		||||
                }
 | 
			
		||||
            if (classFound) {
 | 
			
		||||
                const patt2 = new RegExp('([0-9])+$');
 | 
			
		||||
                const match = patt2.exec(classFound);
 | 
			
		||||
 | 
			
		||||
                return Number(match?.[0]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,24 +1,24 @@
 | 
			
		||||
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')" class="addon-qtype-ddimageortext-container">
 | 
			
		||||
<div *ngIf="question && (question.text || question.text === '')" class="addon-qtype-ddimageortext-container">
 | 
			
		||||
    <!-- Content is outside the core-loading to let the script calculate drag items position -->
 | 
			
		||||
    <core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
 | 
			
		||||
    <core-loading [hideUntil]="question.loaded"></core-loading>
 | 
			
		||||
 | 
			
		||||
    <ion-item class="ion-text-wrap" [hidden]="!ddQuestion.loaded">
 | 
			
		||||
    <ion-item class="ion-text-wrap" [hidden]="!question.loaded">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
 | 
			
		||||
            <ion-card *ngIf="!question.readOnly" class="core-info-card">
 | 
			
		||||
                <ion-item>
 | 
			
		||||
                    <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                    <ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ion-card>
 | 
			
		||||
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" [contextLevel]="contextLevel"
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
 | 
			
		||||
                [contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
 | 
			
		||||
        <core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
 | 
			
		||||
            [text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
 | 
			
		||||
    <div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
 | 
			
		||||
        <core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
 | 
			
		||||
            [text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
 | 
			
		||||
        </core-format-text>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -12,11 +12,10 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component, OnDestroy, ElementRef } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -27,9 +26,9 @@ import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
 | 
			
		||||
    templateUrl: 'addon-qtype-ddimageortext.html',
 | 
			
		||||
    styleUrls: ['ddimageortext.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    ddQuestion?: AddonModQuizDdImageOrTextQuestionData;
 | 
			
		||||
export class AddonQtypeDdImageOrTextComponent
 | 
			
		||||
    extends CoreQuestionBaseComponent<AddonModQuizDdImageOrTextQuestionData>
 | 
			
		||||
    implements OnDestroy {
 | 
			
		||||
 | 
			
		||||
    protected questionInstance?: AddonQtypeDdImageOrTextQuestion;
 | 
			
		||||
    protected drops?: unknown[]; // The drop zones received in the init object of the question.
 | 
			
		||||
@ -44,50 +43,47 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        if (!this.question) {
 | 
			
		||||
            this.logger.warn('Aborting because of no question received.');
 | 
			
		||||
 | 
			
		||||
            return CoreQuestionHelper.showComponentError(this.onAbort);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion = this.question;
 | 
			
		||||
 | 
			
		||||
        const element = CoreDomUtils.convertToElement(this.ddQuestion.html);
 | 
			
		||||
        const questionElement = this.initComponent();
 | 
			
		||||
        if (!questionElement) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get D&D area and question text.
 | 
			
		||||
        const ddArea = element.querySelector('.ddarea');
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
 | 
			
		||||
        if (!ddArea || this.ddQuestion.text === undefined) {
 | 
			
		||||
            this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
 | 
			
		||||
        const ddArea = questionElement.querySelector('.ddarea');
 | 
			
		||||
        if (!ddArea) {
 | 
			
		||||
            this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
 | 
			
		||||
 | 
			
		||||
            return CoreQuestionHelper.showComponentError(this.onAbort);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set the D&D area HTML.
 | 
			
		||||
        this.ddQuestion.ddArea = ddArea.outerHTML;
 | 
			
		||||
        this.ddQuestion.readOnly = false;
 | 
			
		||||
        this.question.ddArea = ddArea.outerHTML;
 | 
			
		||||
        this.question.readOnly = false;
 | 
			
		||||
 | 
			
		||||
        if (this.ddQuestion.initObjects) {
 | 
			
		||||
        if (this.question.initObjects) {
 | 
			
		||||
            // Moodle version = 3.5.
 | 
			
		||||
            if (this.ddQuestion.initObjects.drops !== undefined) {
 | 
			
		||||
                this.drops = <unknown[]> this.ddQuestion.initObjects.drops;
 | 
			
		||||
            if (this.question.initObjects.drops !== undefined) {
 | 
			
		||||
                this.drops = <unknown[]> this.question.initObjects.drops;
 | 
			
		||||
            }
 | 
			
		||||
            if (this.ddQuestion.initObjects.readonly !== undefined) {
 | 
			
		||||
                this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly;
 | 
			
		||||
            if (this.question.initObjects.readonly !== undefined) {
 | 
			
		||||
                this.question.readOnly = !!this.question.initObjects.readonly;
 | 
			
		||||
            }
 | 
			
		||||
        } else if (this.ddQuestion.amdArgs) {
 | 
			
		||||
        } else if (this.question.amdArgs) {
 | 
			
		||||
            // Moodle version >= 3.6.
 | 
			
		||||
            if (this.ddQuestion.amdArgs[1] !== undefined) {
 | 
			
		||||
                this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[1];
 | 
			
		||||
            if (this.question.amdArgs[1] !== undefined) {
 | 
			
		||||
                this.question.readOnly = !!this.question.amdArgs[1];
 | 
			
		||||
            }
 | 
			
		||||
            if (this.ddQuestion.amdArgs[2] !== undefined) {
 | 
			
		||||
                this.drops = <unknown[]> this.ddQuestion.amdArgs[2];
 | 
			
		||||
            if (this.question.amdArgs[2] !== undefined) {
 | 
			
		||||
                this.drops = <unknown[]> this.question.amdArgs[2];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion.loaded = false;
 | 
			
		||||
        this.question.loaded = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -114,12 +110,12 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
 | 
			
		||||
     * The question has been rendered.
 | 
			
		||||
     */
 | 
			
		||||
    protected questionRendered(): void {
 | 
			
		||||
        if (!this.destroyed && this.ddQuestion) {
 | 
			
		||||
        if (!this.destroyed && this.question) {
 | 
			
		||||
            // Create the instance.
 | 
			
		||||
            this.questionInstance = new AddonQtypeDdImageOrTextQuestion(
 | 
			
		||||
                this.hostElement,
 | 
			
		||||
                this.ddQuestion,
 | 
			
		||||
                !!this.ddQuestion.readOnly,
 | 
			
		||||
                this.question,
 | 
			
		||||
                !!this.question.readOnly,
 | 
			
		||||
                this.drops,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,23 @@
 | 
			
		||||
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')" class="addon-qtype-ddmarker-container">
 | 
			
		||||
<div *ngIf="question && (question.text || question.text === '')" class="addon-qtype-ddmarker-container">
 | 
			
		||||
    <!-- Content is outside the core-loading to let the script calculate drag items position -->
 | 
			
		||||
    <core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
 | 
			
		||||
    <core-loading [hideUntil]="question.loaded"></core-loading>
 | 
			
		||||
 | 
			
		||||
    <ion-item class="ion-text-wrap" [hidden]="!ddQuestion.loaded">
 | 
			
		||||
    <ion-item class="ion-text-wrap" [hidden]="!question.loaded">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
 | 
			
		||||
            <ion-card *ngIf="!question.readOnly" class="core-info-card">
 | 
			
		||||
                <ion-item>
 | 
			
		||||
                    <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                    <ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ion-card>
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" #questiontext
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" #questiontext
 | 
			
		||||
                [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
 | 
			
		||||
        <core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
 | 
			
		||||
            [text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
 | 
			
		||||
    <div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
 | 
			
		||||
        <core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
 | 
			
		||||
            [text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
 | 
			
		||||
        </core-format-text>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
 | 
			
		||||
import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
			
		||||
@ -29,12 +29,12 @@ import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
 | 
			
		||||
    templateUrl: 'addon-qtype-ddmarker.html',
 | 
			
		||||
    styleUrls: ['ddmarker.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
 | 
			
		||||
export class AddonQtypeDdMarkerComponent
 | 
			
		||||
    extends CoreQuestionBaseComponent<AddonQtypeDdMarkerQuestionData>
 | 
			
		||||
    implements OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild('questiontext') questionTextEl?: ElementRef;
 | 
			
		||||
 | 
			
		||||
    ddQuestion?: AddonQtypeDdMarkerQuestionData;
 | 
			
		||||
 | 
			
		||||
    protected questionInstance?: AddonQtypeDdMarkerQuestion;
 | 
			
		||||
    protected dropZones: unknown[] = []; // The drop zones received in the init object of the question.
 | 
			
		||||
    protected imgSrc?: string; // Background image URL.
 | 
			
		||||
@ -49,65 +49,64 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        if (!this.question) {
 | 
			
		||||
            this.logger.warn('Aborting because of no question received.');
 | 
			
		||||
 | 
			
		||||
            return CoreQuestionHelper.showComponentError(this.onAbort);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion = this.question;
 | 
			
		||||
        const element = CoreDomUtils.convertToElement(this.question.html);
 | 
			
		||||
        const questionElement = this.initComponent();
 | 
			
		||||
        if (!questionElement) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get D&D area, form and question text.
 | 
			
		||||
        const ddArea = element.querySelector('.ddarea');
 | 
			
		||||
        const ddForm = element.querySelector('.ddform');
 | 
			
		||||
        const ddArea = questionElement.querySelector('.ddarea');
 | 
			
		||||
        const ddForm = questionElement.querySelector('.ddform');
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
 | 
			
		||||
        if (!ddArea || !ddForm || this.ddQuestion.text === undefined) {
 | 
			
		||||
            this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
 | 
			
		||||
        if (!ddArea || !ddForm) {
 | 
			
		||||
            this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
 | 
			
		||||
 | 
			
		||||
            return CoreQuestionHelper.showComponentError(this.onAbort);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Build the D&D area HTML.
 | 
			
		||||
        this.ddQuestion.ddArea = ddArea.outerHTML;
 | 
			
		||||
        this.question.ddArea = ddArea.outerHTML;
 | 
			
		||||
 | 
			
		||||
        const wrongParts = element.querySelector('.wrongparts');
 | 
			
		||||
        const wrongParts = questionElement.querySelector('.wrongparts');
 | 
			
		||||
        if (wrongParts) {
 | 
			
		||||
            this.ddQuestion.ddArea += wrongParts.outerHTML;
 | 
			
		||||
            this.question.ddArea += wrongParts.outerHTML;
 | 
			
		||||
        }
 | 
			
		||||
        this.ddQuestion.ddArea += ddForm.outerHTML;
 | 
			
		||||
        this.ddQuestion.readOnly = false;
 | 
			
		||||
        this.question.ddArea += ddForm.outerHTML;
 | 
			
		||||
        this.question.readOnly = false;
 | 
			
		||||
 | 
			
		||||
        if (this.ddQuestion.initObjects) {
 | 
			
		||||
        if (this.question.initObjects) {
 | 
			
		||||
            // Moodle version = 3.5.
 | 
			
		||||
            if (this.ddQuestion.initObjects.dropzones !== undefined) {
 | 
			
		||||
                this.dropZones = <unknown[]> this.ddQuestion.initObjects.dropzones;
 | 
			
		||||
            if (this.question.initObjects.dropzones !== undefined) {
 | 
			
		||||
                this.dropZones = <unknown[]> this.question.initObjects.dropzones;
 | 
			
		||||
            }
 | 
			
		||||
            if (this.ddQuestion.initObjects.readonly !== undefined) {
 | 
			
		||||
                this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly;
 | 
			
		||||
            if (this.question.initObjects.readonly !== undefined) {
 | 
			
		||||
                this.question.readOnly = !!this.question.initObjects.readonly;
 | 
			
		||||
            }
 | 
			
		||||
        } else if (this.ddQuestion.amdArgs) {
 | 
			
		||||
        } else if (this.question.amdArgs) {
 | 
			
		||||
            // Moodle version >= 3.6.
 | 
			
		||||
            let nextIndex = 1;
 | 
			
		||||
            // Moodle version >= 3.9, imgSrc is not specified, do not advance index.
 | 
			
		||||
            if (this.ddQuestion.amdArgs[nextIndex] !== undefined && typeof this.ddQuestion.amdArgs[nextIndex] != 'boolean') {
 | 
			
		||||
                this.imgSrc = <string> this.ddQuestion.amdArgs[nextIndex];
 | 
			
		||||
            if (this.question.amdArgs[nextIndex] !== undefined && typeof this.question.amdArgs[nextIndex] !== 'boolean') {
 | 
			
		||||
                this.imgSrc = <string> this.question.amdArgs[nextIndex];
 | 
			
		||||
                nextIndex++;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.ddQuestion.amdArgs[nextIndex] !== undefined) {
 | 
			
		||||
                this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[nextIndex];
 | 
			
		||||
            if (this.question.amdArgs[nextIndex] !== undefined) {
 | 
			
		||||
                this.question.readOnly = !!this.question.amdArgs[nextIndex];
 | 
			
		||||
            }
 | 
			
		||||
            nextIndex++;
 | 
			
		||||
 | 
			
		||||
            if (this.ddQuestion.amdArgs[nextIndex] !== undefined) {
 | 
			
		||||
                this.dropZones = <unknown[]> this.ddQuestion.amdArgs[nextIndex];
 | 
			
		||||
            if (this.question.amdArgs[nextIndex] !== undefined) {
 | 
			
		||||
                this.dropZones = <unknown[]> this.question.amdArgs[nextIndex];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion.loaded = false;
 | 
			
		||||
        this.question.loaded = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -134,9 +133,10 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
 | 
			
		||||
     * The question has been rendered.
 | 
			
		||||
     */
 | 
			
		||||
    protected async questionRendered(): Promise<void> {
 | 
			
		||||
        if (this.destroyed) {
 | 
			
		||||
        if (this.destroyed || !this.question) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Download background image (3.6+ sites).
 | 
			
		||||
        let imgSrc = this.imgSrc;
 | 
			
		||||
        const site = CoreSites.getCurrentSite();
 | 
			
		||||
@ -160,8 +160,8 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
 | 
			
		||||
        // Create the instance.
 | 
			
		||||
        this.questionInstance = new AddonQtypeDdMarkerQuestion(
 | 
			
		||||
            this.hostElement,
 | 
			
		||||
            this.ddQuestion!,
 | 
			
		||||
            !!this.ddQuestion!.readOnly,
 | 
			
		||||
            this.question,
 | 
			
		||||
            !!this.question.readOnly,
 | 
			
		||||
            this.dropZones,
 | 
			
		||||
            imgSrc,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,20 @@
 | 
			
		||||
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')">
 | 
			
		||||
<div *ngIf="question && (question.text || question.text === '')">
 | 
			
		||||
    <!-- Content is outside the core-loading to let the script calculate drag items position -->
 | 
			
		||||
    <core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
 | 
			
		||||
    <core-loading [hideUntil]="question.loaded"></core-loading>
 | 
			
		||||
 | 
			
		||||
    <div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
 | 
			
		||||
        <ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
 | 
			
		||||
    <div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
 | 
			
		||||
        <ion-card *ngIf="!question.readOnly" class="core-info-card">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
        <div class="addon-qtype-ddwtos-container">
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" [contextLevel]="contextLevel"
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
 | 
			
		||||
                [contextInstanceId]="contextInstanceId" [courseId]="courseId" #questiontext (afterRender)="textRendered()">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
 | 
			
		||||
            <core-format-text *ngIf="ddQuestion.answers" [component]="component" [componentId]="componentId" [text]="ddQuestion.answers"
 | 
			
		||||
            <core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers"
 | 
			
		||||
                [filter]="false" (afterRender)="answersRendered()">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
 | 
			
		||||
import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
			
		||||
@ -27,12 +27,10 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
 | 
			
		||||
    templateUrl: 'addon-qtype-ddwtos.html',
 | 
			
		||||
    styleUrls: ['ddwtos.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
 | 
			
		||||
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent<AddonModQuizDdwtosQuestionData> implements OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild('questiontext') questionTextEl?: ElementRef;
 | 
			
		||||
 | 
			
		||||
    ddQuestion?: AddonModQuizDdwtosQuestionData;
 | 
			
		||||
 | 
			
		||||
    protected questionInstance?: AddonQtypeDdwtosQuestion;
 | 
			
		||||
    protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored).
 | 
			
		||||
    protected destroyed = false;
 | 
			
		||||
@ -46,52 +44,50 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        if (!this.question) {
 | 
			
		||||
            this.logger.warn('Aborting because of no question received.');
 | 
			
		||||
 | 
			
		||||
            return CoreQuestionHelper.showComponentError(this.onAbort);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion = this.question;
 | 
			
		||||
        const element = CoreDomUtils.convertToElement(this.ddQuestion.html);
 | 
			
		||||
        const questionElement = this.initComponent();
 | 
			
		||||
        if (!questionElement) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Replace Moodle's correct/incorrect and feedback classes with our own.
 | 
			
		||||
        CoreQuestionHelper.replaceCorrectnessClasses(element);
 | 
			
		||||
        CoreQuestionHelper.replaceFeedbackClasses(element);
 | 
			
		||||
        CoreQuestionHelper.replaceCorrectnessClasses(questionElement);
 | 
			
		||||
        CoreQuestionHelper.replaceFeedbackClasses(questionElement);
 | 
			
		||||
 | 
			
		||||
        // Treat the correct/incorrect icons.
 | 
			
		||||
        CoreQuestionHelper.treatCorrectnessIcons(element);
 | 
			
		||||
        CoreQuestionHelper.treatCorrectnessIcons(questionElement);
 | 
			
		||||
 | 
			
		||||
        const answerContainer = element.querySelector('.answercontainer');
 | 
			
		||||
        const answerContainer = questionElement.querySelector('.answercontainer');
 | 
			
		||||
        if (!answerContainer) {
 | 
			
		||||
            this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
 | 
			
		||||
            this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
 | 
			
		||||
 | 
			
		||||
            return CoreQuestionHelper.showComponentError(this.onAbort);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion.readOnly = answerContainer.classList.contains('readonly');
 | 
			
		||||
        this.ddQuestion.answers = answerContainer.outerHTML;
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
 | 
			
		||||
        if (this.ddQuestion.text === undefined) {
 | 
			
		||||
            this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
 | 
			
		||||
 | 
			
		||||
            return CoreQuestionHelper.showComponentError(this.onAbort);
 | 
			
		||||
        }
 | 
			
		||||
        this.question.readOnly = answerContainer.classList.contains('readonly');
 | 
			
		||||
        this.question.answers = answerContainer.outerHTML;
 | 
			
		||||
 | 
			
		||||
        // Get the inputs where the answers will be stored and add them to the question text.
 | 
			
		||||
        const inputEls = <HTMLElement[]> Array.from(element.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])'));
 | 
			
		||||
        const inputEls = Array.from(
 | 
			
		||||
            questionElement.querySelectorAll<HTMLInputElement>('input[type="hidden"]:not([name*=sequencecheck])'),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let questionText = this.question.text;
 | 
			
		||||
        inputEls.forEach((inputEl) => {
 | 
			
		||||
            this.ddQuestion!.text += inputEl.outerHTML;
 | 
			
		||||
            questionText += inputEl.outerHTML;
 | 
			
		||||
            const id = inputEl.getAttribute('id');
 | 
			
		||||
            if (id) {
 | 
			
		||||
                this.inputIds.push(id);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion.loaded = false;
 | 
			
		||||
        this.question.text = questionText;
 | 
			
		||||
 | 
			
		||||
        this.question.loaded = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -118,7 +114,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
 | 
			
		||||
     * The question has been rendered.
 | 
			
		||||
     */
 | 
			
		||||
    protected async questionRendered(): Promise<void> {
 | 
			
		||||
        if (this.destroyed) {
 | 
			
		||||
        if (this.destroyed || !this.question) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -129,8 +125,8 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
 | 
			
		||||
        // Create the instance.
 | 
			
		||||
        this.questionInstance = new AddonQtypeDdwtosQuestion(
 | 
			
		||||
            this.hostElement,
 | 
			
		||||
            this.ddQuestion!,
 | 
			
		||||
            !!this.ddQuestion!.readOnly,
 | 
			
		||||
            this.question,
 | 
			
		||||
            !!this.question.readOnly,
 | 
			
		||||
            this.inputIds,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@ -143,7 +139,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
 | 
			
		||||
            this.courseId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.ddQuestion!.loaded = true;
 | 
			
		||||
        this.question.loaded = true;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { Component, ElementRef } from '@angular/core';
 | 
			
		||||
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -23,7 +22,7 @@ import { CoreQuestionBaseComponent } from '@features/question/classes/base-quest
 | 
			
		||||
    selector: 'addon-qtype-description',
 | 
			
		||||
    templateUrl: 'addon-qtype-description.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent implements OnInit {
 | 
			
		||||
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent {
 | 
			
		||||
 | 
			
		||||
    seenInput?: { name: string; value: string };
 | 
			
		||||
 | 
			
		||||
@ -34,20 +33,22 @@ export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent im
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        const questionEl = this.initComponent();
 | 
			
		||||
        if (!questionEl) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get the "seen" hidden input.
 | 
			
		||||
        const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=seen]');
 | 
			
		||||
        if (input) {
 | 
			
		||||
            this.seenInput = {
 | 
			
		||||
                name: input.name,
 | 
			
		||||
                value: input.value,
 | 
			
		||||
            };
 | 
			
		||||
        const input = questionEl.querySelector<HTMLInputElement>('input[type="hidden"][name*=seen]');
 | 
			
		||||
        if (!input) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.seenInput = {
 | 
			
		||||
            name: input.name,
 | 
			
		||||
            value: input.value,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
<ion-list *ngIf="essayQuestion && (essayQuestion.text || essayQuestion.text === '')">
 | 
			
		||||
<ion-list *ngIf="question && (question.text || question.text === '')">
 | 
			
		||||
    <!-- Question text. -->
 | 
			
		||||
    <ion-item class="ion-text-wrap">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.text" [contextLevel]="contextLevel"
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
 | 
			
		||||
                [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
@ -11,27 +11,26 @@
 | 
			
		||||
    <!-- Editing the question. -->
 | 
			
		||||
    <ng-container *ngIf="!review">
 | 
			
		||||
        <!-- Textarea. -->
 | 
			
		||||
        <ion-item *ngIf="essayQuestion.textarea && (!essayQuestion.hasDraftFiles || uploadFilesSupported)">
 | 
			
		||||
        <ion-item *ngIf="question.textarea && (!question.hasDraftFiles || uploadFilesSupported)">
 | 
			
		||||
            <ion-label class="sr-only">{{ 'core.question.answer' | translate }}</ion-label>
 | 
			
		||||
            <!-- "Format" and draftid hidden inputs -->
 | 
			
		||||
            <input *ngIf="essayQuestion.formatInput" type="hidden" [name]="essayQuestion.formatInput.name"
 | 
			
		||||
                [value]="essayQuestion.formatInput.value">
 | 
			
		||||
            <input *ngIf="essayQuestion.answerDraftIdInput" type="hidden" [name]="essayQuestion.answerDraftIdInput.name"
 | 
			
		||||
                [value]="essayQuestion.answerDraftIdInput.value">
 | 
			
		||||
            <input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value">
 | 
			
		||||
            <input *ngIf="question.answerDraftIdInput" type="hidden" [name]="question.answerDraftIdInput.name"
 | 
			
		||||
                [value]="question.answerDraftIdInput.value">
 | 
			
		||||
            <!-- Plain text textarea. -->
 | 
			
		||||
            <ion-textarea *ngIf="essayQuestion.isPlainText" class="core-question-textarea"
 | 
			
		||||
                [ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}"
 | 
			
		||||
                [attr.name]="essayQuestion.textarea.name" [ngModel]="essayQuestion.textarea.text">
 | 
			
		||||
            <ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}'
 | 
			
		||||
                placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name"
 | 
			
		||||
                [ngModel]="question.textarea.text">
 | 
			
		||||
            </ion-textarea>
 | 
			
		||||
            <!-- Rich text editor. -->
 | 
			
		||||
            <core-rich-text-editor *ngIf="!essayQuestion.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
 | 
			
		||||
                [control]="formControl" [name]="essayQuestion.textarea.name" [component]="component" [componentId]="componentId"
 | 
			
		||||
            <core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
 | 
			
		||||
                [control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId"
 | 
			
		||||
                [autoSave]="false">
 | 
			
		||||
            </core-rich-text-editor>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <!-- Draft files not supported. -->
 | 
			
		||||
        <ng-container *ngIf="essayQuestion.textarea && essayQuestion.hasDraftFiles && !uploadFilesSupported">
 | 
			
		||||
        <ng-container *ngIf="question.textarea && question.hasDraftFiles && !uploadFilesSupported">
 | 
			
		||||
            <ion-item class="ion-text-wrap core-danger-item">
 | 
			
		||||
                <ion-label class="core-question-warning">
 | 
			
		||||
                    {{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }}
 | 
			
		||||
@ -39,7 +38,7 @@
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item class="ion-text-wrap">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.textarea.text"
 | 
			
		||||
                    <core-format-text [component]="component" [componentId]="componentId" [text]="question.textarea.text"
 | 
			
		||||
                        [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
                    </core-format-text>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
@ -47,15 +46,14 @@
 | 
			
		||||
        </ng-container>
 | 
			
		||||
 | 
			
		||||
        <!-- Attachments. -->
 | 
			
		||||
        <ng-container *ngIf="essayQuestion.allowsAttachments">
 | 
			
		||||
            <core-attachments *ngIf="uploadFilesSupported && essayQuestion.attachmentsDraftIdInput" [files]="attachments"
 | 
			
		||||
                [component]="component" [componentId]="componentId" [maxSize]="essayQuestion.attachmentsMaxBytes"
 | 
			
		||||
                [maxSubmissions]="essayQuestion.attachmentsMaxFiles" [allowOffline]="offlineEnabled"
 | 
			
		||||
                [acceptedTypes]="essayQuestion.attachmentsAcceptedTypes" [courseId]="courseId">
 | 
			
		||||
        <ng-container *ngIf="question.allowsAttachments">
 | 
			
		||||
            <core-attachments *ngIf="uploadFilesSupported && question.attachmentsDraftIdInput" [files]="attachments" [component]="component"
 | 
			
		||||
                [componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles"
 | 
			
		||||
                [allowOffline]="offlineEnabled" [acceptedTypes]="question.attachmentsAcceptedTypes" [courseId]="courseId">
 | 
			
		||||
            </core-attachments>
 | 
			
		||||
 | 
			
		||||
            <input *ngIf="essayQuestion.attachmentsDraftIdInput" type="hidden" [name]="essayQuestion.attachmentsDraftIdInput.name"
 | 
			
		||||
                [value]="essayQuestion.attachmentsDraftIdInput.value">
 | 
			
		||||
            <input *ngIf="question.attachmentsDraftIdInput" type="hidden" [name]="question.attachmentsDraftIdInput.name"
 | 
			
		||||
                [value]="question.attachmentsDraftIdInput.value">
 | 
			
		||||
 | 
			
		||||
            <!-- Attachments not supported in this site. -->
 | 
			
		||||
            <ion-item *ngIf="!uploadFilesSupported" class="ion-text-wrap core-danger-item">
 | 
			
		||||
@ -69,36 +67,35 @@
 | 
			
		||||
    <!-- Reviewing the question. -->
 | 
			
		||||
    <ng-container *ngIf="review">
 | 
			
		||||
        <!-- Answer to the question and attachments (reviewing). -->
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="essayQuestion.answer || essayQuestion.answer == ''">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="question.answer || question.answer == ''">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <core-format-text [ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' [component]="component"
 | 
			
		||||
                    [componentId]="componentId" [text]="essayQuestion.answer" [contextLevel]="contextLevel"
 | 
			
		||||
                    [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
                <core-format-text [ngClass]='{"core-monospaced": question.isMonospaced}' [component]="component" [componentId]="componentId"
 | 
			
		||||
                    [text]="question.answer" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
                </core-format-text>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <!-- Word count info. -->
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="essayQuestion.wordCountInfo">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="question.wordCountInfo">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.wordCountInfo"
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="question.wordCountInfo"
 | 
			
		||||
                    [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
                </core-format-text>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <!-- Answer plagiarism. -->
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="essayQuestion.answerPlagiarism">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="question.answerPlagiarism">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.answerPlagiarism"
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="question.answerPlagiarism"
 | 
			
		||||
                    [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
                </core-format-text>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <!-- List of attachments when reviewing. -->
 | 
			
		||||
        <core-files *ngIf="essayQuestion.attachments" [files]="essayQuestion.attachments" [component]="component"
 | 
			
		||||
            [componentId]="componentId" [extraHtml]="essayQuestion.attachmentsPlagiarisms">
 | 
			
		||||
        <core-files *ngIf="question.attachments" [files]="question.attachments" [component]="component" [componentId]="componentId"
 | 
			
		||||
            [extraHtml]="question.attachmentsPlagiarisms">
 | 
			
		||||
        </core-files>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
</ion-list>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormControl } from '@angular/forms';
 | 
			
		||||
import { FileEntry } from '@ionic-native/file/ngx';
 | 
			
		||||
 | 
			
		||||
@ -30,12 +30,11 @@ import { CoreFileEntry } from '@services/file-helper';
 | 
			
		||||
    selector: 'addon-qtype-essay',
 | 
			
		||||
    templateUrl: 'addon-qtype-essay.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit {
 | 
			
		||||
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent<AddonModQuizEssayQuestion> {
 | 
			
		||||
 | 
			
		||||
    formControl?: FormControl;
 | 
			
		||||
    attachments?: CoreFileEntry[];
 | 
			
		||||
    uploadFilesSupported = false;
 | 
			
		||||
    essayQuestion?: AddonModQuizEssayQuestion;
 | 
			
		||||
 | 
			
		||||
    constructor(elementRef: ElementRef, protected fb: FormBuilder) {
 | 
			
		||||
        super('AddonQtypeEssayComponent', elementRef);
 | 
			
		||||
@ -44,14 +43,18 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.uploadFilesSupported = this.question?.responsefileareas !== undefined;
 | 
			
		||||
    init(): void {
 | 
			
		||||
        if (!this.question) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.uploadFilesSupported = this.question.responsefileareas !== undefined;
 | 
			
		||||
 | 
			
		||||
        this.initEssayComponent(this.review);
 | 
			
		||||
        this.essayQuestion = this.question;
 | 
			
		||||
 | 
			
		||||
        this.formControl = this.fb.control(this.essayQuestion?.textarea?.text);
 | 
			
		||||
        this.formControl = this.fb.control(this.question?.textarea?.text);
 | 
			
		||||
 | 
			
		||||
        if (this.essayQuestion?.allowsAttachments && this.uploadFilesSupported && !this.review) {
 | 
			
		||||
        if (this.question?.allowsAttachments && this.uploadFilesSupported && !this.review) {
 | 
			
		||||
            this.loadAttachments();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -62,10 +65,14 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async loadAttachments(): Promise<void> {
 | 
			
		||||
        if (this.offlineEnabled && this.essayQuestion?.localAnswers?.attachments_offline) {
 | 
			
		||||
        if (!this.question) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.offlineEnabled && this.question.localAnswers?.attachments_offline) {
 | 
			
		||||
 | 
			
		||||
            const attachmentsData: CoreFileUploaderStoreFilesResult = CoreTextUtils.parseJSON(
 | 
			
		||||
                this.essayQuestion.localAnswers.attachments_offline,
 | 
			
		||||
                this.question.localAnswers.attachments_offline,
 | 
			
		||||
                {
 | 
			
		||||
                    online: [],
 | 
			
		||||
                    offline: 0,
 | 
			
		||||
@ -75,7 +82,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
 | 
			
		||||
 | 
			
		||||
            if (attachmentsData.offline) {
 | 
			
		||||
                offlineFiles = <FileEntry[]> await CoreQuestionHelper.getStoredQuestionFiles(
 | 
			
		||||
                    this.essayQuestion,
 | 
			
		||||
                    this.question,
 | 
			
		||||
                    this.component || '',
 | 
			
		||||
                    this.componentId || -1,
 | 
			
		||||
                );
 | 
			
		||||
@ -83,12 +90,12 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
 | 
			
		||||
 | 
			
		||||
            this.attachments = [...attachmentsData.online, ...offlineFiles];
 | 
			
		||||
        } else {
 | 
			
		||||
            this.attachments = Array.from(CoreQuestionHelper.getResponseFileAreaFiles(this.question!, 'attachments'));
 | 
			
		||||
            this.attachments = Array.from(CoreQuestionHelper.getResponseFileAreaFiles(this.question, 'attachments'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreFileSession.setFiles(
 | 
			
		||||
            this.component || '',
 | 
			
		||||
            CoreQuestion.getQuestionComponentId(this.question!, this.componentId || -1),
 | 
			
		||||
            CoreQuestion.getQuestionComponentId(this.question, this.componentId || -1),
 | 
			
		||||
            this.attachments,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component,  ElementRef } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
			
		||||
@ -25,7 +25,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper'
 | 
			
		||||
    templateUrl: 'addon-qtype-gapselect.html',
 | 
			
		||||
    styleUrls: ['gapselect.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent implements OnInit {
 | 
			
		||||
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent {
 | 
			
		||||
 | 
			
		||||
    constructor(elementRef: ElementRef) {
 | 
			
		||||
        super('AddonQtypeGapSelectComponent', elementRef);
 | 
			
		||||
@ -34,7 +34,7 @@ export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent impl
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        this.initOriginalTextComponent('.qtext');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,12 @@
 | 
			
		||||
<section class="addon-qtype-match-container" *ngIf="matchQuestion && matchQuestion.loaded">
 | 
			
		||||
<section class="addon-qtype-match-container" *ngIf="question && question.loaded">
 | 
			
		||||
    <ion-item class="ion-text-wrap">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="matchQuestion.text" [contextLevel]="contextLevel"
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
 | 
			
		||||
                [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <ion-item class="ion-text-wrap" *ngFor="let row of matchQuestion.rows">
 | 
			
		||||
    <ion-item class="ion-text-wrap" *ngFor="let row of question.rows">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId"
 | 
			
		||||
                [text]="row.text" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
 | 
			
		||||
@ -24,9 +24,7 @@ import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/
 | 
			
		||||
    templateUrl: 'addon-qtype-match.html',
 | 
			
		||||
    styleUrls: ['match.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    matchQuestion?: AddonModQuizMatchQuestion;
 | 
			
		||||
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent<AddonModQuizMatchQuestion> {
 | 
			
		||||
 | 
			
		||||
    constructor(elementRef: ElementRef) {
 | 
			
		||||
        super('AddonQtypeMatchComponent', elementRef);
 | 
			
		||||
@ -35,9 +33,8 @@ export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implemen
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        this.initMatchComponent();
 | 
			
		||||
        this.matchQuestion = this.question;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef } from '@angular/core';
 | 
			
		||||
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper'
 | 
			
		||||
    templateUrl: 'addon-qtype-multianswer.html',
 | 
			
		||||
    styleUrls: ['multianswer.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
 | 
			
		||||
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent {
 | 
			
		||||
 | 
			
		||||
    constructor(elementRef: ElementRef) {
 | 
			
		||||
        super('AddonQtypeMultiAnswerComponent', elementRef);
 | 
			
		||||
@ -33,7 +33,7 @@ export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent im
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        this.initOriginalTextComponent('.formulation');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,23 @@
 | 
			
		||||
<ion-list *ngIf="multiQuestion && (multiQuestion.text || multiQuestion.text === '')">
 | 
			
		||||
<ion-list *ngIf="question && (question.text || question.text === '')">
 | 
			
		||||
    <!-- Question text first. -->
 | 
			
		||||
    <ion-item class="ion-text-wrap">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <p>
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.text"
 | 
			
		||||
                    [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
 | 
			
		||||
                    [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
                </core-format-text>
 | 
			
		||||
            </p>
 | 
			
		||||
            <p *ngIf="multiQuestion.prompt">
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.prompt"
 | 
			
		||||
                    [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
            <p *ngIf="question.prompt">
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="question.prompt" [contextLevel]="contextLevel"
 | 
			
		||||
                    [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
                </core-format-text>
 | 
			
		||||
            </p>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
 | 
			
		||||
    <!-- Checkbox for multiple choice. -->
 | 
			
		||||
    <ng-container *ngIf="multiQuestion.multi">
 | 
			
		||||
        <ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options">
 | 
			
		||||
    <ng-container *ngIf="question.multi">
 | 
			
		||||
        <ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
 | 
			
		||||
            <ion-label [color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")' [class]="option.class">
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
 | 
			
		||||
                    [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
@ -44,8 +44,8 @@
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <!-- Radio buttons for single choice. -->
 | 
			
		||||
    <ion-radio-group *ngIf="!multiQuestion.multi" [(ngModel)]="multiQuestion.singleChoiceModel" [name]="multiQuestion.optionsName">
 | 
			
		||||
        <ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options">
 | 
			
		||||
    <ion-radio-group *ngIf="!question.multi" [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
 | 
			
		||||
        <ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
 | 
			
		||||
            <ion-label [class]="option.class">
 | 
			
		||||
                <core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
 | 
			
		||||
                    [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
@ -66,12 +66,12 @@
 | 
			
		||||
            <ion-icon *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-times" color="danger"
 | 
			
		||||
                [attr.aria-label]="'core.question.incorrect' | translate"></ion-icon>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
        <ion-button *ngIf="!multiQuestion.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
 | 
			
		||||
            [disabled]="!multiQuestion.singleChoiceModel" (click)="clear()" type="button">
 | 
			
		||||
        <ion-button *ngIf="!question.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
 | 
			
		||||
            [disabled]="!question.singleChoiceModel" (click)="clear()" type="button">
 | 
			
		||||
            {{ 'addon.mod_quiz.clearchoice' | translate }}
 | 
			
		||||
        </ion-button>
 | 
			
		||||
 | 
			
		||||
        <!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
 | 
			
		||||
        <input type="hidden" [ngModel]="multiQuestion.singleChoiceModel" [attr.name]="multiQuestion.optionsName">
 | 
			
		||||
        <input type="hidden" [ngModel]="question.singleChoiceModel" [attr.name]="question.optionsName">
 | 
			
		||||
    </ion-radio-group>
 | 
			
		||||
</ion-list>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
 | 
			
		||||
@ -24,9 +24,7 @@ import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@fea
 | 
			
		||||
    templateUrl: 'addon-qtype-multichoice.html',
 | 
			
		||||
    styleUrls: ['multichoice.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    multiQuestion?: AddonModQuizMultichoiceQuestion;
 | 
			
		||||
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent<AddonModQuizMultichoiceQuestion> {
 | 
			
		||||
 | 
			
		||||
    constructor(elementRef: ElementRef) {
 | 
			
		||||
        super('AddonQtypeMultichoiceComponent', elementRef);
 | 
			
		||||
@ -35,16 +33,19 @@ export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent im
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        this.initMultichoiceComponent();
 | 
			
		||||
        this.multiQuestion = this.question;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Clear selected choices.
 | 
			
		||||
     */
 | 
			
		||||
    clear(): void {
 | 
			
		||||
        this.multiQuestion!.singleChoiceModel = undefined;
 | 
			
		||||
        if (!this.question) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.question.singleChoiceModel = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -117,7 +117,7 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
 | 
			
		||||
 | 
			
		||||
        // To know if it's single or multi answer we need to search for answers with "choice" in the name.
 | 
			
		||||
        for (const name in newAnswers) {
 | 
			
		||||
            if (name.indexOf('choice') != -1) {
 | 
			
		||||
            if (name.indexOf('choice') !== -1) {
 | 
			
		||||
                isSingle = false;
 | 
			
		||||
                if (!CoreUtils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, name)) {
 | 
			
		||||
                    isMultiSame = false;
 | 
			
		||||
@ -128,9 +128,9 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
 | 
			
		||||
 | 
			
		||||
        if (isSingle) {
 | 
			
		||||
            return this.isSameResponseSingle(prevAnswers, newAnswers);
 | 
			
		||||
        } else {
 | 
			
		||||
            return isMultiSame;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return isMultiSame;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -151,10 +151,11 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
 | 
			
		||||
        question: AddonModQuizMultichoiceQuestion,
 | 
			
		||||
        answers: CoreQuestionsAnswers,
 | 
			
		||||
    ): void {
 | 
			
		||||
        if (question && !question.multi && answers[question.optionsName!] !== undefined && !answers[question.optionsName!]) {
 | 
			
		||||
        if (question && !question.multi &&
 | 
			
		||||
            question.optionsName && answers[question.optionsName] !== undefined && !answers[question.optionsName]) {
 | 
			
		||||
            /* It's a single choice and the user hasn't answered. Delete the answer because
 | 
			
		||||
               sending an empty string (default value) will mark the first option as selected. */
 | 
			
		||||
            delete answers[question.optionsName!];
 | 
			
		||||
            delete answers[question.optionsName];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,19 @@
 | 
			
		||||
<ion-list *ngIf="textQuestion && (textQuestion.text || textQuestion.text === '')">
 | 
			
		||||
<ion-list *ngIf="question && (question.text || question.text === '')">
 | 
			
		||||
    <ion-item class="ion-text-wrap addon-qtype-shortanswer-text">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="textQuestion.text" [contextLevel]="contextLevel"
 | 
			
		||||
            <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
 | 
			
		||||
                [contextInstanceId]="contextInstanceId" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <ion-item *ngIf="textQuestion.input && !textQuestion.input.isInline"
 | 
			
		||||
        class="ion-text-wrap addon-qtype-shortanswer-input core-{{textQuestion.input.correctIconColor}}-item">
 | 
			
		||||
    <ion-item *ngIf="question.input && !question.input.isInline"
 | 
			
		||||
        class="ion-text-wrap addon-qtype-shortanswer-input core-{{question.input.correctIconColor}}-item">
 | 
			
		||||
        <ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
 | 
			
		||||
        <ion-input type="text" [placeholder]="textQuestion.input.readOnly ? '' : 'core.question.answer' | translate"
 | 
			
		||||
            [attr.name]="textQuestion.input.name" [value]="textQuestion.input.value" autocorrect="off"
 | 
			
		||||
            [disabled]="textQuestion.input.readOnly">
 | 
			
		||||
        <ion-input type="text" [placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate"
 | 
			
		||||
            [attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly">
 | 
			
		||||
        </ion-input>
 | 
			
		||||
        <ion-icon *ngIf="textQuestion.input.correctIcon" class="core-correct-icon" slot="end" [name]="textQuestion.input.correctIcon"
 | 
			
		||||
            [color]="[textQuestion.input.correctIconColor]">
 | 
			
		||||
        <ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon" slot="end" [name]="question.input.correctIcon"
 | 
			
		||||
            [color]="[question.input.correctIconColor]">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
</ion-list>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AddonModQuizTextQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
			
		||||
 | 
			
		||||
@ -24,9 +24,7 @@ import { AddonModQuizTextQuestion, CoreQuestionBaseComponent } from '@features/q
 | 
			
		||||
    templateUrl: 'addon-qtype-shortanswer.html',
 | 
			
		||||
    styleUrls: ['shortanswer.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    textQuestion?: AddonModQuizTextQuestion;
 | 
			
		||||
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent<AddonModQuizTextQuestion> {
 | 
			
		||||
 | 
			
		||||
    constructor(elementRef: ElementRef) {
 | 
			
		||||
        super('AddonQtypeShortAnswerComponent', elementRef);
 | 
			
		||||
@ -35,9 +33,8 @@ export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent im
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
    init(): void {
 | 
			
		||||
        this.initInputTextComponent();
 | 
			
		||||
        this.textQuestion = this.question;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -83,9 +83,9 @@ export class AddonQtypeTrueFalseHandlerService implements CoreQuestionHandler {
 | 
			
		||||
        question: AddonModQuizMultichoiceQuestion,
 | 
			
		||||
        answers: CoreQuestionsAnswers,
 | 
			
		||||
    ): void | Promise<void> {
 | 
			
		||||
        if (question && answers[question.optionsName!] !== undefined && !answers[question.optionsName!]) {
 | 
			
		||||
        if (question && question.optionsName && answers[question.optionsName] !== undefined && !answers[question.optionsName]) {
 | 
			
		||||
            // The user hasn't answered. Delete the answer to prevent marking one of the answers automatically.
 | 
			
		||||
            delete answers[question.optionsName!];
 | 
			
		||||
            delete answers[question.optionsName];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -145,7 +145,7 @@ export function conditionalRoutes(routes: Routes, condition: () => boolean): Rou
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            ...newRoute,
 | 
			
		||||
            matcher: buildConditionalUrlMatcher(matcher || path!, condition),
 | 
			
		||||
            matcher: buildConditionalUrlMatcher(matcher || path || '', condition),
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -91,7 +91,12 @@ export class CoreQueueRunner {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const item = this.orderedQueue.shift()!;
 | 
			
		||||
        const item = this.orderedQueue.shift();
 | 
			
		||||
        if (!item) {
 | 
			
		||||
            // No item found.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.numberRunning++;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
 | 
			
		||||
@ -174,7 +174,8 @@ export class SQLiteDB {
 | 
			
		||||
            sql = equal ? '= ?' : '<> ?';
 | 
			
		||||
            params = Array.isArray(items) ? items : [items];
 | 
			
		||||
        } else {
 | 
			
		||||
            sql = (equal ? '' : 'NOT ') + 'IN (' + ',?'.repeat(items.length).substring(1) + ')';
 | 
			
		||||
            const questionMarks = ',?'.repeat(items.length).substring(1);
 | 
			
		||||
            sql = (equal ? '' : 'NOT ') + `IN (${questionMarks})`;
 | 
			
		||||
            params = items;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -237,7 +238,7 @@ export class SQLiteDB {
 | 
			
		||||
        tableCheck?: string,
 | 
			
		||||
    ): string {
 | 
			
		||||
        const columnsSql: string[] = [];
 | 
			
		||||
        let sql = `CREATE TABLE IF NOT EXISTS ${name} (`;
 | 
			
		||||
        let tableStructureSQL = '';
 | 
			
		||||
 | 
			
		||||
        // First define all the columns.
 | 
			
		||||
        for (const index in columns) {
 | 
			
		||||
@ -245,7 +246,7 @@ export class SQLiteDB {
 | 
			
		||||
            let columnSql: string = column.name || '';
 | 
			
		||||
 | 
			
		||||
            if (column.type) {
 | 
			
		||||
                columnSql += ' ' + column.type;
 | 
			
		||||
                columnSql += ` ${column.type}`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (column.primaryKey) {
 | 
			
		||||
@ -273,25 +274,25 @@ export class SQLiteDB {
 | 
			
		||||
 | 
			
		||||
            columnsSql.push(columnSql);
 | 
			
		||||
        }
 | 
			
		||||
        sql += columnsSql.join(', ');
 | 
			
		||||
        tableStructureSQL += columnsSql.join(', ');
 | 
			
		||||
 | 
			
		||||
        // Now add the table constraints.
 | 
			
		||||
 | 
			
		||||
        if (primaryKeys && primaryKeys.length) {
 | 
			
		||||
            sql += `, PRIMARY KEY (${primaryKeys.join(', ')})`;
 | 
			
		||||
            tableStructureSQL += `, PRIMARY KEY (${primaryKeys.join(', ')})`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (uniqueKeys && uniqueKeys.length) {
 | 
			
		||||
            for (const index in uniqueKeys) {
 | 
			
		||||
                const setOfKeys = uniqueKeys[index];
 | 
			
		||||
                if (setOfKeys && setOfKeys.length) {
 | 
			
		||||
                    sql += `, UNIQUE (${setOfKeys.join(', ')})`;
 | 
			
		||||
                    tableStructureSQL += `, UNIQUE (${setOfKeys.join(', ')})`;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tableCheck) {
 | 
			
		||||
            sql += `, CHECK (${tableCheck})`;
 | 
			
		||||
            tableStructureSQL += `, CHECK (${tableCheck})`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const index in foreignKeys) {
 | 
			
		||||
@ -301,18 +302,18 @@ export class SQLiteDB {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            sql += `, FOREIGN KEY (${foreignKey.columns.join(', ')}) REFERENCES ${foreignKey.table} `;
 | 
			
		||||
            tableStructureSQL += `, FOREIGN KEY (${foreignKey.columns.join(', ')}) REFERENCES ${foreignKey.table} `;
 | 
			
		||||
 | 
			
		||||
            if (foreignKey.foreignColumns && foreignKey.foreignColumns.length) {
 | 
			
		||||
                sql += `(${foreignKey.foreignColumns.join(', ')})`;
 | 
			
		||||
                tableStructureSQL += `(${foreignKey.foreignColumns.join(', ')})`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (foreignKey.actions) {
 | 
			
		||||
                sql += ` ${foreignKey.actions}`;
 | 
			
		||||
                tableStructureSQL += ` ${foreignKey.actions}`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return sql + ')';
 | 
			
		||||
        return `CREATE TABLE IF NOT EXISTS ${name} (${tableStructureSQL})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -323,7 +324,7 @@ export class SQLiteDB {
 | 
			
		||||
    async close(): Promise<void> {
 | 
			
		||||
        await this.ready();
 | 
			
		||||
 | 
			
		||||
        await this.db!.close();
 | 
			
		||||
        await this.db?.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -355,7 +356,7 @@ export class SQLiteDB {
 | 
			
		||||
        countItem: string = 'COUNT(\'x\')',
 | 
			
		||||
    ): Promise<number> {
 | 
			
		||||
        if (select) {
 | 
			
		||||
            select = 'WHERE ' + select;
 | 
			
		||||
            select = `WHERE ${select}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.countRecordsSql(`SELECT ${countItem} FROM ${table} ${select}`, params);
 | 
			
		||||
@ -470,7 +471,7 @@ export class SQLiteDB {
 | 
			
		||||
     */
 | 
			
		||||
    async deleteRecordsSelect(table: string, select: string = '', params?: SQLiteDBRecordValue[]): Promise<number> {
 | 
			
		||||
        if (select) {
 | 
			
		||||
            select = 'WHERE ' + select;
 | 
			
		||||
            select = `WHERE ${select}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const result = await this.execute(`DELETE FROM ${table} ${select}`, params);
 | 
			
		||||
@ -501,7 +502,7 @@ export class SQLiteDB {
 | 
			
		||||
    async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise<any> {
 | 
			
		||||
        await this.ready();
 | 
			
		||||
 | 
			
		||||
        return this.db!.executeSql(sql, params);
 | 
			
		||||
        return this.db?.executeSql(sql, params);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -516,7 +517,7 @@ export class SQLiteDB {
 | 
			
		||||
    async executeBatch(sqlStatements: (string | string[] | any)[]): Promise<void> {
 | 
			
		||||
        await this.ready();
 | 
			
		||||
 | 
			
		||||
        await this.db!.sqlBatch(sqlStatements);
 | 
			
		||||
        await this.db?.sqlBatch(sqlStatements);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -529,22 +530,8 @@ export class SQLiteDB {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove undefined entries and convert null to "NULL".
 | 
			
		||||
        for (const name in data) {
 | 
			
		||||
            if (data[name] === undefined) {
 | 
			
		||||
                delete data[name];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Format the data to where params.
 | 
			
		||||
     *
 | 
			
		||||
     * @param data Object data.
 | 
			
		||||
     * @returns List of params.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] {
 | 
			
		||||
        return Object.keys(data).map((key) => data[key]!);
 | 
			
		||||
        // Remove undefined entries.
 | 
			
		||||
        Object.keys(data).forEach(key => data[key] === undefined && delete data[key]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -587,7 +574,7 @@ export class SQLiteDB {
 | 
			
		||||
        params?: SQLiteDBRecordValue[],
 | 
			
		||||
    ): Promise<SQLiteDBRecordValue> {
 | 
			
		||||
        if (select) {
 | 
			
		||||
            select = 'WHERE ' + select;
 | 
			
		||||
            select = `WHERE ${select}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.getFieldSql(`SELECT ${field} FROM ${table} ${select}`, params);
 | 
			
		||||
@ -648,7 +635,7 @@ export class SQLiteDB {
 | 
			
		||||
        fields: string = '*',
 | 
			
		||||
    ): Promise<T> {
 | 
			
		||||
        if (select) {
 | 
			
		||||
            select = ' WHERE ' + select;
 | 
			
		||||
            select = ` WHERE ${select}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.getRecordSql<T>(`SELECT ${fields} FROM ${table} ${select}`, params);
 | 
			
		||||
@ -746,10 +733,10 @@ export class SQLiteDB {
 | 
			
		||||
        limitNum: number = 0,
 | 
			
		||||
    ): Promise<T[]> {
 | 
			
		||||
        if (select) {
 | 
			
		||||
            select = ' WHERE ' + select;
 | 
			
		||||
            select = ` WHERE ${select}`;
 | 
			
		||||
        }
 | 
			
		||||
        if (sort) {
 | 
			
		||||
            sort = ' ORDER BY ' + sort;
 | 
			
		||||
            sort = ` ORDER BY ${sort}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const sql = `SELECT ${fields} FROM ${table} ${select} ${sort}`;
 | 
			
		||||
@ -778,7 +765,7 @@ export class SQLiteDB {
 | 
			
		||||
            if (limits[1] < 1) {
 | 
			
		||||
                limits[1] = Number.MAX_VALUE;
 | 
			
		||||
            }
 | 
			
		||||
            sql += ' LIMIT ' + limits[0] + ', ' + limits[1];
 | 
			
		||||
            sql += ` LIMIT ${limits[0]}, ${limits[1]}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const result = await this.execute(sql, params);
 | 
			
		||||
@ -807,7 +794,7 @@ export class SQLiteDB {
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            sql: `INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`,
 | 
			
		||||
            params: this.formatDataToSQLParams(data),
 | 
			
		||||
            params: Object.values(data),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -897,7 +884,7 @@ export class SQLiteDB {
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            await this.tableExists(oldTable);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Old table does not exist, ignore.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -919,7 +906,7 @@ export class SQLiteDB {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.dropTable(oldTable);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Error deleting old table, ignore.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -958,7 +945,7 @@ export class SQLiteDB {
 | 
			
		||||
    async open(): Promise<void> {
 | 
			
		||||
        await this.ready();
 | 
			
		||||
 | 
			
		||||
        await this.db!.open();
 | 
			
		||||
        await this.db?.open();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1066,7 +1053,7 @@ export class SQLiteDB {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create the list of params using the "data" object and the params for the where clause.
 | 
			
		||||
        let params = this.formatDataToSQLParams(data);
 | 
			
		||||
        let params = Object.values(data);
 | 
			
		||||
        if (where && whereParams) {
 | 
			
		||||
            params = params.concat(whereParams);
 | 
			
		||||
        }
 | 
			
		||||
@ -1090,19 +1077,19 @@ export class SQLiteDB {
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const where: string[] = [];
 | 
			
		||||
        const params: SQLiteDBRecordValue[] = [];
 | 
			
		||||
 | 
			
		||||
        for (const key in conditions) {
 | 
			
		||||
            const value = conditions[key];
 | 
			
		||||
        const where = Object.keys(conditions).map((field) => {
 | 
			
		||||
            const value = conditions[field];
 | 
			
		||||
 | 
			
		||||
            if (value === undefined || value === null) {
 | 
			
		||||
                where.push(key + ' IS NULL');
 | 
			
		||||
            } else {
 | 
			
		||||
                where.push(key + ' = ?');
 | 
			
		||||
                params.push(value);
 | 
			
		||||
                return `${field} IS NULL`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            params.push(value);
 | 
			
		||||
 | 
			
		||||
            return `${field} = ?`;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            sql: where.join(' AND '),
 | 
			
		||||
@ -1130,7 +1117,7 @@ export class SQLiteDB {
 | 
			
		||||
 | 
			
		||||
        values.forEach((value) => {
 | 
			
		||||
            if (value === undefined || value === null) {
 | 
			
		||||
                sql = field + ' IS NULL';
 | 
			
		||||
                sql = `${field} IS NULL`;
 | 
			
		||||
            } else {
 | 
			
		||||
                params.push(value);
 | 
			
		||||
            }
 | 
			
		||||
@ -1138,14 +1125,14 @@ export class SQLiteDB {
 | 
			
		||||
 | 
			
		||||
        if (params && params.length) {
 | 
			
		||||
            if (sql !== '') {
 | 
			
		||||
                sql = sql + ' OR ';
 | 
			
		||||
                sql += ' OR ';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (params.length == 1) {
 | 
			
		||||
                sql = sql + field + ' = ?';
 | 
			
		||||
                sql += `${field} = ?`;
 | 
			
		||||
            } else {
 | 
			
		||||
                const questionMarks = ',?'.repeat(params.length).substring(1);
 | 
			
		||||
                sql = sql + field + ' IN (' + questionMarks + ')';
 | 
			
		||||
                sql += ` ${field} IN (${questionMarks})`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1232,7 +1219,7 @@ export class SQLiteDB {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type SQLiteDBRecordValues = {
 | 
			
		||||
    [key: string]: SQLiteDBRecordValue | undefined | null;
 | 
			
		||||
    [key: string]: SQLiteDBRecordValue;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type SQLiteDBQueryParams = {
 | 
			
		||||
@ -1240,4 +1227,4 @@ export type SQLiteDBQueryParams = {
 | 
			
		||||
    params: SQLiteDBRecordValue[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type SQLiteDBRecordValue = number | string;
 | 
			
		||||
export type SQLiteDBRecordValue = number | string  | undefined | null;
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ describe('CoreError', () => {
 | 
			
		||||
        // Arrange
 | 
			
		||||
        const message = Faker.lorem.sentence();
 | 
			
		||||
 | 
			
		||||
        let error: CoreError | null = null;
 | 
			
		||||
        let error: CoreError;
 | 
			
		||||
 | 
			
		||||
        // Act
 | 
			
		||||
        try {
 | 
			
		||||
@ -37,10 +37,10 @@ describe('CoreError', () => {
 | 
			
		||||
        expect(error).not.toBeNull();
 | 
			
		||||
        expect(error).toBeInstanceOf(Error);
 | 
			
		||||
        expect(error).toBeInstanceOf(CoreError);
 | 
			
		||||
        expect(error!.name).toEqual('CoreError');
 | 
			
		||||
        expect(error!.message).toEqual(message);
 | 
			
		||||
        expect(error!.stack).not.toBeNull();
 | 
			
		||||
        expect(error!.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
 | 
			
		||||
        expect(error.name).toEqual('CoreError');
 | 
			
		||||
        expect(error.message).toEqual(message);
 | 
			
		||||
        expect(error.stack).not.toBeNull();
 | 
			
		||||
        expect(error.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('can be subclassed', () => {
 | 
			
		||||
@ -55,7 +55,7 @@ describe('CoreError', () => {
 | 
			
		||||
 | 
			
		||||
        const message = Faker.lorem.sentence();
 | 
			
		||||
 | 
			
		||||
        let error: CustomCoreError | null = null;
 | 
			
		||||
        let error: CustomCoreError;
 | 
			
		||||
 | 
			
		||||
        // Act
 | 
			
		||||
        try {
 | 
			
		||||
@ -69,10 +69,10 @@ describe('CoreError', () => {
 | 
			
		||||
        expect(error).toBeInstanceOf(Error);
 | 
			
		||||
        expect(error).toBeInstanceOf(CoreError);
 | 
			
		||||
        expect(error).toBeInstanceOf(CustomCoreError);
 | 
			
		||||
        expect(error!.name).toEqual('CustomCoreError');
 | 
			
		||||
        expect(error!.message).toEqual(`Custom message: ${message}`);
 | 
			
		||||
        expect(error!.stack).not.toBeNull();
 | 
			
		||||
        expect(error!.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
 | 
			
		||||
        expect(error.name).toEqual('CustomCoreError');
 | 
			
		||||
        expect(error.message).toEqual(`Custom message: ${message}`);
 | 
			
		||||
        expect(error.stack).not.toBeNull();
 | 
			
		||||
        expect(error.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
})
 | 
			
		||||
export class CoreAttachmentsComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @Input() files?: CoreFileEntry[]; // List of attachments. New attachments will be added to this array.
 | 
			
		||||
    @Input() files: CoreFileEntry[] = []; // List of attachments. New attachments will be added to this array.
 | 
			
		||||
    @Input() maxSize?: number; // Max size. -1 means unlimited, 0 means course/user max size, not defined means unknown.
 | 
			
		||||
    @Input() maxSubmissions?: number; // Max number of attachments. -1 means unlimited, not defined means unknown limit.
 | 
			
		||||
    @Input() component?: string; // Component the downloaded files will be linked to.
 | 
			
		||||
@ -177,7 +177,7 @@ export class CoreAttachmentsComponent implements OnInit {
 | 
			
		||||
     * @param data The data received.
 | 
			
		||||
     */
 | 
			
		||||
    renamed(index: number, data: { file: FileEntry }): void {
 | 
			
		||||
        this.files![index] = data.file;
 | 
			
		||||
        this.files[index] = data.file;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -71,11 +71,11 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
 | 
			
		||||
                    generateLabels: (chart: Chart): ChartLegendLabelItem[] => {
 | 
			
		||||
                        const data = chart.data;
 | 
			
		||||
                        if (data.labels?.length) {
 | 
			
		||||
                            const datasets = data.datasets![0];
 | 
			
		||||
                            const datasets = data.datasets?.[0];
 | 
			
		||||
 | 
			
		||||
                            return data.labels.map((label, i) => ({
 | 
			
		||||
                                text: label + ': ' + datasets.data![i],
 | 
			
		||||
                                fillStyle: datasets.backgroundColor![i],
 | 
			
		||||
                            return data.labels.map<ChartLegendLabelItem>((label, i) => ({
 | 
			
		||||
                                text: label + ': ' + datasets?.data?.[i],
 | 
			
		||||
                                fillStyle: datasets?.backgroundColor?.[i],
 | 
			
		||||
                            }));
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
@ -87,14 +87,18 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
 | 
			
		||||
            legend = Object.assign({}, this.legend);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.type == 'bar' && this.data.length >= 5) {
 | 
			
		||||
        if (this.type === 'bar' && this.data.length >= 5) {
 | 
			
		||||
            this.type = 'horizontalBar';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Format labels if needed.
 | 
			
		||||
        await this.formatLabels();
 | 
			
		||||
 | 
			
		||||
        const context = this.canvas!.nativeElement.getContext('2d')!;
 | 
			
		||||
        const context = this.canvas?.nativeElement.getContext('2d');
 | 
			
		||||
        if (!context) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.chart = new Chart(context, {
 | 
			
		||||
            type: this.type,
 | 
			
		||||
            data: {
 | 
			
		||||
@ -123,7 +127,11 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
 | 
			
		||||
            await this.formatLabels();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.chart.data.datasets![0] = {
 | 
			
		||||
        if (!this.chart.data.datasets) {
 | 
			
		||||
            this.chart.data.datasets = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.chart.data.datasets[0] = {
 | 
			
		||||
            data: this.data,
 | 
			
		||||
            backgroundColor: this.getRandomColors(this.data.length),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,6 @@ import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
 | 
			
		||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
 | 
			
		||||
import { CoreFileComponent } from './file/file';
 | 
			
		||||
import { CoreFilesComponent } from './files/files';
 | 
			
		||||
import { CoreIconComponent } from './icon/icon';
 | 
			
		||||
import { CoreIframeComponent } from './iframe/iframe';
 | 
			
		||||
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
 | 
			
		||||
import { CoreInputErrorsComponent } from './input-errors/input-errors';
 | 
			
		||||
@ -82,7 +81,6 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
 | 
			
		||||
        CoreFileComponent,
 | 
			
		||||
        CoreFilesComponent,
 | 
			
		||||
        CoreGroupSelectorComponent,
 | 
			
		||||
        CoreIconComponent,
 | 
			
		||||
        CoreIframeComponent,
 | 
			
		||||
        CoreInfiniteLoadingComponent,
 | 
			
		||||
        CoreInputErrorsComponent,
 | 
			
		||||
@ -136,7 +134,6 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
 | 
			
		||||
        CoreFileComponent,
 | 
			
		||||
        CoreFilesComponent,
 | 
			
		||||
        CoreGroupSelectorComponent,
 | 
			
		||||
        CoreIconComponent,
 | 
			
		||||
        CoreIframeComponent,
 | 
			
		||||
        CoreInfiniteLoadingComponent,
 | 
			
		||||
        CoreInputErrorsComponent,
 | 
			
		||||
 | 
			
		||||
@ -149,24 +149,30 @@ export class CoreFileComponent implements OnInit, OnDestroy {
 | 
			
		||||
     * @param isOpenButton Whether the open button was clicked.
 | 
			
		||||
     * @returns Promise resolved when file is opened.
 | 
			
		||||
     */
 | 
			
		||||
    openFile(ev?: Event, isOpenButton = false): Promise<void> {
 | 
			
		||||
    async openFile(ev?: Event, isOpenButton = false): Promise<void> {
 | 
			
		||||
        ev?.preventDefault();
 | 
			
		||||
        ev?.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        if (!this.file) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const options: CoreUtilsOpenFileOptions = {};
 | 
			
		||||
        if (isOpenButton) {
 | 
			
		||||
            // Use the non-default method.
 | 
			
		||||
            options.iOSOpenFileAction = this.defaultIsOpenWithPicker ? OpenFileAction.OPEN : OpenFileAction.OPEN_WITH;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CoreFileHelper.downloadAndOpenFile(this.file!, this.component, this.componentId, this.state, (event) => {
 | 
			
		||||
            if (event && 'calculating' in event && event.calculating) {
 | 
			
		||||
                // The process is calculating some data required for the download, show the spinner.
 | 
			
		||||
                this.isDownloading = true;
 | 
			
		||||
            }
 | 
			
		||||
        }, undefined, options).catch((error) => {
 | 
			
		||||
        try {
 | 
			
		||||
            return await CoreFileHelper.downloadAndOpenFile(this.file, this.component, this.componentId, this.state, (event) => {
 | 
			
		||||
                if (event && 'calculating' in event && event.calculating) {
 | 
			
		||||
                    // The process is calculating some data required for the download, show the spinner.
 | 
			
		||||
                    this.isDownloading = true;
 | 
			
		||||
                }
 | 
			
		||||
            }, undefined, options);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
 | 
			
		||||
        });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -264,7 +270,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component destroyed.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.observer?.off();
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@ import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
})
 | 
			
		||||
export class CoreFilesComponent implements OnInit, DoCheck {
 | 
			
		||||
 | 
			
		||||
    @Input() files?: CoreFileEntry[]; // List of files.
 | 
			
		||||
    @Input() files: CoreFileEntry[] = []; // List of files.
 | 
			
		||||
    @Input() component?: string; // Component the downloaded files will be linked to.
 | 
			
		||||
    @Input() componentId?: string | number; // Component ID.
 | 
			
		||||
    @Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded.
 | 
			
		||||
@ -75,7 +75,7 @@ export class CoreFilesComponent implements OnInit, DoCheck {
 | 
			
		||||
     * Calculate contentText based on fils that can be rendered inline.
 | 
			
		||||
     */
 | 
			
		||||
    protected renderInlineFiles(): void {
 | 
			
		||||
        this.contentText = this.files!.reduce((previous, file) => {
 | 
			
		||||
        this.contentText = this.files.reduce((previous, file) => {
 | 
			
		||||
            const text = CoreMimetypeUtils.getEmbeddedHtml(file);
 | 
			
		||||
 | 
			
		||||
            return text ? previous + '<br>' + text : previous;
 | 
			
		||||
 | 
			
		||||
@ -1,128 +0,0 @@
 | 
			
		||||
:host {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context([dir=rtl]).icon-flip-rtl {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(ion-item.md) ion-icon {
 | 
			
		||||
    &[slot] {
 | 
			
		||||
        color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.54);
 | 
			
		||||
        font-size: 24px;
 | 
			
		||||
        margin-top: 12px;
 | 
			
		||||
        margin-bottom: 12px;
 | 
			
		||||
    }
 | 
			
		||||
    &[slot=start] {
 | 
			
		||||
        margin-right: 32px;
 | 
			
		||||
    }
 | 
			
		||||
    &[slot=end] {
 | 
			
		||||
        margin-left: 16px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
 | 
			
		||||
        &[slot=start] {
 | 
			
		||||
            margin-right: unset;
 | 
			
		||||
            -webkit-margin-end: 32px;
 | 
			
		||||
            margin-inline-end: 32px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
 | 
			
		||||
        &[slot=end] {
 | 
			
		||||
            margin-left: unset;
 | 
			
		||||
            -webkit-margin-start: 16px;
 | 
			
		||||
            margin-inline-start: 16px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(ion-item.ios) ion-icon {
 | 
			
		||||
    &[slot] {
 | 
			
		||||
        font-size: 1.6em;
 | 
			
		||||
    }
 | 
			
		||||
    &[slot=start] {
 | 
			
		||||
        margin-top: 7px;
 | 
			
		||||
        margin-bottom: 7px;
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
        margin-right: 20px;
 | 
			
		||||
    }
 | 
			
		||||
    &[slot=end] {
 | 
			
		||||
        margin-top: 7px;
 | 
			
		||||
        margin-bottom: 7px;
 | 
			
		||||
        margin-left: 10px;
 | 
			
		||||
        margin-right: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
 | 
			
		||||
        &[slot=start] {
 | 
			
		||||
            margin-left: unset;
 | 
			
		||||
            margin-right: unset;
 | 
			
		||||
            -webkit-margin-start: 0;
 | 
			
		||||
            margin-inline-start: 0;
 | 
			
		||||
            -webkit-margin-end: 20px;
 | 
			
		||||
            margin-inline-end: 20px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
 | 
			
		||||
        &[slot=end] {
 | 
			
		||||
            margin-left: unset;
 | 
			
		||||
            margin-right: unset;
 | 
			
		||||
            -webkit-margin-start: 10px;
 | 
			
		||||
            margin-inline-start: 10px;
 | 
			
		||||
            -webkit-margin-end: 10px;
 | 
			
		||||
            margin-inline-end: 10px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(ion-item.ion-color) {
 | 
			
		||||
    color: var(--ion-color-contrast);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(ion-button.md) ion-icon,
 | 
			
		||||
:host-context(ion-button.ios) ion-icon {
 | 
			
		||||
    &[slot] {
 | 
			
		||||
        font-size: 1.4em;
 | 
			
		||||
        pointer-events: none;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    &[slot=start] {
 | 
			
		||||
        margin-left: -0.3em;
 | 
			
		||||
        margin-right: 0.3em;
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
    &[slot=end] {
 | 
			
		||||
        margin-left: 0.3em;
 | 
			
		||||
        margin-right: -0.2em;
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
    &[slot=icon-only] {
 | 
			
		||||
        font-size: 1.8em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
 | 
			
		||||
        &[slot=start] {
 | 
			
		||||
            margin-left: unset;
 | 
			
		||||
            margin-right: unset;
 | 
			
		||||
            -webkit-margin-start: -0.3em;
 | 
			
		||||
            margin-inline-start: -0.3em;
 | 
			
		||||
            -webkit-margin-end: 0.3em;
 | 
			
		||||
            margin-inline-end: 0.3em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
 | 
			
		||||
        &[slot=end] {
 | 
			
		||||
            margin-left: unset;
 | 
			
		||||
            margin-right: unset;
 | 
			
		||||
            -webkit-margin-start: 0.3em;
 | 
			
		||||
            margin-inline-start: 0.3em;
 | 
			
		||||
            -webkit-margin-end: -0.2em;
 | 
			
		||||
            margin-inline-end: -0.2em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,116 +0,0 @@
 | 
			
		||||
// (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, Input, OnChanges, ElementRef, SimpleChange } from '@angular/core';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Core Icon is a component that enables the posibility to add fontawesome icon to the html. It
 | 
			
		||||
 * To use fontawesome just place the full icon name with the fa- prefix and
 | 
			
		||||
 * the component will detect it.
 | 
			
		||||
 *
 | 
			
		||||
 * Check available icons at https://fontawesome.com/icons?d=gallery&m=free
 | 
			
		||||
 *
 | 
			
		||||
 * @deprecated since 3.9.3. Please use <ion-icon name="fas-icon"> instead.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'core-icon',
 | 
			
		||||
    template: '<ion-icon [name]="name"><ng-content></ng-content></ion-icon>',
 | 
			
		||||
    styleUrls: ['icon.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class CoreIconComponent implements OnChanges {
 | 
			
		||||
 | 
			
		||||
    // Common params.
 | 
			
		||||
    @Input() name = '';
 | 
			
		||||
    @Input() color?: string;
 | 
			
		||||
    @Input() slash?: boolean; // Display a red slash over the icon.
 | 
			
		||||
 | 
			
		||||
    // FontAwesome params.
 | 
			
		||||
    @Input('fixed-width') fixedWidth?: boolean; // eslint-disable-line @angular-eslint/no-input-rename
 | 
			
		||||
 | 
			
		||||
    @Input() label?: string;
 | 
			
		||||
    @Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false.
 | 
			
		||||
 | 
			
		||||
    protected element: HTMLElement;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        el: ElementRef,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.element = el.nativeElement;
 | 
			
		||||
 | 
			
		||||
        CoreLogger.getInstance('CoreIconComponent').error('CoreIconComponent is deprecated. Please use ion-icon instead.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Detect changes on input properties.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnChanges(changes: {[name: string]: SimpleChange}): void {
 | 
			
		||||
        if (!changes.name || !this.name) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            this.updateIcon(this.element.children[0]);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected updateIcon(iconElement: Element): void {
 | 
			
		||||
        !this.label && iconElement.setAttribute('aria-hidden', 'true');
 | 
			
		||||
        !this.label && iconElement.setAttribute('role', 'presentation');
 | 
			
		||||
        this.label && iconElement.setAttribute('aria-label', this.label);
 | 
			
		||||
        this.label && iconElement.setAttribute('title', this.label);
 | 
			
		||||
 | 
			
		||||
        const attrs = this.element.attributes;
 | 
			
		||||
        for (let i = attrs.length - 1; i >= 0; i--) {
 | 
			
		||||
            if (attrs[i].name != 'name') {
 | 
			
		||||
                iconElement.setAttribute(attrs[i].name, attrs[i].value);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isTrueProperty(this.slash)) {
 | 
			
		||||
            iconElement.classList.add('icon-slash');
 | 
			
		||||
        } else {
 | 
			
		||||
            iconElement.classList.remove('icon-slash');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isTrueProperty(this.flipRtl)) {
 | 
			
		||||
            iconElement.classList.add('icon-flip-rtl');
 | 
			
		||||
        } else {
 | 
			
		||||
            iconElement.classList.remove('icon-flip-rtl');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isTrueProperty(this.fixedWidth)) {
 | 
			
		||||
            iconElement.classList.add('fa-fw');
 | 
			
		||||
        } else {
 | 
			
		||||
            iconElement.classList.remove('fa-fw');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the value is true or on.
 | 
			
		||||
     *
 | 
			
		||||
     * @param val Value to be checked.
 | 
			
		||||
     * @returns If has a value equivalent to true.
 | 
			
		||||
     */
 | 
			
		||||
    isTrueProperty(val: unknown): boolean {
 | 
			
		||||
        if (typeof val === 'string') {
 | 
			
		||||
            val = val.toLowerCase().trim();
 | 
			
		||||
 | 
			
		||||
            return (val === 'true' || val === 'on' || val === '');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return !!val;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -28,10 +28,10 @@ import { CorePath } from '@singletons/path';
 | 
			
		||||
})
 | 
			
		||||
export class CoreRecaptchaComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @Input() model?: Record<string, string>; // The model where to store the recaptcha response.
 | 
			
		||||
    @Input() model: Record<string, string> = {}; // The model where to store the recaptcha response.
 | 
			
		||||
    @Input() publicKey?: string; // The site public key.
 | 
			
		||||
    @Input() modelValueName = 'recaptcharesponse'; // Name of the model property where to store the response.
 | 
			
		||||
    @Input() siteUrl?: string; // The site URL. If not defined, current site.
 | 
			
		||||
    @Input() siteUrl = ''; // The site URL. If not defined, current site.
 | 
			
		||||
 | 
			
		||||
    expired = false;
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ export class CoreRecaptchaComponent implements OnInit {
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.siteUrl = this.siteUrl || CoreSites.getCurrentSite()?.getURL();
 | 
			
		||||
        this.siteUrl = this.siteUrl || CoreSites.getRequiredCurrentSite().getURL();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -62,7 +62,7 @@ export class CoreRecaptchaComponent implements OnInit {
 | 
			
		||||
        // Open the recaptcha challenge in an InAppBrowser.
 | 
			
		||||
        // The app used to use an iframe for this, but the app can no longer access the iframe to create the required callbacks.
 | 
			
		||||
        // The app cannot render the recaptcha directly because it has problems with the local protocols and domains.
 | 
			
		||||
        const src = CorePath.concatenatePaths(this.siteUrl!, 'webservice/recaptcha.php?lang=' + this.lang);
 | 
			
		||||
        const src = CorePath.concatenatePaths(this.siteUrl, 'webservice/recaptcha.php?lang=' + this.lang);
 | 
			
		||||
 | 
			
		||||
        const inAppBrowserWindow = CoreUtils.openInApp(src);
 | 
			
		||||
        if (!inAppBrowserWindow) {
 | 
			
		||||
@ -90,7 +90,7 @@ export class CoreRecaptchaComponent implements OnInit {
 | 
			
		||||
                this.expireRecaptchaAnswer();
 | 
			
		||||
            } else if (event.data.action == 'callback') {
 | 
			
		||||
                this.expired = false;
 | 
			
		||||
                this.model![this.modelValueName] = event.data.value;
 | 
			
		||||
                this.model[this.modelValueName] = event.data.value;
 | 
			
		||||
 | 
			
		||||
                // Close the InAppBrowser now.
 | 
			
		||||
                inAppBrowserWindow.close();
 | 
			
		||||
@ -105,7 +105,7 @@ export class CoreRecaptchaComponent implements OnInit {
 | 
			
		||||
     */
 | 
			
		||||
    expireRecaptchaAnswer(): void {
 | 
			
		||||
        this.expired = true;
 | 
			
		||||
        this.model![this.modelValueName] = '';
 | 
			
		||||
        this.model[this.modelValueName] = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,36 +0,0 @@
 | 
			
		||||
// (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 { CoreIconComponent } from '@components/icon/icon';
 | 
			
		||||
 | 
			
		||||
import { renderWrapperComponent } from '@/testing/utils';
 | 
			
		||||
 | 
			
		||||
describe('CoreIconComponent', () => {
 | 
			
		||||
 | 
			
		||||
    it('should render', async () => {
 | 
			
		||||
        // Act
 | 
			
		||||
        const fixture = await renderWrapperComponent(CoreIconComponent, 'core-icon', { name: 'fa-thumbs-up' });
 | 
			
		||||
 | 
			
		||||
        // Assert
 | 
			
		||||
        expect(fixture.nativeElement.innerHTML.trim()).not.toHaveLength(0);
 | 
			
		||||
 | 
			
		||||
        const icon = fixture.nativeElement.querySelector('ion-icon');
 | 
			
		||||
        const name = icon?.getAttribute('name') || icon?.getAttribute('ng-reflect-name') || '';
 | 
			
		||||
 | 
			
		||||
        expect(icon).not.toBeNull();
 | 
			
		||||
        expect(name).toEqual('fa-thumbs-up');
 | 
			
		||||
        expect(icon?.getAttribute('role')).toEqual('presentation');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
@ -54,7 +54,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
 | 
			
		||||
        let iconName = this.name;
 | 
			
		||||
        let font = 'ionicons';
 | 
			
		||||
        const parts = iconName.split('-', 2);
 | 
			
		||||
        if (parts.length == 2) {
 | 
			
		||||
        if (parts.length === 2) {
 | 
			
		||||
            switch (parts[0]) {
 | 
			
		||||
                case 'far':
 | 
			
		||||
                    library = 'regular';
 | 
			
		||||
@ -82,7 +82,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (font == 'ionicons') {
 | 
			
		||||
        if (font === 'ionicons') {
 | 
			
		||||
            this.element.removeAttribute('src');
 | 
			
		||||
            this.logger.warn(`Ionic icon ${this.name} detected`);
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
 | 
			
		||||
    ngAfterViewInit(): void {
 | 
			
		||||
        if (!this.element.getAttribute('aria-label') &&
 | 
			
		||||
            !this.element.getAttribute('aria-labelledby') &&
 | 
			
		||||
            this.element.getAttribute('aria-hidden') != 'true') {
 | 
			
		||||
            this.element.getAttribute('aria-hidden') !== 'true') {
 | 
			
		||||
            this.logger.warn('Aria label not set on icon ' + this.name, this.element);
 | 
			
		||||
 | 
			
		||||
            this.element.setAttribute('aria-hidden', 'true');
 | 
			
		||||
@ -111,7 +111,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Detect changes on input properties.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnChanges(changes: { [name: string]: SimpleChange }): void {
 | 
			
		||||
        if (!changes.name || !this.name) {
 | 
			
		||||
 | 
			
		||||
@ -36,19 +36,23 @@ export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implemen
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        await super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.';
 | 
			
		||||
        this.fetchContentDefaultError = `Error getting ${this.block.contents?.title} data.`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to the block page.
 | 
			
		||||
     */
 | 
			
		||||
    gotoBlock(): void {
 | 
			
		||||
        if (!this.link) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const navOptions = this.navOptions || {};
 | 
			
		||||
        if (this.linkParams) {
 | 
			
		||||
            navOptions.params = this.linkParams;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigateToSitePath(this.link!, navOptions);
 | 
			
		||||
        CoreNavigator.navigateToSitePath(this.link, navOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -119,7 +119,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
@ -445,9 +445,13 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
     * @returns Promise resolved with modified comment when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadCommentProfile(comment: CoreCommentsDataToDisplay): Promise<CoreCommentsDataToDisplay> {
 | 
			
		||||
        // Get the user profile image.
 | 
			
		||||
        if (!comment.userid) {
 | 
			
		||||
            return comment;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const user = await CoreUser.getProfile(comment.userid!, undefined, true);
 | 
			
		||||
            // Get the user profile image.
 | 
			
		||||
            const user = await CoreUser.getProfile(comment.userid, undefined, true);
 | 
			
		||||
            comment.profileimageurl = user.profileimageurl;
 | 
			
		||||
            comment.fullname = user.fullname;
 | 
			
		||||
        } catch {
 | 
			
		||||
@ -599,7 +603,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page destroyed.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.syncObserver?.off();
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ import { CoreNetwork } from '@services/network';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import { CoreCommentsDBRecord, CoreCommentsDeletedDBRecord } from './database/comments';
 | 
			
		||||
import { CoreSyncResult } from '@services/sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to sync omments.
 | 
			
		||||
@ -318,10 +319,7 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider<CoreCommentsS
 | 
			
		||||
}
 | 
			
		||||
export const CoreCommentsSync = makeSingleton(CoreCommentsSyncProvider);
 | 
			
		||||
 | 
			
		||||
export type CoreCommentsSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
 | 
			
		||||
};
 | 
			
		||||
export type CoreCommentsSyncResult = CoreSyncResult;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNCED event.
 | 
			
		||||
 | 
			
		||||
@ -152,7 +152,7 @@ export class CoreCommentsProvider {
 | 
			
		||||
            this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return commentsResponse![0];
 | 
			
		||||
        return commentsResponse[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -166,9 +166,9 @@ export class CoreCommentsProvider {
 | 
			
		||||
    async addCommentsOnline(
 | 
			
		||||
        comments: CoreCommentsCommentBasicData[],
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreCommentsAddCommentsWSResponse | undefined> {
 | 
			
		||||
    ): Promise<CoreCommentsAddCommentsWSResponse> {
 | 
			
		||||
        if (!comments || !comments.length) {
 | 
			
		||||
            return;
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
@ -231,8 +231,12 @@ export class CoreCommentsProvider {
 | 
			
		||||
 | 
			
		||||
        // Convenience function to store the action to be synchronized later.
 | 
			
		||||
        const storeOffline = async (): Promise<boolean> => {
 | 
			
		||||
            if (!comment.id) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await CoreCommentsOffline.deleteComment(
 | 
			
		||||
                comment.id!,
 | 
			
		||||
                comment.id,
 | 
			
		||||
                comment.contextlevel,
 | 
			
		||||
                comment.instanceid,
 | 
			
		||||
                comment.component,
 | 
			
		||||
 | 
			
		||||
@ -67,7 +67,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB
 | 
			
		||||
        courseId?: number,
 | 
			
		||||
    ): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
 | 
			
		||||
 | 
			
		||||
        courseId = Number(courseId || params.courseid || params.cid);
 | 
			
		||||
        const courseIdentifier = Number(courseId || params.courseid || params.cid);
 | 
			
		||||
 | 
			
		||||
        return [{
 | 
			
		||||
            action: async (siteId): Promise<void> => {
 | 
			
		||||
@ -79,14 +79,14 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB
 | 
			
		||||
                    CoreCourseHelper.navigateToModule(
 | 
			
		||||
                        Number(params.id),
 | 
			
		||||
                        {
 | 
			
		||||
                            courseId,
 | 
			
		||||
                            courseId: courseIdentifier,
 | 
			
		||||
                            modName: this.useModNameToGetModule ? this.modName : undefined,
 | 
			
		||||
                            siteId,
 | 
			
		||||
                        },
 | 
			
		||||
                    );
 | 
			
		||||
                } else if (this.canReview) {
 | 
			
		||||
                    // Use the goToReview function.
 | 
			
		||||
                    this.goToReview(url, params, courseId!, siteId);
 | 
			
		||||
                    this.goToReview(url, params, courseIdentifier, siteId);
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Not current user and cannot review it in the app, open it in browser.
 | 
			
		||||
                    site.openInBrowserWithAutoLogin(url);
 | 
			
		||||
 | 
			
		||||
@ -20,9 +20,9 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreCourse } from '../services/course';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreWSExternalWarning } from '@services/ws';
 | 
			
		||||
import { CoreCourseContentsPage } from '../pages/contents/contents';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreSyncResult } from '@services/sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Template class to easily create CoreCourseModuleMainComponent of activities.
 | 
			
		||||
@ -188,32 +188,34 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(): Promise<unknown> {
 | 
			
		||||
        return {};
 | 
			
		||||
    protected async sync(): Promise<CoreSyncResult> {
 | 
			
		||||
        return {
 | 
			
		||||
            updated: false,
 | 
			
		||||
            warnings: [],
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     * Checks if sync has updated data on the server.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @returns If suceed or not.
 | 
			
		||||
     * @returns If data has been updated or not.
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    protected hasSyncSucceed(result: unknown): boolean {
 | 
			
		||||
        return true;
 | 
			
		||||
    protected hasSyncSucceed(result: CoreSyncResult): boolean {
 | 
			
		||||
        return result.updated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Tries to synchronize the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param showErrors If show errors to the user of hide them.
 | 
			
		||||
     * @returns Promise resolved with true if sync succeed, or false if failed.
 | 
			
		||||
     * @returns Promise resolved with true if sync hast updated data to the server, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncActivity(showErrors: boolean = false): Promise<boolean> {
 | 
			
		||||
        try {
 | 
			
		||||
            const result = <{warnings?: CoreWSExternalWarning[]}> await this.sync();
 | 
			
		||||
            const result = await this.sync();
 | 
			
		||||
 | 
			
		||||
            if (result?.warnings?.length) {
 | 
			
		||||
            if (result.warnings.length) {
 | 
			
		||||
                CoreDomUtils.showErrorModal(result.warnings[0]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    @Input() initialSectionNumber?: number; // The section to load first (by number).
 | 
			
		||||
    @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent<any>>;
 | 
			
		||||
 | 
			
		||||
    // All the possible component classes.
 | 
			
		||||
 | 
			
		||||
@ -58,16 +58,19 @@ export class CoreCourseDownloadModuleMainFileDirective implements OnInit {
 | 
			
		||||
            ev.stopPropagation();
 | 
			
		||||
 | 
			
		||||
            const modal = await CoreDomUtils.showModalLoading();
 | 
			
		||||
            const courseId = typeof this.courseId == 'string' ? parseInt(this.courseId, 10) : this.courseId;
 | 
			
		||||
            const courseId = this.courseId ? Number(this.courseId) : undefined;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                if (!this.module) {
 | 
			
		||||
                    // Try to get the module from cache.
 | 
			
		||||
                    this.moduleId = typeof this.moduleId == 'string' ? parseInt(this.moduleId, 10) : this.moduleId;
 | 
			
		||||
                    this.module = await CoreCourse.getModule(this.moduleId!, courseId);
 | 
			
		||||
                    const moduleId = Number(this.moduleId);
 | 
			
		||||
                    if (!moduleId) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    this.module = await CoreCourse.getModule(moduleId, courseId);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const componentId = this.componentId || module.id;
 | 
			
		||||
                const componentId = this.componentId ? Number(this.componentId) : this.module.id;
 | 
			
		||||
 | 
			
		||||
                await CoreCourseHelper.downloadModuleAndOpenFile(
 | 
			
		||||
                    this.module,
 | 
			
		||||
 | 
			
		||||
@ -448,7 +448,11 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
 | 
			
		||||
                ? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData
 | 
			
		||||
                : (handler as CoreCourseOptionsHandler).getDisplayData;
 | 
			
		||||
 | 
			
		||||
            promises.push(Promise.resolve(getFunction!.call(handler, courseWithOptions)).then((data) => {
 | 
			
		||||
            if (!getFunction) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            promises.push(Promise.resolve(getFunction.call(handler, courseWithOptions)).then((data) => {
 | 
			
		||||
                handlersToDisplay.push({
 | 
			
		||||
                    data: data,
 | 
			
		||||
                    priority: handler.priority || 0,
 | 
			
		||||
@ -468,7 +472,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
 | 
			
		||||
        handlersToDisplay.sort((
 | 
			
		||||
            a: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay,
 | 
			
		||||
            b: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay,
 | 
			
		||||
        ) => b.priority! - a.priority!);
 | 
			
		||||
        ) => (b.priority || 0) - (a.priority || 0));
 | 
			
		||||
 | 
			
		||||
        return handlersToDisplay;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1274,9 +1274,9 @@ export class CoreCourseProvider {
 | 
			
		||||
        if (!result.status) {
 | 
			
		||||
            if (result.warnings && result.warnings.length) {
 | 
			
		||||
                throw new CoreWSError(result.warnings[0]);
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new CoreError('Cannot change completion.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            throw new CoreError('Cannot change completion.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
 | 
			
		||||
@ -141,8 +141,12 @@ ion-card.core-course-list-item {
 | 
			
		||||
 | 
			
		||||
// Card layout.
 | 
			
		||||
ion-card.core-course-list-card {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    &::part(native) {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        align-items: stretch;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    height: calc(100% - var(--card-vertical-margin) - var(--card-vertical-margin));
 | 
			
		||||
    margin-top: var(--card-vertical-margin);
 | 
			
		||||
 | 
			
		||||
@ -119,24 +119,20 @@ export class CoreEmulatorCaptureHelperProvider {
 | 
			
		||||
 | 
			
		||||
        if (mimetypes?.length) {
 | 
			
		||||
            // Search for a supported mimetype.
 | 
			
		||||
            for (let i = 0; i < mimetypes.length; i++) {
 | 
			
		||||
                const mimetype = mimetypes[i];
 | 
			
		||||
            result.mimetype = mimetypes.find((mimetype) => {
 | 
			
		||||
                const matches = mimetype.match(new RegExp('^' + type + '/'));
 | 
			
		||||
 | 
			
		||||
                if (matches?.length && window.MediaRecorder.isTypeSupported(mimetype)) {
 | 
			
		||||
                    result.mimetype = mimetype;
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
                return matches?.length && window.MediaRecorder.isTypeSupported(mimetype);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (result.mimetype) {
 | 
			
		||||
            // Found a supported mimetype in the mimetypes array, get the extension.
 | 
			
		||||
            result.extension = CoreMimetypeUtils.getExtension(result.mimetype);
 | 
			
		||||
        } else if (type == 'video') {
 | 
			
		||||
        } else if (type === 'video' && this.videoMimeType) {
 | 
			
		||||
            // No mimetype found, use default extension.
 | 
			
		||||
            result.mimetype = this.videoMimeType;
 | 
			
		||||
            result.extension = this.possibleVideoMimeTypes[result.mimetype!];
 | 
			
		||||
            result.extension = this.possibleVideoMimeTypes[result.mimetype];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,9 @@ export class FileTransferObjectMock extends FileTransferObject {
 | 
			
		||||
    abort(): void {
 | 
			
		||||
        if (this.xhr) {
 | 
			
		||||
            this.xhr.abort();
 | 
			
		||||
            this.reject!(new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source!, this.target!, 0, '', ''));
 | 
			
		||||
            this.reject?.(
 | 
			
		||||
                new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source || '', this.target || '', 0, '', ''),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -189,7 +189,7 @@ export class CoreFileUploaderDelegateService extends CoreDelegate<CoreFileUpload
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sort them by priority.
 | 
			
		||||
        handlers.sort((a, b) => a.priority! <= b.priority! ? 1 : -1);
 | 
			
		||||
        handlers.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
 | 
			
		||||
 | 
			
		||||
        return handlers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -806,8 +806,8 @@ export class CoreFileUploaderHelperProvider {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (maxSize != -1 && size > maxSize) {
 | 
			
		||||
            throw this.createMaxBytesError(maxSize, file!.name);
 | 
			
		||||
        if (maxSize != -1 && size > maxSize && file) {
 | 
			
		||||
            throw this.createMaxBytesError(maxSize, file.name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (size > 0) {
 | 
			
		||||
@ -849,12 +849,12 @@ export class CoreFileUploaderHelperProvider {
 | 
			
		||||
        stringKey: string,
 | 
			
		||||
        progress: ProgressEvent | CoreFileProgressEvent,
 | 
			
		||||
    ): void {
 | 
			
		||||
        if (!progress || !progress.lengthComputable) {
 | 
			
		||||
        if (!progress || !progress.lengthComputable || progress.loaded === undefined || !progress.total) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate the progress percentage.
 | 
			
		||||
        const perc = Math.min((progress.loaded! / progress.total!) * 100, 100);
 | 
			
		||||
        const perc = Math.min((progress.loaded / progress.total) * 100, 100);
 | 
			
		||||
 | 
			
		||||
        if (isNaN(perc) || perc < 0) {
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
@ -286,8 +286,8 @@ export class CoreFileUploaderProvider {
 | 
			
		||||
 | 
			
		||||
            if (!stillInList) {
 | 
			
		||||
                filesToDelete.push({
 | 
			
		||||
                    filepath: file.filepath!,
 | 
			
		||||
                    filename: file.filename!,
 | 
			
		||||
                    filepath: file.filepath || '',
 | 
			
		||||
                    filename: file.filename || '',
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
@ -643,7 +643,7 @@ export class CoreFileUploaderProvider {
 | 
			
		||||
                filesToUpload.push(<FileEntry> file);
 | 
			
		||||
            } else {
 | 
			
		||||
                // It's an online file.
 | 
			
		||||
                usedNames[file.filename!.toLowerCase()] = file;
 | 
			
		||||
                usedNames[(file.filename || '').toLowerCase()] = file;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -681,7 +681,7 @@ export class CoreFileUploaderProvider {
 | 
			
		||||
    ): Promise<number> {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        let fileName: string | undefined;
 | 
			
		||||
        let fileName = '';
 | 
			
		||||
        let fileEntry: FileEntry | undefined;
 | 
			
		||||
 | 
			
		||||
        const isOnline = !CoreUtils.isFileEntry(file);
 | 
			
		||||
@ -692,7 +692,7 @@ export class CoreFileUploaderProvider {
 | 
			
		||||
            fileEntry = file;
 | 
			
		||||
        } else {
 | 
			
		||||
            // It's an online file. We need to download it and re-upload it.
 | 
			
		||||
            fileName = file.filename;
 | 
			
		||||
            fileName = file.filename || '';
 | 
			
		||||
 | 
			
		||||
            const path = await CoreFilepool.downloadUrl(
 | 
			
		||||
                siteId,
 | 
			
		||||
@ -710,9 +710,9 @@ export class CoreFileUploaderProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Now upload the file.
 | 
			
		||||
        const extension = CoreMimetypeUtils.getFileExtension(fileName!);
 | 
			
		||||
        const extension = CoreMimetypeUtils.getFileExtension(fileName);
 | 
			
		||||
        const mimetype = extension ? CoreMimetypeUtils.getMimeType(extension) : undefined;
 | 
			
		||||
        const options = this.getFileUploadOptions(fileEntry.toURL(), fileName!, mimetype, isOnline, 'draft', itemId);
 | 
			
		||||
        const options = this.getFileUploadOptions(fileEntry.toURL(), fileName, mimetype, isOnline, 'draft', itemId);
 | 
			
		||||
 | 
			
		||||
        const result = await this.uploadFile(fileEntry.toURL(), options, undefined, siteId);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ export class CoreGradesReportLinkHandlerService extends CoreContentLinksHandlerB
 | 
			
		||||
        courseId?: number,
 | 
			
		||||
        data?: { cmid?: string },
 | 
			
		||||
    ): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
 | 
			
		||||
        courseId = courseId || Number(params.id);
 | 
			
		||||
        const courseIdentifier = courseId || Number(params.id);
 | 
			
		||||
        data = data || {};
 | 
			
		||||
 | 
			
		||||
        return [{
 | 
			
		||||
@ -47,7 +47,7 @@ export class CoreGradesReportLinkHandlerService extends CoreContentLinksHandlerB
 | 
			
		||||
                const userId = params.userid ? parseInt(params.userid, 10) : undefined;
 | 
			
		||||
                const moduleId = data?.cmid && parseInt(data.cmid, 10) || undefined;
 | 
			
		||||
 | 
			
		||||
                CoreGradesHelper.goToGrades(courseId!, userId, moduleId, siteId);
 | 
			
		||||
                CoreGradesHelper.goToGrades(courseIdentifier, userId, moduleId, siteId);
 | 
			
		||||
            },
 | 
			
		||||
        }];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ export class CoreGradesUserLinkHandlerService extends CoreContentLinksHandlerBas
 | 
			
		||||
        courseId?: number,
 | 
			
		||||
        data?: { cmid?: string },
 | 
			
		||||
    ): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
 | 
			
		||||
        courseId = courseId || Number(params.id);
 | 
			
		||||
        const courseIdentifier = courseId || Number(params.id);
 | 
			
		||||
        data = data || {};
 | 
			
		||||
 | 
			
		||||
        return [{
 | 
			
		||||
@ -47,7 +47,7 @@ export class CoreGradesUserLinkHandlerService extends CoreContentLinksHandlerBas
 | 
			
		||||
                const userId = params.user ? parseInt(params.user, 10) : undefined;
 | 
			
		||||
                const moduleId = data?.cmid && parseInt(data.cmid, 10) || undefined;
 | 
			
		||||
 | 
			
		||||
                CoreGradesHelper.goToGrades(courseId!, userId, moduleId, siteId);
 | 
			
		||||
                CoreGradesHelper.goToGrades(courseIdentifier, userId, moduleId, siteId);
 | 
			
		||||
            },
 | 
			
		||||
        }];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -166,16 +166,16 @@ export class CoreH5PPlayer {
 | 
			
		||||
     * @returns Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteAllContentIndexesForSite(siteId?: string): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
        const siteIdentifier = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (!siteId) {
 | 
			
		||||
        if (!siteIdentifier) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const records = await this.h5pCore.h5pFramework.getAllContentData(siteId);
 | 
			
		||||
        const records = await this.h5pCore.h5pFramework.getAllContentData(siteIdentifier);
 | 
			
		||||
 | 
			
		||||
        await Promise.all(records.map(async (record) => {
 | 
			
		||||
            await CoreUtils.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteId!));
 | 
			
		||||
            await CoreUtils.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteIdentifier));
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -217,12 +217,13 @@ export class CoreLoginEmailSignupPage implements OnInit {
 | 
			
		||||
            this.countryControl.setValue(this.settings.country || '');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.namefieldsErrors = {};
 | 
			
		||||
        const namefieldsErrors = {};
 | 
			
		||||
        if (this.settings.namefields) {
 | 
			
		||||
            this.settings.namefields.forEach((field) => {
 | 
			
		||||
                this.namefieldsErrors![field] = CoreLoginHelper.getErrorMessages('core.login.missing' + field);
 | 
			
		||||
                namefieldsErrors[field] = CoreLoginHelper.getErrorMessages('core.login.missing' + field);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        this.namefieldsErrors = namefieldsErrors;
 | 
			
		||||
 | 
			
		||||
        this.countries = await CoreUtils.getCountryListSorted();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user