commit
						32be164be7
					
				@ -22,7 +22,6 @@ import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
 | 
			
		||||
import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
 | 
			
		||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
import { CoreObject } from '@singletons/object';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the list of calendar events.
 | 
			
		||||
@ -125,10 +124,10 @@ class AddonBadgesUserBadgesManager extends CorePageItemsListManager<AddonBadgesU
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getItemQueryParams(): Params {
 | 
			
		||||
        return CoreObject.withoutEmpty({
 | 
			
		||||
        return {
 | 
			
		||||
            courseId: this.courseId,
 | 
			
		||||
            userId: this.userId,
 | 
			
		||||
        });
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,6 @@ import { CoreUserProfile } from '@features/user/services/user';
 | 
			
		||||
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreObject } from '@singletons/object';
 | 
			
		||||
import { AddonBadges } from '../badges';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -74,7 +73,7 @@ export class AddonBadgesUserHandlerService implements CoreUserProfileHandler {
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
                event.stopPropagation();
 | 
			
		||||
                CoreNavigator.navigateToSitePath('/badges', {
 | 
			
		||||
                    params: CoreObject.withoutEmpty({ courseId, userId: user.id }),
 | 
			
		||||
                    params: { courseId, userId: user.id },
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -161,7 +161,7 @@
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-datetime #notificationPicker hidden [(ngModel)]="notificationTimeText"
 | 
			
		||||
                    [displayFormat]="notificationFormat" [min]="notificationMin" [max]="notificationMax"
 | 
			
		||||
                    doneText]="'core.add' | translate"(ionChange)="addNotificationTime()">
 | 
			
		||||
                    [doneText]="'core.add' | translate" (ionChange)="addNotificationTime()" [monthNames]="monthNames">
 | 
			
		||||
                </ion-datetime>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ import { AddonCalendarReminderDBRecord } from '../../services/database/calendar'
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { CoreScreen } from '@services/screen';
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreLang } from '@services/lang';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a single calendar event.
 | 
			
		||||
@ -87,6 +88,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
 | 
			
		||||
    isOnline = false;
 | 
			
		||||
    syncIcon = CoreConstants.ICON_LOADING; // Sync icon.
 | 
			
		||||
    isSplitViewOn = false;
 | 
			
		||||
    monthNames?: string[];
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        @Optional() protected svComponent: CoreSplitViewComponent,
 | 
			
		||||
@ -137,6 +139,8 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    protected async asyncConstructor(): Promise<void> {
 | 
			
		||||
        if (this.notificationsEnabled) {
 | 
			
		||||
            this.monthNames = CoreLang.getMonthNames();
 | 
			
		||||
 | 
			
		||||
            this.reminders = await AddonCalendar.getEventReminders(this.eventId);
 | 
			
		||||
            this.defaultTime = await AddonCalendar.getDefaultNotificationTime() * 60;
 | 
			
		||||
 | 
			
		||||
@ -434,8 +438,6 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
 | 
			
		||||
     * Open the page to edit the event.
 | 
			
		||||
     */
 | 
			
		||||
    openEdit(): void {
 | 
			
		||||
        // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
 | 
			
		||||
        // @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
 | 
			
		||||
        CoreNavigator.navigateToSitePath('/calendar/edit', { params: { eventId: this.eventId } });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -74,7 +74,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
    loadUpcoming = false;
 | 
			
		||||
    filter: AddonCalendarFilter = {
 | 
			
		||||
        filtered: false,
 | 
			
		||||
        courseId: -1,
 | 
			
		||||
        courseId: undefined,
 | 
			
		||||
        categoryId: undefined,
 | 
			
		||||
        course: true,
 | 
			
		||||
        group: true,
 | 
			
		||||
@ -149,7 +149,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
                this.filter = filterData;
 | 
			
		||||
 | 
			
		||||
                // Course viewed has changed, check if the user can create events for this course calendar.
 | 
			
		||||
                this.canCreate = await AddonCalendarHelper.canEditEvents(this.filter['courseId']);
 | 
			
		||||
                this.canCreate = await AddonCalendarHelper.canEditEvents(this.filter.courseId);
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@ -170,12 +170,12 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        this.route.queryParams.subscribe(() => {
 | 
			
		||||
            this.eventId = CoreNavigator.getRouteNumberParam('eventId');
 | 
			
		||||
            this.filter.courseId = CoreNavigator.getRouteNumberParam('courseId') || -1;
 | 
			
		||||
            this.filter.courseId = CoreNavigator.getRouteNumberParam('courseId');
 | 
			
		||||
            this.year = CoreNavigator.getRouteNumberParam('year');
 | 
			
		||||
            this.month = CoreNavigator.getRouteNumberParam('month');
 | 
			
		||||
            this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming');
 | 
			
		||||
            this.showCalendar = !this.loadUpcoming;
 | 
			
		||||
            this.filter.filtered = this.filter.courseId > 0;
 | 
			
		||||
            this.filter.filtered = !!this.filter.courseId;
 | 
			
		||||
 | 
			
		||||
            if (this.eventId) {
 | 
			
		||||
                // There is an event to load, open the event in a new state.
 | 
			
		||||
 | 
			
		||||
@ -721,7 +721,7 @@ export const AddonCalendarHelper = makeSingleton(AddonCalendarHelperProvider);
 | 
			
		||||
 */
 | 
			
		||||
export type AddonCalendarFilter = {
 | 
			
		||||
    filtered: boolean; // If filter enabled (some filters applied).
 | 
			
		||||
    courseId: number; // Course Id to filter.
 | 
			
		||||
    courseId: number | undefined; // Course Id to filter.
 | 
			
		||||
    categoryId?: number; // Category Id to filter.
 | 
			
		||||
    course: boolean; // Filter to show course events.
 | 
			
		||||
    group: boolean; // Filter to show group events.
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { Injector, NgModule } from '@angular/core';
 | 
			
		||||
import { Route, RouterModule, ROUTES, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
			
		||||
import { AddonMessagesSettingsHandlerService } from './services/handlers/settings';
 | 
			
		||||
 | 
			
		||||
export const AddonMessagesDiscussionRoute: Route = {
 | 
			
		||||
    path: 'discussion',
 | 
			
		||||
@ -46,11 +45,6 @@ function buildRoutes(injector: Injector): Routes {
 | 
			
		||||
            loadChildren: () => import('./pages/search/search.module')
 | 
			
		||||
                .then(m => m.AddonMessagesSearchPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: AddonMessagesSettingsHandlerService.PAGE_NAME,
 | 
			
		||||
            loadChildren: () => import('./pages/settings/settings.module')
 | 
			
		||||
                .then(m => m.AddonMessagesSettingsPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: 'contacts', // 3.6 or greater.
 | 
			
		||||
            loadChildren: () => import('./pages/contacts/contacts.module')
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-deleg
 | 
			
		||||
import { AddonMessagesMainMenuHandler, AddonMessagesMainMenuHandlerService } from './services/handlers/mainmenu';
 | 
			
		||||
import { CoreCronDelegate } from '@services/cron';
 | 
			
		||||
import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate';
 | 
			
		||||
import { AddonMessagesSettingsHandler } from './services/handlers/settings';
 | 
			
		||||
import { AddonMessagesSettingsHandler, AddonMessagesSettingsHandlerService } from './services/handlers/settings';
 | 
			
		||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
			
		||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { AddonMessagesIndexLinkHandler } from './services/handlers/index-link';
 | 
			
		||||
@ -35,6 +35,7 @@ import { AddonMessagesSendMessageUserHandler } from './services/handlers/user-se
 | 
			
		||||
import { Network, NgZone } from '@singletons';
 | 
			
		||||
import { AddonMessagesSync } from './services/messages-sync';
 | 
			
		||||
import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron';
 | 
			
		||||
import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing';
 | 
			
		||||
 | 
			
		||||
const mainMenuChildrenRoutes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
@ -42,11 +43,18 @@ const mainMenuChildrenRoutes: Routes = [
 | 
			
		||||
        loadChildren: () => import('./messages-lazy.module').then(m => m.AddonMessagesLazyModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
const preferencesRoutes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: AddonMessagesSettingsHandlerService.PAGE_NAME,
 | 
			
		||||
        loadChildren: () => import('./pages/settings/settings.module').then(m => m.AddonMessagesSettingsPageModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }),
 | 
			
		||||
        CoreMainMenuTabRoutingModule.forChild( mainMenuChildrenRoutes),
 | 
			
		||||
        CoreSitePreferencesRoutingModule.forChild(preferencesRoutes),
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonMessages } from '../messages';
 | 
			
		||||
import { AddonMessagesMainMenuHandlerService } from './mainmenu';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Message settings handler.
 | 
			
		||||
@ -24,7 +23,7 @@ import { AddonMessagesMainMenuHandlerService } from './mainmenu';
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonMessagesSettingsHandlerService implements CoreSettingsHandler {
 | 
			
		||||
 | 
			
		||||
    static readonly PAGE_NAME = 'settings';
 | 
			
		||||
    static readonly PAGE_NAME = 'messages';
 | 
			
		||||
 | 
			
		||||
    name = 'AddonMessages';
 | 
			
		||||
    priority = 600;
 | 
			
		||||
@ -49,7 +48,7 @@ export class AddonMessagesSettingsHandlerService implements CoreSettingsHandler
 | 
			
		||||
        return {
 | 
			
		||||
            icon: 'fas-comments',
 | 
			
		||||
            title: 'addon.messages.messages',
 | 
			
		||||
            page: AddonMessagesMainMenuHandlerService.PAGE_NAME + '/' + AddonMessagesSettingsHandlerService.PAGE_NAME,
 | 
			
		||||
            page: AddonMessagesSettingsHandlerService.PAGE_NAME,
 | 
			
		||||
            class: 'addon-messages-settings-handler',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,6 @@ import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreObject } from '@singletons/object';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModAssignAssign,
 | 
			
		||||
    AddonModAssignSubmission,
 | 
			
		||||
@ -368,9 +367,9 @@ class AddonModAssignSubmissionListManager extends CorePageItemsListManager<Addon
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params {
 | 
			
		||||
        return CoreObject.withoutEmpty({
 | 
			
		||||
        return {
 | 
			
		||||
            blindId: submission.blindid,
 | 
			
		||||
        });
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { Injector, NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, ROUTES, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
			
		||||
import { AddonNotificationsSettingsHandlerService } from './services/handlers/settings';
 | 
			
		||||
 | 
			
		||||
function buildRoutes(injector: Injector): Routes {
 | 
			
		||||
    return [
 | 
			
		||||
@ -24,10 +23,6 @@ function buildRoutes(injector: Injector): Routes {
 | 
			
		||||
            path: 'list',
 | 
			
		||||
            loadChildren: () => import('./pages/list/list.module').then(m => m.AddonNotificationsListPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            path: AddonNotificationsSettingsHandlerService.PAGE_NAME,
 | 
			
		||||
            loadChildren: () => import('./pages/settings/settings.module').then(m => m.AddonNotificationsSettingsPageModule),
 | 
			
		||||
        },
 | 
			
		||||
        ...buildTabMainRoutes(injector, {
 | 
			
		||||
            redirectTo: 'list',
 | 
			
		||||
            pathMatch: 'full',
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,8 @@ import { CoreSettingsDelegate } from '@features/settings/services/settings-deleg
 | 
			
		||||
import { AddonNotificationsMainMenuHandler, AddonNotificationsMainMenuHandlerService } from './services/handlers/mainmenu';
 | 
			
		||||
import { AddonNotificationsCronHandler } from './services/handlers/cron';
 | 
			
		||||
import { AddonNotificationsPushClickHandler } from './services/handlers/push-click';
 | 
			
		||||
import { AddonNotificationsSettingsHandler } from './services/handlers/settings';
 | 
			
		||||
import { AddonNotificationsSettingsHandler, AddonNotificationsSettingsHandlerService } from './services/handlers/settings';
 | 
			
		||||
import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
@ -32,11 +33,18 @@ const routes: Routes = [
 | 
			
		||||
        loadChildren: () => import('@/addons/notifications/notifications-lazy.module').then(m => m.AddonNotificationsLazyModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
const preferencesRoutes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: AddonNotificationsSettingsHandlerService.PAGE_NAME,
 | 
			
		||||
        loadChildren: () => import('./pages/settings/settings.module').then(m => m.AddonNotificationsSettingsPageModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreMainMenuRoutingModule.forChild({ children: routes }),
 | 
			
		||||
        CoreMainMenuTabRoutingModule.forChild(routes),
 | 
			
		||||
        CoreSitePreferencesRoutingModule.forChild(preferencesRoutes),
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [CoreMainMenuRoutingModule],
 | 
			
		||||
    providers: [
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,6 @@ import {
 | 
			
		||||
    AddonNotificationsPreferencesProcessorFormatted,
 | 
			
		||||
} from '@addons/notifications/services/notifications-helper';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
// import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays notifications settings.
 | 
			
		||||
@ -61,7 +60,7 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    protected updateTimeout?: number;
 | 
			
		||||
 | 
			
		||||
    constructor() { // @todo @Optional() protected svComponent: CoreSplitViewComponent,
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.notifPrefsEnabled = AddonNotifications.isNotificationPreferencesEnabled();
 | 
			
		||||
        this.canChangeSound = CoreLocalNotifications.canDisableSound();
 | 
			
		||||
    }
 | 
			
		||||
@ -196,8 +195,6 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param handlerData
 | 
			
		||||
     */
 | 
			
		||||
    openExtraPreferences(handlerData: AddonMessageOutputHandlerData): void {
 | 
			
		||||
        // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
 | 
			
		||||
        // @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
 | 
			
		||||
        CoreNavigator.navigateToSitePath(handlerData.page, { params: handlerData.pageParams });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,6 @@ import { CoreLocalNotifications } from '@services/local-notifications';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate';
 | 
			
		||||
import { AddonNotifications } from '../notifications';
 | 
			
		||||
import { AddonNotificationsMainMenuHandlerService } from './mainmenu';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Notifications settings handler.
 | 
			
		||||
@ -26,7 +25,7 @@ import { AddonNotificationsMainMenuHandlerService } from './mainmenu';
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonNotificationsSettingsHandlerService implements CoreSettingsHandler {
 | 
			
		||||
 | 
			
		||||
    static readonly PAGE_NAME = 'settings';
 | 
			
		||||
    static readonly PAGE_NAME = 'notifications';
 | 
			
		||||
 | 
			
		||||
    name = 'AddonNotifications';
 | 
			
		||||
    priority = 500;
 | 
			
		||||
@ -50,7 +49,7 @@ export class AddonNotificationsSettingsHandlerService implements CoreSettingsHan
 | 
			
		||||
        return {
 | 
			
		||||
            icon: 'fas-bell',
 | 
			
		||||
            title: 'addon.notifications.notifications',
 | 
			
		||||
            page: AddonNotificationsMainMenuHandlerService.PAGE_NAME + '/' + AddonNotificationsSettingsHandlerService.PAGE_NAME,
 | 
			
		||||
            page: AddonNotificationsSettingsHandlerService.PAGE_NAME,
 | 
			
		||||
            class: 'addon-notifications-settings-handler',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
        <span [core-mark-required]="required">{{ field.name }}</span>
 | 
			
		||||
    </ion-label>
 | 
			
		||||
    <ion-datetime [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="format"
 | 
			
		||||
        [max]="max" [min]="min">
 | 
			
		||||
        [max]="max" [min]="min" [monthNames]="monthNames">
 | 
			
		||||
    </ion-datetime>
 | 
			
		||||
    <core-input-errors [control]="form.controls[modelName]"></core-input-errors>
 | 
			
		||||
</ion-item>
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import { AuthEmailSignupProfileField } from '@features/login/services/login-help
 | 
			
		||||
import { CoreUserProfileField } from '@features/user/services/user';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
 | 
			
		||||
import { CoreLang } from '@services/lang';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Directive to render a datetime user profile field.
 | 
			
		||||
@ -35,6 +36,7 @@ export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileField
 | 
			
		||||
    min?: number;
 | 
			
		||||
    max?: number;
 | 
			
		||||
    valueNumber = 0;
 | 
			
		||||
    monthNames?: string[];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Init the data when the field is meant to be displayed without editing.
 | 
			
		||||
@ -53,6 +55,8 @@ export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileField
 | 
			
		||||
    protected initForEdit(field: AuthEmailSignupProfileField): void {
 | 
			
		||||
        super.initForEdit(field);
 | 
			
		||||
 | 
			
		||||
        this.monthNames = CoreLang.getMonthNames();
 | 
			
		||||
 | 
			
		||||
        // Check if it's only date or it has time too.
 | 
			
		||||
        const hasTime = CoreUtils.isTrueOrOne(field.param3);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -115,6 +115,8 @@ export class AppComponent implements OnInit, AfterViewInit {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.onPlatformReady();
 | 
			
		||||
 | 
			
		||||
        // @todo: Quit app with back button. How to tell if we're at root level?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -25,9 +25,9 @@ import {
 | 
			
		||||
    ElementRef,
 | 
			
		||||
} from '@angular/core';
 | 
			
		||||
import { IonSlides } from '@ionic/angular';
 | 
			
		||||
import { BackButtonEvent } from '@ionic/core';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { Platform, Translate } from '@singletons';
 | 
			
		||||
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
 | 
			
		||||
 | 
			
		||||
@ -81,7 +81,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
 | 
			
		||||
    protected selectHistory: string[] = [];
 | 
			
		||||
 | 
			
		||||
    protected firstSelectedTab?: string; // ID of the first selected tab to control history.
 | 
			
		||||
    protected unregisterBackButtonAction: any;
 | 
			
		||||
    protected backButtonFunction: (event: BackButtonEvent) => void;
 | 
			
		||||
    protected languageChangedSubscription?: Subscription;
 | 
			
		||||
    protected isInTransition = false; // Weather Slides is in transition.
 | 
			
		||||
    protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any
 | 
			
		||||
@ -91,6 +91,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected element: ElementRef,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.backButtonFunction = this.backButtonClicked.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -171,43 +172,47 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
 | 
			
		||||
 | 
			
		||||
        this.calculateSlides();
 | 
			
		||||
 | 
			
		||||
        this.registerBackButtonAction();
 | 
			
		||||
        document.addEventListener('ionBackButton', this.backButtonFunction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register back button action.
 | 
			
		||||
     * Back button clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    protected registerBackButtonAction(): void {
 | 
			
		||||
        this.unregisterBackButtonAction = CoreApp.registerBackButtonAction(() => {
 | 
			
		||||
            // The previous page in history is not the last one, we need the previous one.
 | 
			
		||||
    protected backButtonClicked(event: BackButtonEvent): void {
 | 
			
		||||
        event.detail.register(40, (processNextHandler: () => void) => {
 | 
			
		||||
            if (this.selectHistory.length > 1) {
 | 
			
		||||
                const tabIndex = this.selectHistory[this.selectHistory.length - 2];
 | 
			
		||||
                // The previous page in history is not the last one, we need the previous one.
 | 
			
		||||
                const previousTabId = this.selectHistory[this.selectHistory.length - 2];
 | 
			
		||||
 | 
			
		||||
                // Remove curent and previous tabs from history.
 | 
			
		||||
                this.selectHistory = this.selectHistory.filter((tabId) => this.selected != tabId && tabIndex != tabId);
 | 
			
		||||
                this.selectHistory = this.selectHistory.filter((tabId) => this.selected != tabId && previousTabId != tabId);
 | 
			
		||||
 | 
			
		||||
                this.selectTab(tabIndex);
 | 
			
		||||
                this.selectTab(previousTabId);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            } else if (this.selected != this.firstSelectedTab) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.firstSelectedTab && this.selected != this.firstSelectedTab) {
 | 
			
		||||
                // All history is gone but we are not in the first selected tab.
 | 
			
		||||
                this.selectHistory = [];
 | 
			
		||||
 | 
			
		||||
                this.selectTab(this.firstSelectedTab!);
 | 
			
		||||
                this.selectTab(this.firstSelectedTab);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }, 750);
 | 
			
		||||
            processNextHandler();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * User left the page that contains the component.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewDidLeave(): void {
 | 
			
		||||
        // Unregister the custom back button action for this page
 | 
			
		||||
        this.unregisterBackButtonAction && this.unregisterBackButtonAction();
 | 
			
		||||
        // Unregister the custom back button action for this component.
 | 
			
		||||
        document.removeEventListener('ionBackButton', this.backButtonFunction);
 | 
			
		||||
 | 
			
		||||
        this.isCurrentView = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										29
									
								
								src/core/components/bs-tooltip/bs-tooltip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/core/components/bs-tooltip/bs-tooltip.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
// (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 } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to display a Bootstrap Tooltip in a popover.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'core-bs-tooltip',
 | 
			
		||||
    templateUrl: 'core-bs-tooltip.html',
 | 
			
		||||
})
 | 
			
		||||
export class CoreBSTooltipComponent {
 | 
			
		||||
 | 
			
		||||
    @Input() content = '';
 | 
			
		||||
    @Input() html?: boolean;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								src/core/components/bs-tooltip/core-bs-tooltip.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/core/components/bs-tooltip/core-bs-tooltip.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
<ion-item class="ion-text-wrap">
 | 
			
		||||
    <ion-label>
 | 
			
		||||
        <p *ngIf="html" [innerHTML]="content"></p>
 | 
			
		||||
        <p *ngIf="!html">{{content}}</p>
 | 
			
		||||
    </ion-label>
 | 
			
		||||
</ion-item>
 | 
			
		||||
@ -51,6 +51,7 @@ import { CorePipesModule } from '@pipes/pipes.module';
 | 
			
		||||
import { CoreAttachmentsComponent } from './attachments/attachments';
 | 
			
		||||
import { CoreFilesComponent } from './files/files';
 | 
			
		||||
import { CoreLocalFileComponent } from './local-file/local-file';
 | 
			
		||||
import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
@ -84,6 +85,7 @@ import { CoreLocalFileComponent } from './local-file/local-file';
 | 
			
		||||
        CoreAttachmentsComponent,
 | 
			
		||||
        CoreFilesComponent,
 | 
			
		||||
        CoreLocalFileComponent,
 | 
			
		||||
        CoreBSTooltipComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CommonModule,
 | 
			
		||||
@ -124,6 +126,7 @@ import { CoreLocalFileComponent } from './local-file/local-file';
 | 
			
		||||
        CoreAttachmentsComponent,
 | 
			
		||||
        CoreFilesComponent,
 | 
			
		||||
        CoreLocalFileComponent,
 | 
			
		||||
        CoreBSTooltipComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class CoreComponentsModule {}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
ion-app.app-root core-iframe {
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    > div {
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
        max-height: 100%;
 | 
			
		||||
@ -8,7 +9,7 @@ ion-app.app-root core-iframe {
 | 
			
		||||
        border: 0;
 | 
			
		||||
        display: block;
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
        background-color: $gray-light;
 | 
			
		||||
        background-color: var(--gray-light);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-loading-container {
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'core-iframe',
 | 
			
		||||
    templateUrl: 'core-iframe.html',
 | 
			
		||||
    styleUrls: ['iframe.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class CoreIframeComponent implements OnChanges {
 | 
			
		||||
 | 
			
		||||
@ -74,8 +75,6 @@ export class CoreIframeComponent implements OnChanges {
 | 
			
		||||
        // Show loading only with external URLs.
 | 
			
		||||
        this.loading = !this.src || !CoreUrlUtils.isLocalFileUrl(this.src);
 | 
			
		||||
 | 
			
		||||
        // @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
 | 
			
		||||
        // CoreIframeUtils.treatFrame(iframe, false, this.navCtrl);
 | 
			
		||||
        CoreIframeUtils.treatFrame(iframe, false);
 | 
			
		||||
 | 
			
		||||
        iframe.addEventListener('load', () => {
 | 
			
		||||
 | 
			
		||||
@ -132,9 +132,9 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
 | 
			
		||||
     * Get the height of the element.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Height.
 | 
			
		||||
     * @todo erase is not needed: I'm depreacating it because if not needed or getBoundingClientRect has the same result, it should
 | 
			
		||||
     * @todo erase if not needed: I'm depreacating it because if not needed or getBoundingClientRect has the same result, it should
 | 
			
		||||
     * be erased, also with getElementHeight
 | 
			
		||||
     * @deprecated
 | 
			
		||||
     * @deprecated since 3.9.5
 | 
			
		||||
     */
 | 
			
		||||
    getHeight(): number {
 | 
			
		||||
        // return this.element.nativeElement.getBoundingClientRect().height;
 | 
			
		||||
 | 
			
		||||
@ -124,7 +124,6 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
 | 
			
		||||
     * If both button containers have a context menu, merge them into a single one.
 | 
			
		||||
     *
 | 
			
		||||
     * @param buttonsContainer The container where the buttons will be moved.
 | 
			
		||||
     * @todo
 | 
			
		||||
     */
 | 
			
		||||
    protected mergeContextMenus(buttonsContainer: HTMLElement): void {
 | 
			
		||||
        // Check if both button containers have a context menu.
 | 
			
		||||
 | 
			
		||||
@ -137,7 +137,6 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        // @todo Decide which navCtrl to use. If this component is inside a split view, use the split view's master nav.
 | 
			
		||||
        CoreNavigator.navigateToSitePath('user', {
 | 
			
		||||
            params: {
 | 
			
		||||
                userId: this.userId,
 | 
			
		||||
 | 
			
		||||
@ -208,8 +208,8 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
 | 
			
		||||
            anchor.classList.add('core-image-viewer-icon');
 | 
			
		||||
            anchor.setAttribute('aria-label', label);
 | 
			
		||||
            // @todo Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
 | 
			
		||||
            anchor.innerHTML = '<ion-icon name="fas-search" class="icon icon-md ion-md-search"></ion-icon>';
 | 
			
		||||
            // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
 | 
			
		||||
            anchor.innerHTML = '<ion-icon name="fas-search" src="assets/fonts/font-awesome/solid/search.svg"></ion-icon>';
 | 
			
		||||
 | 
			
		||||
            anchor.addEventListener('click', (e: Event) => {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
@ -471,8 +471,6 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
     */
 | 
			
		||||
    protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> {
 | 
			
		||||
        const canTreatVimeo = site?.isVersionGreaterEqualThan(['3.3.4', '3.4']) || false;
 | 
			
		||||
        // @todo this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
 | 
			
		||||
        // @todo: Pass navCtrl to all treateFrame calls?
 | 
			
		||||
 | 
			
		||||
        const images = Array.from(div.querySelectorAll('img'));
 | 
			
		||||
        const anchors = Array.from(div.querySelectorAll('a'));
 | 
			
		||||
 | 
			
		||||
@ -56,8 +56,6 @@ export class CoreLinkDirective implements OnInit {
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.inApp = typeof this.inApp == 'undefined' ? this.inApp : CoreUtils.isTrueOrOne(this.inApp);
 | 
			
		||||
 | 
			
		||||
        // @todo: Handle split view?
 | 
			
		||||
 | 
			
		||||
        this.element.addEventListener('click', async (event) => {
 | 
			
		||||
            if (event.defaultPrevented) {
 | 
			
		||||
                return; // Link already treated, stop.
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,6 @@
 | 
			
		||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
 | 
			
		||||
import { CoreObject } from '@singletons/object';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Directive to go to user profile on click.
 | 
			
		||||
 */
 | 
			
		||||
@ -49,12 +47,11 @@ export class CoreUserLinkDirective implements OnInit {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
            event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
            // @todo If this directive is inside a split view, use the split view's master nav.
 | 
			
		||||
            CoreNavigator.navigateToSitePath('user', {
 | 
			
		||||
                params: CoreObject.withoutEmpty({
 | 
			
		||||
                params: {
 | 
			
		||||
                    userId: this.userId,
 | 
			
		||||
                    courseId: this.courseId,
 | 
			
		||||
                }),
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,6 @@ export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implemen
 | 
			
		||||
     * Go to the block page.
 | 
			
		||||
     */
 | 
			
		||||
    gotoBlock(): void {
 | 
			
		||||
        // @todo test that this is working properly.
 | 
			
		||||
        CoreNavigator.navigateToSitePath(this.link!, { params: this.linkParams });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,10 +22,10 @@
 | 
			
		||||
                </core-course-module-completion>
 | 
			
		||||
 | 
			
		||||
                <div class="core-module-buttons-more">
 | 
			
		||||
                    <!-- @todo <core-download-refresh [status]="downloadStatus" [enabled]="downloadEnabled"
 | 
			
		||||
                    <core-download-refresh [status]="downloadStatus" [enabled]="downloadEnabled"
 | 
			
		||||
                        [canTrustDownload]="canCheckUpdates" [loading]="spinner || module.handlerData.spinner"
 | 
			
		||||
                        (action)="download($event)">
 | 
			
		||||
                    </core-download-refresh> -->
 | 
			
		||||
                    </core-download-refresh>
 | 
			
		||||
 | 
			
		||||
                    <!-- Buttons defined by the module handler. -->
 | 
			
		||||
                    <ion-button fill="clear" *ngFor="let button of module.handlerData.buttons" color="dark"
 | 
			
		||||
 | 
			
		||||
@ -34,8 +34,6 @@ export class CoreCourseTagAreaComponent {
 | 
			
		||||
     * @param courseId The course to open.
 | 
			
		||||
     */
 | 
			
		||||
    openCourse(courseId: number): void {
 | 
			
		||||
        // @todo If this component is inside a split view, use the master nav to open it.
 | 
			
		||||
        // const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl;
 | 
			
		||||
        CoreCourseHelper.getAndOpenCourse(courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,14 +16,14 @@ import { Component, Input, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
// import { CoreUser } from '@core/user/services/user';
 | 
			
		||||
import { CoreCourses } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreCourses, CoreCoursesMyCoursesUpdatedEventData, CoreCoursesProvider } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
 | 
			
		||||
import { PopoverController, Translate } from '@singletons';
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
 | 
			
		||||
import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu';
 | 
			
		||||
import { CoreUser } from '@features/user/services/user';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This component is meant to display a course for a list of courses with progress.
 | 
			
		||||
@ -193,7 +193,6 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy {
 | 
			
		||||
     * Show the context menu.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Click Event.
 | 
			
		||||
     * @todo
 | 
			
		||||
     */
 | 
			
		||||
    async showCourseOptionsMenu(e: Event): Promise<void> {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
@ -247,22 +246,62 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy {
 | 
			
		||||
     * Hide/Unhide the course from the course list.
 | 
			
		||||
     *
 | 
			
		||||
     * @param hide True to hide and false to show.
 | 
			
		||||
     * @todo CoreUser
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    protected setCourseHidden(hide: boolean): void {
 | 
			
		||||
        return;
 | 
			
		||||
    protected async setCourseHidden(hide: boolean): Promise<void> {
 | 
			
		||||
        this.showSpinner = true;
 | 
			
		||||
 | 
			
		||||
        // We should use null to unset the preference.
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreUser.updateUserPreference(
 | 
			
		||||
                'block_myoverview_hidden_course_' + this.course.id,
 | 
			
		||||
                hide ? '1' : undefined,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            this.course.hidden = hide;
 | 
			
		||||
            CoreEvents.trigger<CoreCoursesMyCoursesUpdatedEventData>(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
 | 
			
		||||
                courseId: this.course.id,
 | 
			
		||||
                course: this.course,
 | 
			
		||||
                action: CoreCoursesProvider.ACTION_STATE_CHANGED,
 | 
			
		||||
                state: CoreCoursesProvider.STATE_HIDDEN,
 | 
			
		||||
                value: hide,
 | 
			
		||||
            }, CoreSites.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!this.isDestroyed) {
 | 
			
		||||
                CoreDomUtils.showErrorModalDefault(error, 'Error changing course visibility.');
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.showSpinner = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Favourite/Unfavourite the course from the course list.
 | 
			
		||||
     *
 | 
			
		||||
     * @param favourite True to favourite and false to unfavourite.
 | 
			
		||||
     * @todo CoreUser
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    protected setCourseFavourite(favourite: boolean): void {
 | 
			
		||||
        return;
 | 
			
		||||
    protected async setCourseFavourite(favourite: boolean): Promise<void> {
 | 
			
		||||
        this.showSpinner = true;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreCourses.setFavouriteCourse(this.course.id, favourite);
 | 
			
		||||
 | 
			
		||||
            this.course.isfavourite = favourite;
 | 
			
		||||
            CoreEvents.trigger<CoreCoursesMyCoursesUpdatedEventData>(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
 | 
			
		||||
                courseId: this.course.id,
 | 
			
		||||
                course: this.course,
 | 
			
		||||
                action: CoreCoursesProvider.ACTION_STATE_CHANGED,
 | 
			
		||||
                state: CoreCoursesProvider.STATE_FAVOURITE,
 | 
			
		||||
                value: favourite,
 | 
			
		||||
            }, CoreSites.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!this.isDestroyed) {
 | 
			
		||||
                CoreDomUtils.showErrorModalDefault(error, 'Error changing course favourite attribute.');
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.showSpinner = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -450,11 +450,11 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy {
 | 
			
		||||
     * Prefetch the course.
 | 
			
		||||
     */
 | 
			
		||||
    prefetchCourse(): void {
 | 
			
		||||
        /* @todo CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => {
 | 
			
		||||
        CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!).catch((error) => {
 | 
			
		||||
            if (!this.pageDestroyed) {
 | 
			
		||||
                CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
 | 
			
		||||
            }
 | 
			
		||||
        });*/
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -22,13 +22,11 @@ import {
 | 
			
		||||
    CoreGradesFormattedTable,
 | 
			
		||||
    CoreGradesFormattedTableColumn,
 | 
			
		||||
    CoreGradesFormattedTableRow,
 | 
			
		||||
    CoreGradesFormattedTableRowFilled,
 | 
			
		||||
    CoreGradesHelper,
 | 
			
		||||
} from '@features/grades/services/grades-helper';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreSplitViewComponent, CoreSplitViewMode } from '@components/split-view/split-view';
 | 
			
		||||
import { CoreObject } from '@singletons/object';
 | 
			
		||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
 | 
			
		||||
@ -175,7 +173,7 @@ class CoreGradesCourseManager extends CorePageItemsListManager<CoreGradesFormatt
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getItemQueryParams(): Params {
 | 
			
		||||
        return CoreObject.withoutEmpty({ userId: this.userId });
 | 
			
		||||
        return { userId: this.userId };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -203,3 +201,7 @@ class CoreGradesCourseManager extends CorePageItemsListManager<CoreGradesFormatt
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedTableRowFilled = Omit<CoreGradesFormattedTableRow, 'id'> & {
 | 
			
		||||
    id: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,8 @@ import {
 | 
			
		||||
    CoreGradesGradeItem,
 | 
			
		||||
    CoreGradesGradeOverview,
 | 
			
		||||
    CoreGradesTable,
 | 
			
		||||
    CoreGradesTableColumn,
 | 
			
		||||
    CoreGradesTableItemNameColumn,
 | 
			
		||||
    CoreGradesTableRow,
 | 
			
		||||
} from '@features/grades/services/grades';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
@ -56,14 +58,19 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
            rowclass: '',
 | 
			
		||||
        };
 | 
			
		||||
        for (const name in tableRow) {
 | 
			
		||||
            if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
 | 
			
		||||
                let content = String(tableRow[name].content);
 | 
			
		||||
            const column: CoreGradesTableColumn = tableRow[name];
 | 
			
		||||
 | 
			
		||||
            if (column.content === undefined || column.content === null) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let content = String(column.content);
 | 
			
		||||
 | 
			
		||||
            if (name == 'itemname') {
 | 
			
		||||
                this.setRowIcon(row, content);
 | 
			
		||||
                row.link = this.getModuleLink(content);
 | 
			
		||||
                    row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                    row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
                row.rowclass += column.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                row.rowclass += column.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
 | 
			
		||||
                content = content.replace(/<\/span>/gi, '\n');
 | 
			
		||||
                content = CoreTextUtils.cleanTags(content);
 | 
			
		||||
@ -77,7 +84,6 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
 | 
			
		||||
            row[name] = content.trim();
 | 
			
		||||
        }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return row;
 | 
			
		||||
    }
 | 
			
		||||
@ -88,21 +94,28 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
     * @param tableRow JSON object representing row of grades table data.
 | 
			
		||||
     * @return Formatted row object.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable {
 | 
			
		||||
        const row: CoreGradesFormattedRowForTable = {};
 | 
			
		||||
    protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedTableRow {
 | 
			
		||||
        const row: CoreGradesFormattedTableRow = {};
 | 
			
		||||
        for (let name in tableRow) {
 | 
			
		||||
            if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
 | 
			
		||||
                let content = String(tableRow[name].content);
 | 
			
		||||
            const column: CoreGradesTableColumn = tableRow[name];
 | 
			
		||||
 | 
			
		||||
            if (column.content === undefined || column.content === null) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let content = String(column.content);
 | 
			
		||||
 | 
			
		||||
            if (name == 'itemname') {
 | 
			
		||||
                    row.id = parseInt(tableRow[name]!.id.split('_')[1], 10);
 | 
			
		||||
                    row.colspan = tableRow[name]!.colspan;
 | 
			
		||||
                    row.rowspan = (tableRow.leader && tableRow.leader.rowspan) || 1;
 | 
			
		||||
                const itemNameColumn = <CoreGradesTableItemNameColumn> column;
 | 
			
		||||
 | 
			
		||||
                row.id = parseInt(itemNameColumn.id.split('_')[1], 10);
 | 
			
		||||
                row.colspan = itemNameColumn.colspan;
 | 
			
		||||
                row.rowspan = tableRow.leader?.rowspan || 1;
 | 
			
		||||
 | 
			
		||||
                this.setRowIcon(row, content);
 | 
			
		||||
                    row.rowclass = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even';
 | 
			
		||||
                    row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                    row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
                row.rowclass = itemNameColumn.class.indexOf('leveleven') < 0 ? 'odd' : 'even';
 | 
			
		||||
                row.rowclass += itemNameColumn.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                row.rowclass += itemNameColumn.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
 | 
			
		||||
                content = content.replace(/<\/span>/gi, '\n');
 | 
			
		||||
                content = CoreTextUtils.cleanTags(content);
 | 
			
		||||
@ -117,7 +130,6 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
 | 
			
		||||
            row[name] = content.trim();
 | 
			
		||||
        }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return row;
 | 
			
		||||
    }
 | 
			
		||||
@ -147,9 +159,9 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
     */
 | 
			
		||||
    formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable {
 | 
			
		||||
        const maxDepth = table.maxdepth;
 | 
			
		||||
        const formatted = {
 | 
			
		||||
            columns: [] as any[],
 | 
			
		||||
            rows: [] as any[],
 | 
			
		||||
        const formatted: CoreGradesFormattedTable = {
 | 
			
		||||
            columns: [],
 | 
			
		||||
            rows: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Columns, in order.
 | 
			
		||||
@ -185,7 +197,7 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const colName in columns) {
 | 
			
		||||
            if (typeof normalRow[colName] != 'undefined') {
 | 
			
		||||
            if (normalRow && typeof normalRow[colName] != 'undefined') {
 | 
			
		||||
                formatted.columns.push({
 | 
			
		||||
                    name: colName,
 | 
			
		||||
                    colspan: colName == 'gradeitem' ? maxDepth : 1,
 | 
			
		||||
@ -561,10 +573,7 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
     * @param text HTML where the image will be rendered.
 | 
			
		||||
     * @return Row object with the image.
 | 
			
		||||
     */
 | 
			
		||||
    protected setRowIcon(
 | 
			
		||||
        row: CoreGradesFormattedRowForTable | CoreGradesFormattedRow,
 | 
			
		||||
        text: string,
 | 
			
		||||
    ): CoreGradesFormattedRowForTable {
 | 
			
		||||
    protected setRowIcon<T extends CoreGradesFormattedRowCommonData>(row: T, text: string): T {
 | 
			
		||||
        text = text.replace('%2F', '/').replace('%2f', '/');
 | 
			
		||||
 | 
			
		||||
        if (text.indexOf('/agg_mean') > -1) {
 | 
			
		||||
@ -683,10 +692,6 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
 | 
			
		||||
export const CoreGradesHelper = makeSingleton(CoreGradesHelperProvider);
 | 
			
		||||
 | 
			
		||||
// @todo formatted data types.
 | 
			
		||||
export type CoreGradesFormattedRowForTable = any;
 | 
			
		||||
export type CoreGradesFormattedTableColumn = any;
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedItem = CoreGradesGradeItem & {
 | 
			
		||||
    weight?: string; // Weight.
 | 
			
		||||
    grade?: string; // The grade formatted.
 | 
			
		||||
@ -696,15 +701,13 @@ export type CoreGradesFormattedItem = CoreGradesGradeItem & {
 | 
			
		||||
    average?: string; // Grade average.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedRow = {
 | 
			
		||||
export type CoreGradesFormattedRowCommonData = {
 | 
			
		||||
    icon?: string;
 | 
			
		||||
    link?: string | false;
 | 
			
		||||
    rowclass?: string;
 | 
			
		||||
    itemtype?: string;
 | 
			
		||||
    image?: string;
 | 
			
		||||
    itemmodule?: string;
 | 
			
		||||
    rowspan?: number;
 | 
			
		||||
    itemname?: string; // The item returned data.
 | 
			
		||||
    weight?: string; // Weight column.
 | 
			
		||||
    grade?: string; // Grade column.
 | 
			
		||||
    range?: string;// Range column.
 | 
			
		||||
@ -716,20 +719,26 @@ export type CoreGradesFormattedRow = {
 | 
			
		||||
    contributiontocoursetotal?: string; // Contributiontocoursetotal column.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty;
 | 
			
		||||
export type CoreGradesFormattedRow = CoreGradesFormattedRowCommonData & {
 | 
			
		||||
    link?: string | false;
 | 
			
		||||
    itemname?: string; // The item returned data.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedTable = {
 | 
			
		||||
    columns: CoreGradesFormattedTableColumn[];
 | 
			
		||||
    rows: CoreGradesFormattedTableRow[];
 | 
			
		||||
};
 | 
			
		||||
export type CoreGradesFormattedTableRowFilled = {
 | 
			
		||||
    // @todo complete types.
 | 
			
		||||
    id: number;
 | 
			
		||||
    itemtype: 'category' | 'leader';
 | 
			
		||||
    grade: unknown;
 | 
			
		||||
    percentage: unknown;
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedTableRow = CoreGradesFormattedRowCommonData & {
 | 
			
		||||
    id?: number;
 | 
			
		||||
    colspan?: number;
 | 
			
		||||
    gradeitem?: string; // The item returned data.
 | 
			
		||||
};
 | 
			
		||||
type CoreGradesFormattedTableRowEmpty ={
 | 
			
		||||
    //
 | 
			
		||||
 | 
			
		||||
export type CoreGradesFormattedTableColumn = {
 | 
			
		||||
    name: string;
 | 
			
		||||
    colspan: number;
 | 
			
		||||
    hiddenPhone: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -515,64 +515,53 @@ export type CoreGradesTable = {
 | 
			
		||||
 * Grade table data item.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreGradesTableRow = {
 | 
			
		||||
    itemname?: {
 | 
			
		||||
    itemname?: CoreGradesTableItemNameColumn; // The item returned data.
 | 
			
		||||
    leader?: CoreGradesTableLeaderColumn; // The item returned data.
 | 
			
		||||
    weight?: CoreGradesTableCommonColumn; // Weight column.
 | 
			
		||||
    grade?: CoreGradesTableCommonColumn; // Grade column.
 | 
			
		||||
    range?: CoreGradesTableCommonColumn; // Range column.
 | 
			
		||||
    percentage?: CoreGradesTableCommonColumn; // Percentage column.
 | 
			
		||||
    lettergrade?: CoreGradesTableCommonColumn; // Lettergrade column.
 | 
			
		||||
    rank?: CoreGradesTableCommonColumn; // Rank column.
 | 
			
		||||
    average?: CoreGradesTableCommonColumn; // Average column.
 | 
			
		||||
    feedback?: CoreGradesTableCommonColumn; // Feedback column.
 | 
			
		||||
    contributiontocoursetotal?: CoreGradesTableCommonColumn; // Contributiontocoursetotal column.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Grade table common column data.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreGradesTableCommonColumn = {
 | 
			
		||||
    class: string; // Class.
 | 
			
		||||
    content: string; // Cell content.
 | 
			
		||||
    headers: string; // Headers.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Grade table item name column.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreGradesTableItemNameColumn = {
 | 
			
		||||
    class: string; // Class.
 | 
			
		||||
    colspan: number; // Col span.
 | 
			
		||||
    content: string; // Cell content.
 | 
			
		||||
    celltype: string; // Cell type.
 | 
			
		||||
    id: string; // Id.
 | 
			
		||||
    }; // The item returned data.
 | 
			
		||||
    leader?: {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Grade table leader column.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreGradesTableLeaderColumn = {
 | 
			
		||||
    class: string; // Class.
 | 
			
		||||
    rowspan: number; // Row span.
 | 
			
		||||
    }; // The item returned data.
 | 
			
		||||
    weight?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Weight column.
 | 
			
		||||
    grade?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Grade column.
 | 
			
		||||
    range?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Range column.
 | 
			
		||||
    percentage?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Percentage column.
 | 
			
		||||
    lettergrade?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Lettergrade column.
 | 
			
		||||
    rank?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Rank column.
 | 
			
		||||
    average?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Average column.
 | 
			
		||||
    feedback?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Feedback column.
 | 
			
		||||
    contributiontocoursetotal?: {
 | 
			
		||||
        class: string; // Class.
 | 
			
		||||
        content: string; // Cell content.
 | 
			
		||||
        headers: string; // Headers.
 | 
			
		||||
    }; // Contributiontocoursetotal column.
 | 
			
		||||
    content: undefined; // The WS doesn't return this data, but we declare it to make it coherent with the other columns.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Grade table column.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreGradesTableColumn = CoreGradesTableCommonColumn | CoreGradesTableItemNameColumn | CoreGradesTableLeaderColumn;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Grade overview data.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ export const CONTENTS_LIBRARIES_TABLE_NAME = 'h5p_contents_libraries'; // Which
 | 
			
		||||
export const LIBRARIES_CACHEDASSETS_TABLE_NAME = 'h5p_libraries_cachedassets'; // H5P cached library assets.
 | 
			
		||||
export const SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
    name: 'CoreH5PProvider',
 | 
			
		||||
    version: 1,
 | 
			
		||||
    version: 2,
 | 
			
		||||
    canBeCleared: [
 | 
			
		||||
        CONTENT_TABLE_NAME,
 | 
			
		||||
        LIBRARIES_TABLE_NAME,
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,8 @@ import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { CoreLoginHelper } from '@features/login/services/login-helper';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
 | 
			
		||||
import { CoreFilter } from '@features/filter/services/filter';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a "splash screen" while the app is being initialized.
 | 
			
		||||
@ -48,13 +50,12 @@ export class CoreLoginSitesPage implements OnInit {
 | 
			
		||||
        const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]);
 | 
			
		||||
 | 
			
		||||
        // Remove protocol from the url to show more url text.
 | 
			
		||||
        this.sites = sites.map((site) => {
 | 
			
		||||
        this.sites = await Promise.all(sites.map(async (site) => {
 | 
			
		||||
            site.siteUrl = site.siteUrl.replace(/^https?:\/\//, '');
 | 
			
		||||
            site.badge = 0;
 | 
			
		||||
            // @todo: getSiteCounter.
 | 
			
		||||
            site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0;
 | 
			
		||||
 | 
			
		||||
            return site;
 | 
			
		||||
        });
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        this.showDelete = false;
 | 
			
		||||
    }
 | 
			
		||||
@ -76,9 +77,9 @@ export class CoreLoginSitesPage implements OnInit {
 | 
			
		||||
    async deleteSite(e: Event, site: CoreSiteBasicInfo): Promise<void> {
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        const siteName = site.siteName || '';
 | 
			
		||||
        let siteName = site.siteName || '';
 | 
			
		||||
 | 
			
		||||
        // @todo: Format text: siteName.
 | 
			
		||||
        siteName = await CoreFilter.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], site.id);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreDomUtils.showDeleteConfirm('core.login.confirmdeletesite', { sitename: siteName });
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,6 @@ import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { CoreUrl } from '@singletons/url';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreObject } from '@singletons/object';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper provider that provides some common features regarding authentication.
 | 
			
		||||
@ -428,7 +427,7 @@ export class CoreLoginHelperProvider {
 | 
			
		||||
            return ['/login/credentials', { siteUrl: url }];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ['/login/site', CoreObject.withoutEmpty({ showKeyboard: showKeyboard })];
 | 
			
		||||
        return ['/login/site', { showKeyboard }];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
<ion-tabs #mainTabs [hidden]="!showTabs" [class]="'placement-' + tabsPlacement" [class.tabshidden]="hidden">
 | 
			
		||||
<ion-tabs #mainTabs [hidden]="!showTabs" [class]="'placement-' + tabsPlacement" [class.tabshidden]="hidden"
 | 
			
		||||
    (ionTabsDidChange)="tabChanged($event)">
 | 
			
		||||
    <ion-tab-bar slot="bottom" [hidden]="hidden">
 | 
			
		||||
        <ion-spinner *ngIf="!loaded"></ion-spinner>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router';
 | 
			
		||||
import { IonTabs } from '@ionic/angular';
 | 
			
		||||
import { BackButtonEvent } from '@ionic/core';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
@ -50,6 +51,11 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
 | 
			
		||||
    protected pendingRedirect?: CoreRedirectPayload;
 | 
			
		||||
    protected urlToOpen?: string;
 | 
			
		||||
    protected keyboardObserver?: CoreEventObserver;
 | 
			
		||||
    protected resizeFunction: () => void;
 | 
			
		||||
    protected backButtonFunction: (event: BackButtonEvent) => void;
 | 
			
		||||
    protected selectHistory: string[] = [];
 | 
			
		||||
    protected selectedTab?: string;
 | 
			
		||||
    protected firstSelectedTab?: string;
 | 
			
		||||
 | 
			
		||||
    @ViewChild('mainTabs') mainTabs?: IonTabs;
 | 
			
		||||
 | 
			
		||||
@ -57,7 +63,10 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        protected changeDetector: ChangeDetectorRef,
 | 
			
		||||
        protected router: Router,
 | 
			
		||||
    ) {}
 | 
			
		||||
    ) {
 | 
			
		||||
        this.resizeFunction = this.initHandlers.bind(this);
 | 
			
		||||
        this.backButtonFunction = this.backButtonClicked.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize the component.
 | 
			
		||||
@ -100,7 +109,8 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        window.addEventListener('resize', this.initHandlers.bind(this));
 | 
			
		||||
        window.addEventListener('resize', this.resizeFunction);
 | 
			
		||||
        document.addEventListener('ionBackButton', this.backButtonFunction);
 | 
			
		||||
 | 
			
		||||
        if (CoreApp.isIOS()) {
 | 
			
		||||
            // In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done.
 | 
			
		||||
@ -209,7 +219,8 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.subscription?.unsubscribe();
 | 
			
		||||
        this.redirectObs?.off();
 | 
			
		||||
        window.removeEventListener('resize', this.initHandlers.bind(this));
 | 
			
		||||
        window.removeEventListener('resize', this.resizeFunction);
 | 
			
		||||
        document.removeEventListener('ionBackButton', this.backButtonFunction);
 | 
			
		||||
        this.keyboardObserver?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -262,4 +273,46 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Selected tab has changed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    tabChanged(event: {tab: string}): void {
 | 
			
		||||
        this.selectedTab = event.tab;
 | 
			
		||||
        this.firstSelectedTab = this.firstSelectedTab ?? event.tab;
 | 
			
		||||
        this.selectHistory.push(event.tab);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Back button clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    protected backButtonClicked(event: BackButtonEvent): void {
 | 
			
		||||
        event.detail.register(20, (processNextHandler: () => void) => {
 | 
			
		||||
            if (this.selectHistory.length > 1) {
 | 
			
		||||
                // The previous page in history is not the last one, we need the previous one.
 | 
			
		||||
                const previousTab = this.selectHistory[this.selectHistory.length - 2];
 | 
			
		||||
 | 
			
		||||
                // Remove curent and previous tabs from history.
 | 
			
		||||
                this.selectHistory = this.selectHistory.filter((tab) => this.selectedTab != tab && previousTab != tab);
 | 
			
		||||
 | 
			
		||||
                this.mainTabs?.select(previousTab);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.firstSelectedTab && this.selectedTab != this.firstSelectedTab) {
 | 
			
		||||
                // All history is gone but we are not in the first selected tab.
 | 
			
		||||
                this.selectHistory = [];
 | 
			
		||||
                this.mainTabs?.select(this.firstSelectedTab);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            processNextHandler();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -146,10 +146,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param item Item to open.
 | 
			
		||||
     */
 | 
			
		||||
    openItem(item: CoreMainMenuCustomItem): void {
 | 
			
		||||
        // @todo CoreNavigator.navigateToSitePath('CoreViewerIframePage', {title: item.label, url: item.url});
 | 
			
		||||
 | 
			
		||||
        // eslint-disable-next-line no-console
 | 
			
		||||
        console.error('openItem not implemented', item);
 | 
			
		||||
        CoreNavigator.navigateToSitePath('viewer/iframe', { params: { title: item.label, url: item.url } });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -819,7 +819,6 @@ export class CoreQuestionHelperProvider {
 | 
			
		||||
            if (span.innerHTML) {
 | 
			
		||||
                // There's a hidden feedback. Mark the icon as tappable.
 | 
			
		||||
                // The click listener is only added if treatCorrectnessIconsClicks is called.
 | 
			
		||||
                // @todo: Check if another attribute needs to be used now instead of tappable.
 | 
			
		||||
                icon.setAttribute('tappable', '');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
@ -843,9 +842,7 @@ export class CoreQuestionHelperProvider {
 | 
			
		||||
        contextInstanceId?: number,
 | 
			
		||||
        courseId?: number,
 | 
			
		||||
    ): void {
 | 
			
		||||
 | 
			
		||||
        // @todo: Check if another attribute needs to be used now instead of tappable.
 | 
			
		||||
        const icons = <HTMLElement[]> Array.from(element.querySelectorAll('i.icon.questioncorrectnessicon[tappable]'));
 | 
			
		||||
        const icons = <HTMLElement[]> Array.from(element.querySelectorAll('ion-icon.questioncorrectnessicon[tappable]'));
 | 
			
		||||
        const title = Translate.instant('core.question.feedback');
 | 
			
		||||
 | 
			
		||||
        icons.forEach((icon) => {
 | 
			
		||||
 | 
			
		||||
@ -153,8 +153,6 @@ export class CoreSettingsGeneralPage {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the analytics setting is enabled or disabled.
 | 
			
		||||
     *
 | 
			
		||||
     * @todo
 | 
			
		||||
     */
 | 
			
		||||
    async analyticsEnabledChanged(): Promise<void> {
 | 
			
		||||
        await CorePushNotifications.enableAnalytics(this.analyticsEnabled);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								src/core/features/settings/pages/site/site-routing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/core/features/settings/pages/site/site-routing.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
// (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 { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { ModuleRoutesConfig } from '@/app/app-routing.module';
 | 
			
		||||
 | 
			
		||||
export const SITE_PREFERENCES_ROUTES = new InjectionToken('SITE_PREFERENCES_ROUTES');
 | 
			
		||||
 | 
			
		||||
@NgModule()
 | 
			
		||||
export class CoreSitePreferencesRoutingModule {
 | 
			
		||||
 | 
			
		||||
    static forChild(routes: ModuleRoutesConfig): ModuleWithProviders<CoreSitePreferencesRoutingModule> {
 | 
			
		||||
        return {
 | 
			
		||||
            ngModule: CoreSitePreferencesRoutingModule,
 | 
			
		||||
            providers: [
 | 
			
		||||
                { provide: SITE_PREFERENCES_ROUTES, multi: true, useValue: routes },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -9,10 +9,11 @@
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
 | 
			
		||||
    <core-split-view>
 | 
			
		||||
        <ion-refresher slot="fixed" [disabled]="!handlers.loaded" (ionRefresh)="refreshData($event)">
 | 
			
		||||
            <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
        </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <core-loading [hideUntil]="handlers.loaded">
 | 
			
		||||
            <ion-list>
 | 
			
		||||
                <ion-item *ngIf="siteInfo" class="ion-text-wrap">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
@ -25,7 +26,7 @@
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item-divider><ion-label></ion-label></ion-item-divider>
 | 
			
		||||
            <ion-item *ngIf="isIOS"
 | 
			
		||||
                <!-- <ion-item *ngIf="isIOS"
 | 
			
		||||
                    (click)="openHandler('CoreSharedFilesListPage', {manage: true, siteId: siteId, hideSitePicker: true})"
 | 
			
		||||
                    [title]="'core.sharedfiles.sharedfiles' | translate"
 | 
			
		||||
                    [class.core-selected-item]="'CoreSharedFilesListPage' == selectedPage" detail>
 | 
			
		||||
@ -34,11 +35,11 @@
 | 
			
		||||
                        <h2>{{ 'core.sharedfiles.sharedfiles' | translate }}</h2>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-badge slot="end">{{ iosSharedFiles }}</ion-badge>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
                </ion-item> -->
 | 
			
		||||
 | 
			
		||||
            <ion-item *ngFor="let handler of handlers" [ngClass]="['core-settings-handler', handler.class]"
 | 
			
		||||
                (click)="openHandler(handler.page, handler.params)" [title]="handler.title | translate" detail
 | 
			
		||||
                [class.core-selected-item]="handler.page == selectedPage">
 | 
			
		||||
                <ion-item *ngFor="let handler of handlers.items" [ngClass]="['core-settings-handler', handler.class]"
 | 
			
		||||
                    [title]="handler.title | translate" detail="true" (click)="handlers.select(handler)"
 | 
			
		||||
                    [class.core-selected-item]="handlers.isSelected(handler)">
 | 
			
		||||
                    <ion-icon [name]="handler.icon" slot="start" *ngIf="handler.icon">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
@ -77,4 +78,5 @@
 | 
			
		||||
                </ion-card>
 | 
			
		||||
            </ion-list>
 | 
			
		||||
        </core-loading>
 | 
			
		||||
    </core-split-view>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
@ -12,26 +12,51 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { Injector, NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, ROUTES, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreSitePreferencesPage } from './site';
 | 
			
		||||
import { conditionalRoutes, resolveModuleRoutes } from '@/app/app-routing.module';
 | 
			
		||||
import { SITE_PREFERENCES_ROUTES } from './site-routing';
 | 
			
		||||
import { CoreScreen } from '@services/screen';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
function buildRoutes(injector: Injector): Routes {
 | 
			
		||||
    const routes = resolveModuleRoutes(injector, SITE_PREFERENCES_ROUTES);
 | 
			
		||||
 | 
			
		||||
    const mobileRoutes: Routes = [
 | 
			
		||||
        {
 | 
			
		||||
            path: '',
 | 
			
		||||
            component: CoreSitePreferencesPage,
 | 
			
		||||
        },
 | 
			
		||||
        ...routes.siblings,
 | 
			
		||||
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const tabletRoutes: Routes = [
 | 
			
		||||
        {
 | 
			
		||||
            path: '',
 | 
			
		||||
            component: CoreSitePreferencesPage,
 | 
			
		||||
            children: routes.siblings,
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
        ...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
 | 
			
		||||
        ...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
 | 
			
		||||
    ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    providers: [
 | 
			
		||||
        { provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] },
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        CoreSitePreferencesPage,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class CoreSitePreferencesPageModule {}
 | 
			
		||||
 | 
			
		||||
@ -12,11 +12,11 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
 | 
			
		||||
import { ActivatedRouteSnapshot, Params } from '@angular/router';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
 | 
			
		||||
import { CoreSettingsDelegate, CoreSettingsHandlerData } from '../../services/settings-delegate';
 | 
			
		||||
import { CoreSettingsDelegate, CoreSettingsHandlerToDisplay } from '../../services/settings-delegate';
 | 
			
		||||
import { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@singletons/events';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
@ -26,7 +26,8 @@ import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreSiteInfo } from '@classes/site';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreScreen } from '@services/screen';
 | 
			
		||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
 | 
			
		||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the list of site settings pages.
 | 
			
		||||
@ -35,12 +36,13 @@ import { CoreScreen } from '@services/screen';
 | 
			
		||||
    selector: 'page-core-site-preferences',
 | 
			
		||||
    templateUrl: 'site.html',
 | 
			
		||||
})
 | 
			
		||||
export class CoreSitePreferencesPage implements OnInit, OnDestroy {
 | 
			
		||||
export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
 | 
			
		||||
 | 
			
		||||
    handlers: CoreSettingsSitePreferencesManager;
 | 
			
		||||
 | 
			
		||||
    isIOS: boolean;
 | 
			
		||||
    selectedPage?: string;
 | 
			
		||||
 | 
			
		||||
    handlers: CoreSettingsHandlerData[] = [];
 | 
			
		||||
    siteId: string;
 | 
			
		||||
    siteInfo?: CoreSiteInfo;
 | 
			
		||||
    siteName?: string;
 | 
			
		||||
@ -50,7 +52,6 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy {
 | 
			
		||||
        spaceUsage: 0,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    iosSharedFiles = 0;
 | 
			
		||||
    protected sitesObserver: CoreEventObserver;
 | 
			
		||||
    protected isDestroyed = false;
 | 
			
		||||
@ -59,6 +60,7 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        this.isIOS = CoreApp.isIOS();
 | 
			
		||||
        this.siteId = CoreSites.getCurrentSiteId();
 | 
			
		||||
        this.handlers = new CoreSettingsSitePreferencesManager(CoreSitePreferencesPage);
 | 
			
		||||
 | 
			
		||||
        this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, (data: CoreEventSiteUpdatedData) => {
 | 
			
		||||
            if (data.siteId == this.siteId) {
 | 
			
		||||
@ -70,30 +72,29 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy {
 | 
			
		||||
    /**
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        // @todo this.selectedPage = route.snapshot.paramMap.get('page') || undefined;
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        const pageToOpen = CoreNavigator.getRouteParam('page');
 | 
			
		||||
 | 
			
		||||
        this.fetchData().finally(() => {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
        } finally {
 | 
			
		||||
 | 
			
		||||
            if (this.selectedPage) {
 | 
			
		||||
                this.openHandler(this.selectedPage);
 | 
			
		||||
            } else if (CoreScreen.isTablet) {
 | 
			
		||||
                if (this.isIOS) {
 | 
			
		||||
                    // @todo
 | 
			
		||||
                    // this.openHandler('CoreSharedFilesListPage', { manage: true, siteId: this.siteId, hideSitePicker: true });
 | 
			
		||||
                } else if (this.handlers.length > 0) {
 | 
			
		||||
                    this.openHandler(this.handlers[0].page, this.handlers[0].params);
 | 
			
		||||
            const handler = pageToOpen ? this.handlers.items.find(handler => handler.page == pageToOpen) : undefined;
 | 
			
		||||
 | 
			
		||||
            if (handler) {
 | 
			
		||||
                this.handlers.select(handler);
 | 
			
		||||
                this.handlers.watchSplitViewOutlet(this.splitView);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.handlers.start(this.splitView);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch Data.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        this.handlers = CoreSettingsDelegate.getHandlers();
 | 
			
		||||
        this.handlers.setItems(CoreSettingsDelegate.getHandlers());
 | 
			
		||||
 | 
			
		||||
        const currentSite = CoreSites.getCurrentSite();
 | 
			
		||||
        this.siteInfo = currentSite!.getInfo();
 | 
			
		||||
@ -170,18 +171,6 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open a handler.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page Page to open.
 | 
			
		||||
     * @param params Params of the page to open.
 | 
			
		||||
     */
 | 
			
		||||
    openHandler(page: string, params?: Params): void {
 | 
			
		||||
        this.selectedPage = page;
 | 
			
		||||
        // this.splitviewCtrl.push(page, params);
 | 
			
		||||
        CoreNavigator.navigateToSitePath(page, { params });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show information about space usage actions.
 | 
			
		||||
     */
 | 
			
		||||
@ -211,3 +200,33 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper class to manage sections.
 | 
			
		||||
 */
 | 
			
		||||
class CoreSettingsSitePreferencesManager extends CorePageItemsListManager<CoreSettingsHandlerToDisplay> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getItemPath(handler: CoreSettingsHandlerToDisplay): string {
 | 
			
		||||
        return handler.page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getItemQueryParams(handler: CoreSettingsHandlerToDisplay): Params {
 | 
			
		||||
        return handler.params || {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null {
 | 
			
		||||
        // @todo: routeConfig doesn't have a path after refreshing the app.
 | 
			
		||||
        // route.component is null too, and route.parent.url is empty.
 | 
			
		||||
        return route.parent?.routeConfig?.path ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,6 @@ export class CoreTagListComponent {
 | 
			
		||||
            fromContextId: tag.taginstancecontextid,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // @todo: Check split view to navigate on the outlet if any.
 | 
			
		||||
        CoreNavigator.navigateToSitePath('/tag/index', { params, preferCurrentTab: false });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,6 @@ export interface CoreTagAreaHandler extends CoreDelegateHandler {
 | 
			
		||||
     * Get the component to use to display items.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The component (or promise resolved with component) to use, undefined if not found.
 | 
			
		||||
     * @todo, check return types.
 | 
			
		||||
     */
 | 
			
		||||
    getComponent(): Type<unknown> | Promise<Type<unknown>>;
 | 
			
		||||
}
 | 
			
		||||
@ -85,7 +84,6 @@ export class CoreTagAreaDelegateService extends CoreDelegate<CoreTagAreaHandler>
 | 
			
		||||
     * @param component Component name.
 | 
			
		||||
     * @param itemType Item type.
 | 
			
		||||
     * @return The component (or promise resolved with component) to use, undefined if not found.
 | 
			
		||||
     * @todo, check return types.
 | 
			
		||||
     */
 | 
			
		||||
    async getComponent(component: string, itemType: string): Promise<Type<unknown> | undefined> {
 | 
			
		||||
        const type = component + '/' + itemType;
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuplo
 | 
			
		||||
import { CoreIonLoadingElement } from '@classes/ion-loading';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreCourses } from '@features/courses/services/courses';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-core-user-profile',
 | 
			
		||||
@ -239,8 +240,8 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
 | 
			
		||||
    async refreshUser(event?: CustomEvent<IonRefresher>): Promise<void> {
 | 
			
		||||
        await CoreUtils.ignoreErrors(Promise.all([
 | 
			
		||||
            CoreUser.invalidateUserCache(this.userId),
 | 
			
		||||
            // @todo this.coursesProvider.invalidateUserNavigationOptions(),
 | 
			
		||||
            // this.coursesProvider.invalidateUserAdministrationOptions()
 | 
			
		||||
            CoreCourses.invalidateUserNavigationOptions(),
 | 
			
		||||
            CoreCourses.invalidateUserAdministrationOptions(),
 | 
			
		||||
        ]));
 | 
			
		||||
 | 
			
		||||
        await this.fetchUser();
 | 
			
		||||
@ -260,8 +261,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
 | 
			
		||||
     * Open the page with the user details.
 | 
			
		||||
     */
 | 
			
		||||
    openUserDetails(): void {
 | 
			
		||||
        // @todo: Navigate out of split view if this page is in the right pane.
 | 
			
		||||
        CoreNavigator.navigate('../about', {
 | 
			
		||||
        CoreNavigator.navigateToSitePath('user/about', {
 | 
			
		||||
            params: {
 | 
			
		||||
                courseId: this.courseId,
 | 
			
		||||
                userId: this.userId,
 | 
			
		||||
@ -276,7 +276,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param handler Handler that was clicked.
 | 
			
		||||
     */
 | 
			
		||||
    handlerClicked(event: Event, handler: CoreUserProfileHandlerData): void {
 | 
			
		||||
        // @todo: Pass the right navCtrl if this page is in the right pane of split view.
 | 
			
		||||
        handler.action(event, this.user!, this.courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,8 @@ import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreUserProfile } from './user';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreCourses, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface that all user profile handlers must implement.
 | 
			
		||||
@ -241,9 +242,22 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async calculateUserHandlers(user: CoreUserProfile, courseId?: number): Promise<void> {
 | 
			
		||||
        // @todo: Get Course admin/nav options.
 | 
			
		||||
        let navOptions;
 | 
			
		||||
        let admOptions;
 | 
			
		||||
        let navOptions: CoreCourseUserAdminOrNavOptionIndexed | undefined;
 | 
			
		||||
        let admOptions: CoreCourseUserAdminOrNavOptionIndexed | undefined;
 | 
			
		||||
 | 
			
		||||
        if (CoreCourses.canGetAdminAndNavOptions()) {
 | 
			
		||||
            // Get course options.
 | 
			
		||||
            const courses = await CoreCourses.getUserCourses(true);
 | 
			
		||||
            const courseIds = courses.map((course) => course.id);
 | 
			
		||||
 | 
			
		||||
            const options = await CoreCourses.getCoursesAdminAndNavOptions(courseIds);
 | 
			
		||||
 | 
			
		||||
            // For backwards compatibility we don't modify the courseId.
 | 
			
		||||
            const courseIdForOptions = courseId || CoreSites.getCurrentSiteHomeId();
 | 
			
		||||
 | 
			
		||||
            navOptions = options.navOptions[courseIdForOptions];
 | 
			
		||||
            admOptions = options.admOptions[courseIdForOptions];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const userData = this.userHandlers[user.id];
 | 
			
		||||
        userData.handlers = [];
 | 
			
		||||
 | 
			
		||||
@ -759,7 +759,7 @@ export class CoreUserProvider {
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if success.
 | 
			
		||||
     */
 | 
			
		||||
    updateUserPreference(name: string, value: string, userId?: number, siteId?: string): Promise<void> {
 | 
			
		||||
    updateUserPreference(name: string, value: string | undefined, userId?: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const preferences = [
 | 
			
		||||
            {
 | 
			
		||||
                type: name,
 | 
			
		||||
@ -780,7 +780,7 @@ export class CoreUserProvider {
 | 
			
		||||
     * @return Promise resolved if success.
 | 
			
		||||
     */
 | 
			
		||||
    async updateUserPreferences(
 | 
			
		||||
        preferences: { type: string; value: string }[],
 | 
			
		||||
        preferences: { type: string; value: string | undefined }[],
 | 
			
		||||
        disableNotifications?: boolean,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
 | 
			
		||||
@ -15,16 +15,19 @@
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreViewerImageComponent } from './image/image';
 | 
			
		||||
import { CoreViewerTextComponent } from './text/text';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        CoreViewerImageComponent,
 | 
			
		||||
        CoreViewerTextComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        CoreViewerImageComponent,
 | 
			
		||||
        CoreViewerTextComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								src/core/features/viewer/components/image/image.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/core/features/viewer/components/image/image.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-title>{{ title }}</ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
 | 
			
		||||
                <ion-icon name="fas-times" slot="icon-only"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<!-- @todo: zoom="true" maxZoom="2" . Now we need to use ionSlider? -->
 | 
			
		||||
<ion-content [scrollX]="true" [scrollY]="true" class="core-zoom-pane">
 | 
			
		||||
    <img [src]="image" [alt]="title" core-external-content [component]="component" [componentId]="componentId">
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/core/features/viewer/components/image/image.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/core/features/viewer/components/image/image.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
:host {
 | 
			
		||||
    .core-zoom-pane {
 | 
			
		||||
        height: 100%;
 | 
			
		||||
 | 
			
		||||
        img {
 | 
			
		||||
            max-width: none;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								src/core/features/viewer/components/image/image.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/core/features/viewer/components/image/image.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
// (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, OnInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { ModalController, Translate } from '@singletons';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Modal component to view an image.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'core-viewer-image',
 | 
			
		||||
    templateUrl: 'image.html',
 | 
			
		||||
    styleUrls: ['image.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class CoreViewerImageComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @Input() title?: string; // Modal title.
 | 
			
		||||
    @Input() image?: string; // Image URL.
 | 
			
		||||
    @Input() component?: string; // Component to use in external-content.
 | 
			
		||||
    @Input() componentId?: string | number; // Component ID to use in external-content.
 | 
			
		||||
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.title = this.title || Translate.instant('core.imageviewer');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close modal.
 | 
			
		||||
     */
 | 
			
		||||
    closeModal(): void {
 | 
			
		||||
        ModalController.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/core/features/viewer/pages/iframe/iframe.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/core/features/viewer/pages/iframe/iframe.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ title }}</ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-loading [hideUntil]="finalUrl">
 | 
			
		||||
        <core-iframe *ngIf="finalUrl" [src]="finalUrl"></core-iframe>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										38
									
								
								src/core/features/viewer/pages/iframe/iframe.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/core/features/viewer/pages/iframe/iframe.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
// (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 { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreViewerIframePage } from './iframe';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: CoreViewerIframePage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        CoreViewerIframePage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class CoreViewerIframePageModule {}
 | 
			
		||||
							
								
								
									
										57
									
								
								src/core/features/viewer/pages/iframe/iframe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/core/features/viewer/pages/iframe/iframe.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page to display a URL in an iframe.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'core-viewer-iframe',
 | 
			
		||||
    templateUrl: 'iframe.html',
 | 
			
		||||
})
 | 
			
		||||
export class CoreViewerIframePage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    title?: string; // Page title.
 | 
			
		||||
    url?: string; // Iframe URL.
 | 
			
		||||
    /* Whether the URL should be open with auto-login. Accepts the following values:
 | 
			
		||||
        "yes" -> Always auto-login.
 | 
			
		||||
        "no" -> Never auto-login.
 | 
			
		||||
        "check" -> Auto-login only if it points to the current site. Default value. */
 | 
			
		||||
    autoLogin?: string;
 | 
			
		||||
    finalUrl?: string;
 | 
			
		||||
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.title = CoreNavigator.getRouteParam('title');
 | 
			
		||||
        this.url = CoreNavigator.getRouteParam('url');
 | 
			
		||||
        this.autoLogin = CoreNavigator.getRouteParam('autoLogin') || 'check';
 | 
			
		||||
 | 
			
		||||
        if (!this.url) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const currentSite = CoreSites.getCurrentSite();
 | 
			
		||||
 | 
			
		||||
        if (currentSite && (this.autoLogin == 'yes' || (this.autoLogin == 'check' && currentSite.containsUrl(this.url)))) {
 | 
			
		||||
            // Format the URL to add auto-login.
 | 
			
		||||
            this.finalUrl = await currentSite.getAutoLoginUrl(this.url, false);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.finalUrl = this.url;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/core/features/viewer/viewer-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/core/features/viewer/viewer-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
// (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 { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: 'iframe',
 | 
			
		||||
        loadChildren: () => import('./pages/iframe/iframe.module').then( m => m.CoreViewerIframePageModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [RouterModule.forChild(routes)],
 | 
			
		||||
})
 | 
			
		||||
export class CoreViewerLazyModule {}
 | 
			
		||||
@ -13,11 +13,21 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
			
		||||
import { CoreViewerComponentsModule } from './components/components.module';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: 'viewer',
 | 
			
		||||
        loadChildren: () => import('./viewer-lazy.module').then(m => m.CoreViewerLazyModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreMainMenuTabRoutingModule.forChild(routes),
 | 
			
		||||
        CoreViewerComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,6 @@ import { CanActivate, CanLoad, UrlTree } from '@angular/router';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { Router } from '@singletons';
 | 
			
		||||
import { CoreObject } from '@singletons/object';
 | 
			
		||||
import { CoreConstants } from '../constants';
 | 
			
		||||
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
@ -62,10 +61,10 @@ export class CoreRedirectGuard implements CanLoad, CanActivate {
 | 
			
		||||
                );
 | 
			
		||||
                const route = Router.parseUrl('/main');
 | 
			
		||||
 | 
			
		||||
                route.queryParams = CoreObject.withoutEmpty({
 | 
			
		||||
                route.queryParams = {
 | 
			
		||||
                    redirectPath: redirect.page,
 | 
			
		||||
                    redirectParams: redirect.params,
 | 
			
		||||
                });
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                return loggedIn ? route : true;
 | 
			
		||||
            }
 | 
			
		||||
@ -78,10 +77,10 @@ export class CoreRedirectGuard implements CanLoad, CanActivate {
 | 
			
		||||
            // Redirect to non-site path.
 | 
			
		||||
            const route = Router.parseUrl(redirect.page);
 | 
			
		||||
 | 
			
		||||
            route.queryParams = CoreObject.withoutEmpty({
 | 
			
		||||
            route.queryParams = {
 | 
			
		||||
                redirectPath: redirect.page,
 | 
			
		||||
                redirectParams: redirect.params,
 | 
			
		||||
            });
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            return route;
 | 
			
		||||
        } finally {
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,6 @@ export class CoreAppProvider {
 | 
			
		||||
    protected isKeyboardShown = false;
 | 
			
		||||
    protected keyboardOpening = false;
 | 
			
		||||
    protected keyboardClosing = false;
 | 
			
		||||
    protected backActions: {callback: () => boolean; priority: number}[] = [];
 | 
			
		||||
    protected forceOffline = false;
 | 
			
		||||
    protected redirect?: CoreRedirectData;
 | 
			
		||||
 | 
			
		||||
@ -68,11 +67,6 @@ export class CoreAppProvider {
 | 
			
		||||
        this.schemaVersionsManager = new Promise(resolve => this.resolveSchemaVersionsManager = resolve);
 | 
			
		||||
        this.db = CoreDB.getDB(DBNAME);
 | 
			
		||||
        this.logger = CoreLogger.getInstance('CoreAppProvider');
 | 
			
		||||
 | 
			
		||||
        // @todo
 | 
			
		||||
        // this.platform.registerBackButtonAction(() => {
 | 
			
		||||
        //     this.backButtonAction();
 | 
			
		||||
        // }, 100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -592,37 +586,18 @@ export class CoreAppProvider {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The back button event is triggered when the user presses the native
 | 
			
		||||
     * platform's back button, also referred to as the "hardware" back button.
 | 
			
		||||
     * This event is only used within Cordova apps running on Android and
 | 
			
		||||
     * Windows platforms. This event is not fired on iOS since iOS doesn't come
 | 
			
		||||
     * with a hardware back button in the same sense an Android or Windows device
 | 
			
		||||
     * does.
 | 
			
		||||
     * Register a back button action.
 | 
			
		||||
     * This function is deprecated and no longer works. You should now use Ionic events directly, please see:
 | 
			
		||||
     * https://ionicframework.com/docs/developing/hardware-back-button
 | 
			
		||||
     *
 | 
			
		||||
     * Registering a hardware back button action and setting a priority allows
 | 
			
		||||
     * apps to control which action should be called when the hardware back
 | 
			
		||||
     * button is pressed. This method decides which of the registered back button
 | 
			
		||||
     * actions has the highest priority and should be called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param callback Called when the back button is pressed, if this registered action has the highest priority.
 | 
			
		||||
     * @param priority Set the priority for this action. All actions sorted by priority will be executed since one of
 | 
			
		||||
     *                 them returns true.
 | 
			
		||||
     *                 - Priorities higher or equal than 1000 will go before closing modals
 | 
			
		||||
     *                 - Priorities lower than 500 will only be executed if you are in the first state of the app (before exit).
 | 
			
		||||
     * @param callback Called when the back button is pressed.
 | 
			
		||||
     * @param priority Priority.
 | 
			
		||||
     * @return A function that, when called, will unregister the back button action.
 | 
			
		||||
     * @deprecated since 3.9.5
 | 
			
		||||
     */
 | 
			
		||||
    registerBackButtonAction(callback: () => boolean, priority: number = 0): () => boolean {
 | 
			
		||||
        const action = { callback, priority };
 | 
			
		||||
 | 
			
		||||
        this.backActions.push(action);
 | 
			
		||||
 | 
			
		||||
        this.backActions.sort((a, b) => b.priority - a.priority);
 | 
			
		||||
 | 
			
		||||
        return (): boolean => {
 | 
			
		||||
            const index = this.backActions.indexOf(action);
 | 
			
		||||
 | 
			
		||||
            return index >= 0 && !!this.backActions.splice(index, 1);
 | 
			
		||||
        };
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    registerBackButtonAction(callback: () => boolean, priority = 0): () => boolean {
 | 
			
		||||
        return () => false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreWSExternalWarning } from '@services/ws';
 | 
			
		||||
import { CoreCourses } from '@features/courses/services/courses';
 | 
			
		||||
 | 
			
		||||
const ROOT_CACHE_KEY = 'mmGroups:';
 | 
			
		||||
 | 
			
		||||
@ -242,8 +243,11 @@ export class CoreGroupsProvider {
 | 
			
		||||
            return this.getUserGroupsInCourse(0, siteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // @todo Get courses.
 | 
			
		||||
        return <CoreGroup[]>[];
 | 
			
		||||
        const courses = <CoreCourseBase[]> await CoreCourses.getUserCourses(false, siteId);
 | 
			
		||||
 | 
			
		||||
        courses.push({ id: site.getSiteHomeId() }); // Add site home.
 | 
			
		||||
 | 
			
		||||
        return this.getUserGroups(courses, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -156,8 +156,6 @@ export class CoreLangProvider {
 | 
			
		||||
        // Use british english when parent english is loaded.
 | 
			
		||||
        moment.locale(language == 'en' ? 'en-gb' : language);
 | 
			
		||||
 | 
			
		||||
        // @todo: Set data for ion-datetime.
 | 
			
		||||
 | 
			
		||||
        this.currentLanguage = language;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
@ -275,6 +273,42 @@ export class CoreLangProvider {
 | 
			
		||||
        return this.fallbackLanguage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get translated month names.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Translated month names.
 | 
			
		||||
     */
 | 
			
		||||
    getMonthNames(): string[] {
 | 
			
		||||
        return moment.months().map(this.capitalize.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get translated month short names.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Translated month short names.
 | 
			
		||||
     */
 | 
			
		||||
    getMonthShortNames(): string[] {
 | 
			
		||||
        return moment.monthsShort().map(this.capitalize.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get translated day names.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Translated day names.
 | 
			
		||||
     */
 | 
			
		||||
    getDayNames(): string[] {
 | 
			
		||||
        return moment.weekdays().map(this.capitalize.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get translated day short names.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Translated day short names.
 | 
			
		||||
     */
 | 
			
		||||
    getDayShortNames(): string[] {
 | 
			
		||||
        return moment.weekdaysShort().map(this.capitalize.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the full list of translations for a certain language.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -127,7 +127,7 @@ export class CoreNavigatorService {
 | 
			
		||||
        const url: string[] = [/^[./]/.test(path) ? path : `./${path}`];
 | 
			
		||||
        const navigationOptions: NavigationOptions = CoreObject.withoutEmpty({
 | 
			
		||||
            animated: options.animated,
 | 
			
		||||
            queryParams: CoreObject.isEmpty(options.params ?? {}) ? null : options.params,
 | 
			
		||||
            queryParams: CoreObject.isEmpty(options.params ?? {}) ? null : CoreObject.withoutEmpty(options.params),
 | 
			
		||||
            relativeTo: path.startsWith('/') ? null : this.getCurrentRoute(),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ import { CoreConfig } from '@services/config';
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreH5P } from '@features/h5p/services/h5p';
 | 
			
		||||
 | 
			
		||||
const VERSION_APPLIED = 'version_applied';
 | 
			
		||||
 | 
			
		||||
@ -42,13 +43,13 @@ export class CoreUpdateManagerProvider {
 | 
			
		||||
     * @return Promise resolved when the update process finishes.
 | 
			
		||||
     */
 | 
			
		||||
    async load(): Promise<void> {
 | 
			
		||||
        const promises = [];
 | 
			
		||||
        const promises: Promise<unknown>[] = [];
 | 
			
		||||
        const versionCode = CoreConstants.CONFIG.versioncode;
 | 
			
		||||
 | 
			
		||||
        const versionApplied = await CoreConfig.get<number>(VERSION_APPLIED, 0);
 | 
			
		||||
 | 
			
		||||
        if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) {
 | 
			
		||||
            // @todo: H5P update.
 | 
			
		||||
            promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
 | 
			
		||||
@ -31,11 +31,20 @@ import { CoreIonLoadingElement } from '@classes/ion-loading';
 | 
			
		||||
import { CoreCanceledError } from '@classes/errors/cancelederror';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreSilentError } from '@classes/errors/silenterror';
 | 
			
		||||
 | 
			
		||||
import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons';
 | 
			
		||||
import {
 | 
			
		||||
    makeSingleton,
 | 
			
		||||
    Translate,
 | 
			
		||||
    AlertController,
 | 
			
		||||
    LoadingController,
 | 
			
		||||
    ToastController,
 | 
			
		||||
    PopoverController,
 | 
			
		||||
    ModalController,
 | 
			
		||||
} from '@singletons';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
 | 
			
		||||
import { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import { CoreBSTooltipComponent } from '@components/bs-tooltip/bs-tooltip';
 | 
			
		||||
import { CoreViewerImageComponent } from '@features/viewer/components/image/image';
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * "Utils" service with helper functions for UI, DOM elements and HTML code.
 | 
			
		||||
@ -810,8 +819,18 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
            el.setAttribute('data-original-title', content);
 | 
			
		||||
            el.setAttribute('title', '');
 | 
			
		||||
 | 
			
		||||
            el.addEventListener('click', () => {
 | 
			
		||||
                // @todo
 | 
			
		||||
            el.addEventListener('click', async (ev: Event) => {
 | 
			
		||||
                const html = el.getAttribute('data-html');
 | 
			
		||||
 | 
			
		||||
                const popover = await PopoverController.create({
 | 
			
		||||
                    component: CoreBSTooltipComponent,
 | 
			
		||||
                    componentProps: {
 | 
			
		||||
                        content,
 | 
			
		||||
                        html: html === 'true',
 | 
			
		||||
                    },
 | 
			
		||||
                    event: ev,
 | 
			
		||||
                });
 | 
			
		||||
                await popover.present();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@ -1554,12 +1573,32 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
     * @param message Modal message.
 | 
			
		||||
     * @param buttons Buttons to pass to the modal.
 | 
			
		||||
     * @param placeholder Placeholder of the input element if any.
 | 
			
		||||
     * @return Promise resolved when modal presented.
 | 
			
		||||
     * @return Promise resolved with the entered text if any.
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    showTextareaPrompt(title: string, message: string, buttons: (string | unknown)[], placeholder?: string): Promise<unknown> {
 | 
			
		||||
        // @todo
 | 
			
		||||
        return Promise.resolve();
 | 
			
		||||
    async showTextareaPrompt(
 | 
			
		||||
        title: string,
 | 
			
		||||
        message: string,
 | 
			
		||||
        buttons: AlertButton[],
 | 
			
		||||
        placeholder?: string,
 | 
			
		||||
    ): Promise<string | undefined> {
 | 
			
		||||
        const alert = await AlertController.create({
 | 
			
		||||
            header: title,
 | 
			
		||||
            message,
 | 
			
		||||
            inputs: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'textarea-prompt',
 | 
			
		||||
                    type: 'textarea',
 | 
			
		||||
                    placeholder: placeholder,
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            buttons,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await alert.present();
 | 
			
		||||
 | 
			
		||||
        const result = await alert.onWillDismiss();
 | 
			
		||||
 | 
			
		||||
        return result.data?.values?.['textarea-prompt'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1671,9 +1710,30 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
     * @param componentId An ID to use in conjunction with the component.
 | 
			
		||||
     * @param fullScreen Whether the modal should be full screen.
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    viewImage(image: string, title?: string | null, component?: string, componentId?: string | number, fullScreen?: boolean): void {
 | 
			
		||||
        // @todo
 | 
			
		||||
    async viewImage(
 | 
			
		||||
        image: string,
 | 
			
		||||
        title?: string | null,
 | 
			
		||||
        component?: string,
 | 
			
		||||
        componentId?: string | number,
 | 
			
		||||
        fullScreen?: boolean,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        if (!image) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const modal = await ModalController.create({
 | 
			
		||||
            component: CoreViewerImageComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                title,
 | 
			
		||||
                image,
 | 
			
		||||
                component,
 | 
			
		||||
                componentId,
 | 
			
		||||
            },
 | 
			
		||||
            cssClass: fullScreen ? 'core-modal-fullscreen' : '',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        await modal.present();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -25,10 +25,11 @@ import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUrlUtils } from '@services/utils/url';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
 | 
			
		||||
import { makeSingleton, Network, Platform, NgZone } from '@singletons';
 | 
			
		||||
import { makeSingleton, Network, Platform, NgZone, Translate } from '@singletons';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { CoreUrl } from '@singletons/url';
 | 
			
		||||
import { CoreWindow } from '@singletons/window';
 | 
			
		||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Possible types of frame elements.
 | 
			
		||||
@ -74,21 +75,7 @@ export class CoreIframeUtilsProvider {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // The frame has an online URL but the app is offline. Show a warning, or a link if the URL can be opened in the app.
 | 
			
		||||
            const div = document.createElement('div');
 | 
			
		||||
 | 
			
		||||
            div.setAttribute('text-center', '');
 | 
			
		||||
            div.setAttribute('padding', '');
 | 
			
		||||
            div.classList.add('core-iframe-offline-warning');
 | 
			
		||||
 | 
			
		||||
            // @todo Handle link
 | 
			
		||||
 | 
			
		||||
            // Add a class to specify that the iframe is hidden.
 | 
			
		||||
            element.classList.add('core-iframe-offline-disabled');
 | 
			
		||||
 | 
			
		||||
            if (isSubframe) {
 | 
			
		||||
                // We cannot apply CSS styles in subframes, just hide the iframe.
 | 
			
		||||
                element.style.display = 'none';
 | 
			
		||||
            }
 | 
			
		||||
            this.addOfflineWarning(element, src, isSubframe);
 | 
			
		||||
 | 
			
		||||
            // If the network changes, check it again.
 | 
			
		||||
            const subscription = Network.onConnect().subscribe(() => {
 | 
			
		||||
@ -124,6 +111,77 @@ export class CoreIframeUtilsProvider {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add an offline warning message.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element The frame to check (iframe, embed, ...).
 | 
			
		||||
     * @param src Frame src.
 | 
			
		||||
     * @param isSubframe Whether it's a frame inside another frame.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async addOfflineWarning(element: HTMLElement, src: string, isSubframe?: boolean): Promise<void> {
 | 
			
		||||
        const site = CoreSites.getCurrentSite();
 | 
			
		||||
        const username = site ? site.getInfo()?.username : undefined;
 | 
			
		||||
 | 
			
		||||
        const div = document.createElement('div');
 | 
			
		||||
        div.classList.add('core-iframe-offline-warning', 'ion-padding', 'ion-text-center');
 | 
			
		||||
 | 
			
		||||
        // Add a class to specify that the iframe is hidden.
 | 
			
		||||
        element.classList.add('core-iframe-offline-disabled');
 | 
			
		||||
        if (isSubframe) {
 | 
			
		||||
            // We cannot apply CSS styles in subframes, just hide the iframe.
 | 
			
		||||
            element.style.display = 'none';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const canHandleLink = await CoreContentLinksHelper.canHandleLink(src, undefined, username);
 | 
			
		||||
 | 
			
		||||
        if (!canHandleLink) {
 | 
			
		||||
            // @todo: The not connected icon isn't seen due to the div's height. Also, it's quite big.
 | 
			
		||||
            div.innerHTML = (isSubframe ? '' : '<div class="core-iframe-network-error"></div>') +
 | 
			
		||||
                '<p>' + Translate.instant('core.networkerroriframemsg') + '</p>';
 | 
			
		||||
 | 
			
		||||
            element.parentElement?.insertBefore(div, element);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let link: HTMLElement | undefined;
 | 
			
		||||
 | 
			
		||||
        if (isSubframe) {
 | 
			
		||||
            // Ionic styles are not available in subframes, adding some minimal inline styles.
 | 
			
		||||
            link = document.createElement('a');
 | 
			
		||||
            link.style.display = 'block';
 | 
			
		||||
            link.style.padding = '1em';
 | 
			
		||||
            link.style.fontWeight = '500';
 | 
			
		||||
            link.style.textAlign = 'center';
 | 
			
		||||
            link.style.textTransform = 'uppercase';
 | 
			
		||||
            link.style.cursor = 'pointer';
 | 
			
		||||
        } else {
 | 
			
		||||
            link = document.createElement('ion-button');
 | 
			
		||||
            link.setAttribute('expand', 'block');
 | 
			
		||||
            link.setAttribute('size', 'default');
 | 
			
		||||
            link.classList.add(
 | 
			
		||||
                'button',
 | 
			
		||||
                'button-block',
 | 
			
		||||
                'button-default',
 | 
			
		||||
                'button-solid',
 | 
			
		||||
                'ion-activatable',
 | 
			
		||||
                'ion-focusable',
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        link.innerHTML = Translate.instant('core.viewembeddedcontent');
 | 
			
		||||
 | 
			
		||||
        link.onclick = (event: Event): void => {
 | 
			
		||||
            CoreContentLinksHelper.handleLink(src, username);
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        div.appendChild(link);
 | 
			
		||||
 | 
			
		||||
        element.parentElement?.insertBefore(div, element);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given an element, return the content window and document.
 | 
			
		||||
     * Please notice that the element should be an iframe, embed or similar.
 | 
			
		||||
 | 
			
		||||
@ -252,4 +252,4 @@ export const NavController = makeSingleton(NavControllerService);
 | 
			
		||||
export const Router = makeSingleton(RouterService, ['routerState', 'url']);
 | 
			
		||||
 | 
			
		||||
// Convert external libraries injectables.
 | 
			
		||||
export const Translate = makeSingleton(TranslateService, ['onLangChange']);
 | 
			
		||||
export const Translate = makeSingleton(TranslateService, ['onLangChange', 'translations']);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
/** Format Text - Show more styles. */
 | 
			
		||||
/** Styles of elements inside the directive should be placed in format-text.scss */
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
core-format-text {
 | 
			
		||||
    user-select: text;
 | 
			
		||||
    word-break: break-word;
 | 
			
		||||
@ -78,4 +80,40 @@ core-format-text {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-adapted-img-container {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-image-viewer-icon {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        @include position(null, 10px, 10px, null);
 | 
			
		||||
        color: var(--black);
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
        background-color: rgba(255, 255, 255, .5);
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
 | 
			
		||||
        width: 32px;
 | 
			
		||||
        height: 32px;
 | 
			
		||||
        max-width: 32px;
 | 
			
		||||
        line-height: 32px;
 | 
			
		||||
        font-size: 24px;
 | 
			
		||||
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            margin-top: 3px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
            opacity: .7;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.dark {
 | 
			
		||||
    core-format-text .core-image-viewer-icon {
 | 
			
		||||
        background-color: rgba(0, 0, 0, .5);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -95,7 +95,8 @@ ion-button.button-small ion-icon.faicon[slot] {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Ionic alert.
 | 
			
		||||
ion-alert.core-alert-network-error .alert-head {
 | 
			
		||||
ion-alert.core-alert-network-error .alert-head,
 | 
			
		||||
div.core-iframe-network-error {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    content: " ";
 | 
			
		||||
    background: url("/assets/fonts/font-awesome/solid/wifi.svg") no-repeat 50% 50%;
 | 
			
		||||
@ -113,7 +114,8 @@ ion-alert.core-alert-network-error .alert-head {
 | 
			
		||||
        mask: url("/assets/fonts/font-awesome/solid/exclamation-triangle.svg") no-repeat 50% 50%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
[dir=rtl] ion-alert.core-alert-network-error .alert-head::after {
 | 
			
		||||
[dir=rtl] ion-alert.core-alert-network-error .alert-head::after,
 | 
			
		||||
[dir=rtl] div.core-iframe-network-error::after {
 | 
			
		||||
    right: unset;
 | 
			
		||||
    left: -15%;
 | 
			
		||||
}
 | 
			
		||||
@ -442,3 +444,7 @@ ion-button.core-button-select {
 | 
			
		||||
.core-monospaced {
 | 
			
		||||
    font-family: Andale Mono,Monaco,Courier New,DejaVu Sans Mono,monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.core-iframe-offline-disabled {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user