Merge pull request #2688 from dpalou/MOBILE-3708

Mobile 3708
main
Dani Palou 2021-03-03 13:23:02 +01:00 committed by GitHub
commit 32be164be7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1046 additions and 444 deletions

View File

@ -22,7 +22,6 @@ import { CoreUtils } from '@services/utils/utils';
import { CorePageItemsListManager } from '@classes/page-items-list-manager'; import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreObject } from '@singletons/object';
/** /**
* Page that displays the list of calendar events. * Page that displays the list of calendar events.
@ -125,10 +124,10 @@ class AddonBadgesUserBadgesManager extends CorePageItemsListManager<AddonBadgesU
* @inheritdoc * @inheritdoc
*/ */
protected getItemQueryParams(): Params { protected getItemQueryParams(): Params {
return CoreObject.withoutEmpty({ return {
courseId: this.courseId, courseId: this.courseId,
userId: this.userId, userId: this.userId,
}); };
} }
/** /**

View File

@ -18,7 +18,6 @@ import { CoreUserProfile } from '@features/user/services/user';
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreObject } from '@singletons/object';
import { AddonBadges } from '../badges'; import { AddonBadges } from '../badges';
/** /**
@ -74,7 +73,7 @@ export class AddonBadgesUserHandlerService implements CoreUserProfileHandler {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
CoreNavigator.navigateToSitePath('/badges', { CoreNavigator.navigateToSitePath('/badges', {
params: CoreObject.withoutEmpty({ courseId, userId: user.id }), params: { courseId, userId: user.id },
}); });
}, },
}; };

View File

@ -161,7 +161,7 @@
</ion-item> </ion-item>
<ion-datetime #notificationPicker hidden [(ngModel)]="notificationTimeText" <ion-datetime #notificationPicker hidden [(ngModel)]="notificationTimeText"
[displayFormat]="notificationFormat" [min]="notificationMin" [max]="notificationMax" [displayFormat]="notificationFormat" [min]="notificationMin" [max]="notificationMax"
doneText]="'core.add' | translate"(ionChange)="addNotificationTime()"> [doneText]="'core.add' | translate" (ionChange)="addNotificationTime()" [monthNames]="monthNames">
</ion-datetime> </ion-datetime>
</ng-container> </ng-container>
</ion-card> </ion-card>

View File

@ -46,6 +46,7 @@ import { AddonCalendarReminderDBRecord } from '../../services/database/calendar'
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { CoreScreen } from '@services/screen'; import { CoreScreen } from '@services/screen';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreLang } from '@services/lang';
/** /**
* Page that displays a single calendar event. * Page that displays a single calendar event.
@ -87,6 +88,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
isOnline = false; isOnline = false;
syncIcon = CoreConstants.ICON_LOADING; // Sync icon. syncIcon = CoreConstants.ICON_LOADING; // Sync icon.
isSplitViewOn = false; isSplitViewOn = false;
monthNames?: string[];
constructor( constructor(
@Optional() protected svComponent: CoreSplitViewComponent, @Optional() protected svComponent: CoreSplitViewComponent,
@ -137,6 +139,8 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
protected async asyncConstructor(): Promise<void> { protected async asyncConstructor(): Promise<void> {
if (this.notificationsEnabled) { if (this.notificationsEnabled) {
this.monthNames = CoreLang.getMonthNames();
this.reminders = await AddonCalendar.getEventReminders(this.eventId); this.reminders = await AddonCalendar.getEventReminders(this.eventId);
this.defaultTime = await AddonCalendar.getDefaultNotificationTime() * 60; this.defaultTime = await AddonCalendar.getDefaultNotificationTime() * 60;
@ -434,8 +438,6 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
* Open the page to edit the event. * Open the page to edit the event.
*/ */
openEdit(): void { 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 } }); CoreNavigator.navigateToSitePath('/calendar/edit', { params: { eventId: this.eventId } });
} }

View File

@ -74,7 +74,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
loadUpcoming = false; loadUpcoming = false;
filter: AddonCalendarFilter = { filter: AddonCalendarFilter = {
filtered: false, filtered: false,
courseId: -1, courseId: undefined,
categoryId: undefined, categoryId: undefined,
course: true, course: true,
group: true, group: true,
@ -149,7 +149,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
this.filter = filterData; this.filter = filterData;
// Course viewed has changed, check if the user can create events for this course calendar. // 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.route.queryParams.subscribe(() => {
this.eventId = CoreNavigator.getRouteNumberParam('eventId'); this.eventId = CoreNavigator.getRouteNumberParam('eventId');
this.filter.courseId = CoreNavigator.getRouteNumberParam('courseId') || -1; this.filter.courseId = CoreNavigator.getRouteNumberParam('courseId');
this.year = CoreNavigator.getRouteNumberParam('year'); this.year = CoreNavigator.getRouteNumberParam('year');
this.month = CoreNavigator.getRouteNumberParam('month'); this.month = CoreNavigator.getRouteNumberParam('month');
this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming'); this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming');
this.showCalendar = !this.loadUpcoming; this.showCalendar = !this.loadUpcoming;
this.filter.filtered = this.filter.courseId > 0; this.filter.filtered = !!this.filter.courseId;
if (this.eventId) { if (this.eventId) {
// There is an event to load, open the event in a new state. // There is an event to load, open the event in a new state.

View File

@ -721,7 +721,7 @@ export const AddonCalendarHelper = makeSingleton(AddonCalendarHelperProvider);
*/ */
export type AddonCalendarFilter = { export type AddonCalendarFilter = {
filtered: boolean; // If filter enabled (some filters applied). 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. categoryId?: number; // Category Id to filter.
course: boolean; // Filter to show course events. course: boolean; // Filter to show course events.
group: boolean; // Filter to show group events. group: boolean; // Filter to show group events.

View File

@ -16,7 +16,6 @@ import { Injector, NgModule } from '@angular/core';
import { Route, RouterModule, ROUTES, Routes } from '@angular/router'; import { Route, RouterModule, ROUTES, Routes } from '@angular/router';
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
import { AddonMessagesSettingsHandlerService } from './services/handlers/settings';
export const AddonMessagesDiscussionRoute: Route = { export const AddonMessagesDiscussionRoute: Route = {
path: 'discussion', path: 'discussion',
@ -46,11 +45,6 @@ function buildRoutes(injector: Injector): Routes {
loadChildren: () => import('./pages/search/search.module') loadChildren: () => import('./pages/search/search.module')
.then(m => m.AddonMessagesSearchPageModule), .then(m => m.AddonMessagesSearchPageModule),
}, },
{
path: AddonMessagesSettingsHandlerService.PAGE_NAME,
loadChildren: () => import('./pages/settings/settings.module')
.then(m => m.AddonMessagesSettingsPageModule),
},
{ {
path: 'contacts', // 3.6 or greater. path: 'contacts', // 3.6 or greater.
loadChildren: () => import('./pages/contacts/contacts.module') loadChildren: () => import('./pages/contacts/contacts.module')

View File

@ -22,7 +22,7 @@ import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-deleg
import { AddonMessagesMainMenuHandler, AddonMessagesMainMenuHandlerService } from './services/handlers/mainmenu'; import { AddonMessagesMainMenuHandler, AddonMessagesMainMenuHandlerService } from './services/handlers/mainmenu';
import { CoreCronDelegate } from '@services/cron'; import { CoreCronDelegate } from '@services/cron';
import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate'; 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 { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { AddonMessagesIndexLinkHandler } from './services/handlers/index-link'; import { AddonMessagesIndexLinkHandler } from './services/handlers/index-link';
@ -35,6 +35,7 @@ import { AddonMessagesSendMessageUserHandler } from './services/handlers/user-se
import { Network, NgZone } from '@singletons'; import { Network, NgZone } from '@singletons';
import { AddonMessagesSync } from './services/messages-sync'; import { AddonMessagesSync } from './services/messages-sync';
import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron'; import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron';
import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing';
const mainMenuChildrenRoutes: Routes = [ const mainMenuChildrenRoutes: Routes = [
{ {
@ -42,11 +43,18 @@ const mainMenuChildrenRoutes: Routes = [
loadChildren: () => import('./messages-lazy.module').then(m => m.AddonMessagesLazyModule), 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({ @NgModule({
imports: [ imports: [
CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }), CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }),
CoreMainMenuTabRoutingModule.forChild( mainMenuChildrenRoutes), CoreMainMenuTabRoutingModule.forChild( mainMenuChildrenRoutes),
CoreSitePreferencesRoutingModule.forChild(preferencesRoutes),
], ],
providers: [ providers: [
{ {

View File

@ -16,7 +16,6 @@ import { Injectable } from '@angular/core';
import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { AddonMessages } from '../messages'; import { AddonMessages } from '../messages';
import { AddonMessagesMainMenuHandlerService } from './mainmenu';
/** /**
* Message settings handler. * Message settings handler.
@ -24,7 +23,7 @@ import { AddonMessagesMainMenuHandlerService } from './mainmenu';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AddonMessagesSettingsHandlerService implements CoreSettingsHandler { export class AddonMessagesSettingsHandlerService implements CoreSettingsHandler {
static readonly PAGE_NAME = 'settings'; static readonly PAGE_NAME = 'messages';
name = 'AddonMessages'; name = 'AddonMessages';
priority = 600; priority = 600;
@ -49,7 +48,7 @@ export class AddonMessagesSettingsHandlerService implements CoreSettingsHandler
return { return {
icon: 'fas-comments', icon: 'fas-comments',
title: 'addon.messages.messages', title: 'addon.messages.messages',
page: AddonMessagesMainMenuHandlerService.PAGE_NAME + '/' + AddonMessagesSettingsHandlerService.PAGE_NAME, page: AddonMessagesSettingsHandlerService.PAGE_NAME,
class: 'addon-messages-settings-handler', class: 'addon-messages-settings-handler',
}; };
} }

View File

@ -24,7 +24,6 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreObject } from '@singletons/object';
import { import {
AddonModAssignAssign, AddonModAssignAssign,
AddonModAssignSubmission, AddonModAssignSubmission,
@ -368,9 +367,9 @@ class AddonModAssignSubmissionListManager extends CorePageItemsListManager<Addon
* @inheritdoc * @inheritdoc
*/ */
protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params { protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params {
return CoreObject.withoutEmpty({ return {
blindId: submission.blindid, blindId: submission.blindid,
}); };
} }
/** /**

View File

@ -16,7 +16,6 @@ import { Injector, NgModule } from '@angular/core';
import { RouterModule, ROUTES, Routes } from '@angular/router'; import { RouterModule, ROUTES, Routes } from '@angular/router';
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
import { AddonNotificationsSettingsHandlerService } from './services/handlers/settings';
function buildRoutes(injector: Injector): Routes { function buildRoutes(injector: Injector): Routes {
return [ return [
@ -24,10 +23,6 @@ function buildRoutes(injector: Injector): Routes {
path: 'list', path: 'list',
loadChildren: () => import('./pages/list/list.module').then(m => m.AddonNotificationsListPageModule), 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, { ...buildTabMainRoutes(injector, {
redirectTo: 'list', redirectTo: 'list',
pathMatch: 'full', pathMatch: 'full',

View File

@ -24,7 +24,8 @@ import { CoreSettingsDelegate } from '@features/settings/services/settings-deleg
import { AddonNotificationsMainMenuHandler, AddonNotificationsMainMenuHandlerService } from './services/handlers/mainmenu'; import { AddonNotificationsMainMenuHandler, AddonNotificationsMainMenuHandlerService } from './services/handlers/mainmenu';
import { AddonNotificationsCronHandler } from './services/handlers/cron'; import { AddonNotificationsCronHandler } from './services/handlers/cron';
import { AddonNotificationsPushClickHandler } from './services/handlers/push-click'; 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 = [ const routes: Routes = [
{ {
@ -32,11 +33,18 @@ const routes: Routes = [
loadChildren: () => import('@/addons/notifications/notifications-lazy.module').then(m => m.AddonNotificationsLazyModule), 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({ @NgModule({
imports: [ imports: [
CoreMainMenuRoutingModule.forChild({ children: routes }), CoreMainMenuRoutingModule.forChild({ children: routes }),
CoreMainMenuTabRoutingModule.forChild(routes), CoreMainMenuTabRoutingModule.forChild(routes),
CoreSitePreferencesRoutingModule.forChild(preferencesRoutes),
], ],
exports: [CoreMainMenuRoutingModule], exports: [CoreMainMenuRoutingModule],
providers: [ providers: [

View File

@ -38,7 +38,6 @@ import {
AddonNotificationsPreferencesProcessorFormatted, AddonNotificationsPreferencesProcessorFormatted,
} from '@addons/notifications/services/notifications-helper'; } from '@addons/notifications/services/notifications-helper';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
// import { CoreSplitViewComponent } from '@components/split-view/split-view';
/** /**
* Page that displays notifications settings. * Page that displays notifications settings.
@ -61,7 +60,7 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy {
protected updateTimeout?: number; protected updateTimeout?: number;
constructor() { // @todo @Optional() protected svComponent: CoreSplitViewComponent, constructor() {
this.notifPrefsEnabled = AddonNotifications.isNotificationPreferencesEnabled(); this.notifPrefsEnabled = AddonNotifications.isNotificationPreferencesEnabled();
this.canChangeSound = CoreLocalNotifications.canDisableSound(); this.canChangeSound = CoreLocalNotifications.canDisableSound();
} }
@ -196,8 +195,6 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy {
* @param handlerData * @param handlerData
*/ */
openExtraPreferences(handlerData: AddonMessageOutputHandlerData): void { 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 }); CoreNavigator.navigateToSitePath(handlerData.page, { params: handlerData.pageParams });
} }

View File

@ -18,7 +18,6 @@ import { CoreLocalNotifications } from '@services/local-notifications';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate';
import { AddonNotifications } from '../notifications'; import { AddonNotifications } from '../notifications';
import { AddonNotificationsMainMenuHandlerService } from './mainmenu';
/** /**
* Notifications settings handler. * Notifications settings handler.
@ -26,7 +25,7 @@ import { AddonNotificationsMainMenuHandlerService } from './mainmenu';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AddonNotificationsSettingsHandlerService implements CoreSettingsHandler { export class AddonNotificationsSettingsHandlerService implements CoreSettingsHandler {
static readonly PAGE_NAME = 'settings'; static readonly PAGE_NAME = 'notifications';
name = 'AddonNotifications'; name = 'AddonNotifications';
priority = 500; priority = 500;
@ -50,7 +49,7 @@ export class AddonNotificationsSettingsHandlerService implements CoreSettingsHan
return { return {
icon: 'fas-bell', icon: 'fas-bell',
title: 'addon.notifications.notifications', title: 'addon.notifications.notifications',
page: AddonNotificationsMainMenuHandlerService.PAGE_NAME + '/' + AddonNotificationsSettingsHandlerService.PAGE_NAME, page: AddonNotificationsSettingsHandlerService.PAGE_NAME,
class: 'addon-notifications-settings-handler', class: 'addon-notifications-settings-handler',
}; };
} }

View File

@ -12,7 +12,7 @@
<span [core-mark-required]="required">{{ field.name }}</span> <span [core-mark-required]="required">{{ field.name }}</span>
</ion-label> </ion-label>
<ion-datetime [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="format" <ion-datetime [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="format"
[max]="max" [min]="min"> [max]="max" [min]="min" [monthNames]="monthNames">
</ion-datetime> </ion-datetime>
<core-input-errors [control]="form.controls[modelName]"></core-input-errors> <core-input-errors [control]="form.controls[modelName]"></core-input-errors>
</ion-item> </ion-item>

View File

@ -21,6 +21,7 @@ import { AuthEmailSignupProfileField } from '@features/login/services/login-help
import { CoreUserProfileField } from '@features/user/services/user'; import { CoreUserProfileField } from '@features/user/services/user';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component'; import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
import { CoreLang } from '@services/lang';
/** /**
* Directive to render a datetime user profile field. * Directive to render a datetime user profile field.
@ -35,6 +36,7 @@ export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileField
min?: number; min?: number;
max?: number; max?: number;
valueNumber = 0; valueNumber = 0;
monthNames?: string[];
/** /**
* Init the data when the field is meant to be displayed without editing. * 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 { protected initForEdit(field: AuthEmailSignupProfileField): void {
super.initForEdit(field); super.initForEdit(field);
this.monthNames = CoreLang.getMonthNames();
// Check if it's only date or it has time too. // Check if it's only date or it has time too.
const hasTime = CoreUtils.isTrueOrOne(field.param3); const hasTime = CoreUtils.isTrueOrOne(field.param3);

View File

@ -115,6 +115,8 @@ export class AppComponent implements OnInit, AfterViewInit {
}); });
this.onPlatformReady(); this.onPlatformReady();
// @todo: Quit app with back button. How to tell if we're at root level?
} }
/** /**

View File

@ -25,9 +25,9 @@ import {
ElementRef, ElementRef,
} from '@angular/core'; } from '@angular/core';
import { IonSlides } from '@ionic/angular'; import { IonSlides } from '@ionic/angular';
import { BackButtonEvent } from '@ionic/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CoreApp } from '@services/app';
import { Platform, Translate } from '@singletons'; import { Platform, Translate } from '@singletons';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper'; 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 selectHistory: string[] = [];
protected firstSelectedTab?: string; // ID of the first selected tab to control history. protected firstSelectedTab?: string; // ID of the first selected tab to control history.
protected unregisterBackButtonAction: any; protected backButtonFunction: (event: BackButtonEvent) => void;
protected languageChangedSubscription?: Subscription; protected languageChangedSubscription?: Subscription;
protected isInTransition = false; // Weather Slides is in transition. protected isInTransition = false; // Weather Slides is in transition.
protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any 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( constructor(
protected element: ElementRef, 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.calculateSlides();
this.registerBackButtonAction(); document.addEventListener('ionBackButton', this.backButtonFunction);
} }
/** /**
* Register back button action. * Back button clicked.
*
* @param event Event.
*/ */
protected registerBackButtonAction(): void { protected backButtonClicked(event: BackButtonEvent): void {
this.unregisterBackButtonAction = CoreApp.registerBackButtonAction(() => { event.detail.register(40, (processNextHandler: () => void) => {
// The previous page in history is not the last one, we need the previous one.
if (this.selectHistory.length > 1) { 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. // 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; return;
} else if (this.selected != this.firstSelectedTab) { }
if (this.firstSelectedTab && this.selected != this.firstSelectedTab) {
// All history is gone but we are not in the first selected tab. // All history is gone but we are not in the first selected tab.
this.selectHistory = []; this.selectHistory = [];
this.selectTab(this.firstSelectedTab!); this.selectTab(this.firstSelectedTab);
return true; return;
} }
return false; processNextHandler();
}, 750); });
} }
/** /**
* User left the page that contains the component. * User left the page that contains the component.
*/ */
ionViewDidLeave(): void { ionViewDidLeave(): void {
// Unregister the custom back button action for this page // Unregister the custom back button action for this component.
this.unregisterBackButtonAction && this.unregisterBackButtonAction(); document.removeEventListener('ionBackButton', this.backButtonFunction);
this.isCurrentView = false; this.isCurrentView = false;
} }

View 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;
}

View 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>

View File

@ -51,6 +51,7 @@ import { CorePipesModule } from '@pipes/pipes.module';
import { CoreAttachmentsComponent } from './attachments/attachments'; import { CoreAttachmentsComponent } from './attachments/attachments';
import { CoreFilesComponent } from './files/files'; import { CoreFilesComponent } from './files/files';
import { CoreLocalFileComponent } from './local-file/local-file'; import { CoreLocalFileComponent } from './local-file/local-file';
import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -84,6 +85,7 @@ import { CoreLocalFileComponent } from './local-file/local-file';
CoreAttachmentsComponent, CoreAttachmentsComponent,
CoreFilesComponent, CoreFilesComponent,
CoreLocalFileComponent, CoreLocalFileComponent,
CoreBSTooltipComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -124,6 +126,7 @@ import { CoreLocalFileComponent } from './local-file/local-file';
CoreAttachmentsComponent, CoreAttachmentsComponent,
CoreFilesComponent, CoreFilesComponent,
CoreLocalFileComponent, CoreLocalFileComponent,
CoreBSTooltipComponent,
], ],
}) })
export class CoreComponentsModule {} export class CoreComponentsModule {}

View File

@ -1,5 +1,6 @@
ion-app.app-root core-iframe { @import "~theme/globals";
:host {
> div { > div {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
@ -8,7 +9,7 @@ ion-app.app-root core-iframe {
border: 0; border: 0;
display: block; display: block;
max-width: 100%; max-width: 100%;
background-color: $gray-light; background-color: var(--gray-light);
} }
.core-loading-container { .core-loading-container {

View File

@ -27,6 +27,7 @@ import { CoreLogger } from '@singletons/logger';
@Component({ @Component({
selector: 'core-iframe', selector: 'core-iframe',
templateUrl: 'core-iframe.html', templateUrl: 'core-iframe.html',
styleUrls: ['iframe.scss'],
}) })
export class CoreIframeComponent implements OnChanges { export class CoreIframeComponent implements OnChanges {
@ -74,8 +75,6 @@ export class CoreIframeComponent implements OnChanges {
// Show loading only with external URLs. // Show loading only with external URLs.
this.loading = !this.src || !CoreUrlUtils.isLocalFileUrl(this.src); 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); CoreIframeUtils.treatFrame(iframe, false);
iframe.addEventListener('load', () => { iframe.addEventListener('load', () => {

View File

@ -132,9 +132,9 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
* Get the height of the element. * Get the height of the element.
* *
* @return Height. * @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 * be erased, also with getElementHeight
* @deprecated * @deprecated since 3.9.5
*/ */
getHeight(): number { getHeight(): number {
// return this.element.nativeElement.getBoundingClientRect().height; // return this.element.nativeElement.getBoundingClientRect().height;

View File

@ -124,7 +124,6 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
* If both button containers have a context menu, merge them into a single one. * If both button containers have a context menu, merge them into a single one.
* *
* @param buttonsContainer The container where the buttons will be moved. * @param buttonsContainer The container where the buttons will be moved.
* @todo
*/ */
protected mergeContextMenus(buttonsContainer: HTMLElement): void { protected mergeContextMenus(buttonsContainer: HTMLElement): void {
// Check if both button containers have a context menu. // Check if both button containers have a context menu.

View File

@ -137,7 +137,6 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); 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', { CoreNavigator.navigateToSitePath('user', {
params: { params: {
userId: this.userId, userId: this.userId,

View File

@ -208,8 +208,8 @@ export class CoreFormatTextDirective implements OnChanges {
anchor.classList.add('core-image-viewer-icon'); anchor.classList.add('core-image-viewer-icon');
anchor.setAttribute('aria-label', label); 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. // 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>'; anchor.innerHTML = '<ion-icon name="fas-search" src="assets/fonts/font-awesome/solid/search.svg"></ion-icon>';
anchor.addEventListener('click', (e: Event) => { anchor.addEventListener('click', (e: Event) => {
e.preventDefault(); e.preventDefault();
@ -471,8 +471,6 @@ export class CoreFormatTextDirective implements OnChanges {
*/ */
protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> { protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> {
const canTreatVimeo = site?.isVersionGreaterEqualThan(['3.3.4', '3.4']) || false; 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 images = Array.from(div.querySelectorAll('img'));
const anchors = Array.from(div.querySelectorAll('a')); const anchors = Array.from(div.querySelectorAll('a'));

View File

@ -56,8 +56,6 @@ export class CoreLinkDirective implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.inApp = typeof this.inApp == 'undefined' ? this.inApp : CoreUtils.isTrueOrOne(this.inApp); this.inApp = typeof this.inApp == 'undefined' ? this.inApp : CoreUtils.isTrueOrOne(this.inApp);
// @todo: Handle split view?
this.element.addEventListener('click', async (event) => { this.element.addEventListener('click', async (event) => {
if (event.defaultPrevented) { if (event.defaultPrevented) {
return; // Link already treated, stop. return; // Link already treated, stop.

View File

@ -15,8 +15,6 @@
import { Directive, Input, OnInit, ElementRef } from '@angular/core'; import { Directive, Input, OnInit, ElementRef } from '@angular/core';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreObject } from '@singletons/object';
/** /**
* Directive to go to user profile on click. * Directive to go to user profile on click.
*/ */
@ -49,12 +47,11 @@ export class CoreUserLinkDirective implements OnInit {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
// @todo If this directive is inside a split view, use the split view's master nav.
CoreNavigator.navigateToSitePath('user', { CoreNavigator.navigateToSitePath('user', {
params: CoreObject.withoutEmpty({ params: {
userId: this.userId, userId: this.userId,
courseId: this.courseId, courseId: this.courseId,
}), },
}); });
}); });
} }

View File

@ -43,7 +43,6 @@ export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implemen
* Go to the block page. * Go to the block page.
*/ */
gotoBlock(): void { gotoBlock(): void {
// @todo test that this is working properly.
CoreNavigator.navigateToSitePath(this.link!, { params: this.linkParams }); CoreNavigator.navigateToSitePath(this.link!, { params: this.linkParams });
} }

View File

@ -22,10 +22,10 @@
</core-course-module-completion> </core-course-module-completion>
<div class="core-module-buttons-more"> <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" [canTrustDownload]="canCheckUpdates" [loading]="spinner || module.handlerData.spinner"
(action)="download($event)"> (action)="download($event)">
</core-download-refresh> --> </core-download-refresh>
<!-- Buttons defined by the module handler. --> <!-- Buttons defined by the module handler. -->
<ion-button fill="clear" *ngFor="let button of module.handlerData.buttons" color="dark" <ion-button fill="clear" *ngFor="let button of module.handlerData.buttons" color="dark"

View File

@ -34,8 +34,6 @@ export class CoreCourseTagAreaComponent {
* @param courseId The course to open. * @param courseId The course to open.
*/ */
openCourse(courseId: number): void { 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); CoreCourseHelper.getAndOpenCourse(courseId);
} }

View File

@ -16,14 +16,14 @@ import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
// import { CoreUser } from '@core/user/services/user'; import { CoreCourses, CoreCoursesMyCoursesUpdatedEventData, CoreCoursesProvider } from '@features/courses/services/courses';
import { CoreCourses } from '@features/courses/services/courses';
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
import { PopoverController, Translate } from '@singletons'; import { PopoverController, Translate } from '@singletons';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper'; import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu'; 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. * 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. * Show the context menu.
* *
* @param e Click Event. * @param e Click Event.
* @todo
*/ */
async showCourseOptionsMenu(e: Event): Promise<void> { async showCourseOptionsMenu(e: Event): Promise<void> {
e.preventDefault(); e.preventDefault();
@ -247,22 +246,62 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy {
* Hide/Unhide the course from the course list. * Hide/Unhide the course from the course list.
* *
* @param hide True to hide and false to show. * @param hide True to hide and false to show.
* @todo CoreUser
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars protected async setCourseHidden(hide: boolean): Promise<void> {
protected setCourseHidden(hide: boolean): void { this.showSpinner = true;
return;
// 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. * Favourite/Unfavourite the course from the course list.
* *
* @param favourite True to favourite and false to unfavourite. * @param favourite True to favourite and false to unfavourite.
* @todo CoreUser
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars protected async setCourseFavourite(favourite: boolean): Promise<void> {
protected setCourseFavourite(favourite: boolean): void { this.showSpinner = true;
return;
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;
}
} }
/** /**

View File

@ -450,11 +450,11 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy {
* Prefetch the course. * Prefetch the course.
*/ */
prefetchCourse(): void { prefetchCourse(): void {
/* @todo CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => { CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!).catch((error) => {
if (!this.pageDestroyed) { if (!this.pageDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
} }
});*/ });
} }
/** /**

View File

@ -22,13 +22,11 @@ import {
CoreGradesFormattedTable, CoreGradesFormattedTable,
CoreGradesFormattedTableColumn, CoreGradesFormattedTableColumn,
CoreGradesFormattedTableRow, CoreGradesFormattedTableRow,
CoreGradesFormattedTableRowFilled,
CoreGradesHelper, CoreGradesHelper,
} from '@features/grades/services/grades-helper'; } from '@features/grades/services/grades-helper';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreSplitViewComponent, CoreSplitViewMode } from '@components/split-view/split-view'; import { CoreSplitViewComponent, CoreSplitViewMode } from '@components/split-view/split-view';
import { CoreObject } from '@singletons/object';
import { CorePageItemsListManager } from '@classes/page-items-list-manager'; import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
@ -175,7 +173,7 @@ class CoreGradesCourseManager extends CorePageItemsListManager<CoreGradesFormatt
* @inheritdoc * @inheritdoc
*/ */
protected getItemQueryParams(): Params { 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;
};

View File

@ -23,6 +23,8 @@ import {
CoreGradesGradeItem, CoreGradesGradeItem,
CoreGradesGradeOverview, CoreGradesGradeOverview,
CoreGradesTable, CoreGradesTable,
CoreGradesTableColumn,
CoreGradesTableItemNameColumn,
CoreGradesTableRow, CoreGradesTableRow,
} from '@features/grades/services/grades'; } from '@features/grades/services/grades';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
@ -56,14 +58,19 @@ export class CoreGradesHelperProvider {
rowclass: '', rowclass: '',
}; };
for (const name in tableRow) { for (const name in tableRow) {
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) { const column: CoreGradesTableColumn = tableRow[name];
let content = String(tableRow[name].content);
if (column.content === undefined || column.content === null) {
continue;
}
let content = String(column.content);
if (name == 'itemname') { if (name == 'itemname') {
this.setRowIcon(row, content); this.setRowIcon(row, content);
row.link = this.getModuleLink(content); row.link = this.getModuleLink(content);
row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; row.rowclass += column.class.indexOf('hidden') >= 0 ? ' hidden' : '';
row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; row.rowclass += column.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
content = content.replace(/<\/span>/gi, '\n'); content = content.replace(/<\/span>/gi, '\n');
content = CoreTextUtils.cleanTags(content); content = CoreTextUtils.cleanTags(content);
@ -77,7 +84,6 @@ export class CoreGradesHelperProvider {
row[name] = content.trim(); row[name] = content.trim();
} }
}
return row; return row;
} }
@ -88,21 +94,28 @@ export class CoreGradesHelperProvider {
* @param tableRow JSON object representing row of grades table data. * @param tableRow JSON object representing row of grades table data.
* @return Formatted row object. * @return Formatted row object.
*/ */
protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable { protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedTableRow {
const row: CoreGradesFormattedRowForTable = {}; const row: CoreGradesFormattedTableRow = {};
for (let name in tableRow) { for (let name in tableRow) {
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) { const column: CoreGradesTableColumn = tableRow[name];
let content = String(tableRow[name].content);
if (column.content === undefined || column.content === null) {
continue;
}
let content = String(column.content);
if (name == 'itemname') { if (name == 'itemname') {
row.id = parseInt(tableRow[name]!.id.split('_')[1], 10); const itemNameColumn = <CoreGradesTableItemNameColumn> column;
row.colspan = tableRow[name]!.colspan;
row.rowspan = (tableRow.leader && tableRow.leader.rowspan) || 1; row.id = parseInt(itemNameColumn.id.split('_')[1], 10);
row.colspan = itemNameColumn.colspan;
row.rowspan = tableRow.leader?.rowspan || 1;
this.setRowIcon(row, content); this.setRowIcon(row, content);
row.rowclass = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even'; row.rowclass = itemNameColumn.class.indexOf('leveleven') < 0 ? 'odd' : 'even';
row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; row.rowclass += itemNameColumn.class.indexOf('hidden') >= 0 ? ' hidden' : '';
row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; row.rowclass += itemNameColumn.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
content = content.replace(/<\/span>/gi, '\n'); content = content.replace(/<\/span>/gi, '\n');
content = CoreTextUtils.cleanTags(content); content = CoreTextUtils.cleanTags(content);
@ -117,7 +130,6 @@ export class CoreGradesHelperProvider {
row[name] = content.trim(); row[name] = content.trim();
} }
}
return row; return row;
} }
@ -147,9 +159,9 @@ export class CoreGradesHelperProvider {
*/ */
formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable { formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable {
const maxDepth = table.maxdepth; const maxDepth = table.maxdepth;
const formatted = { const formatted: CoreGradesFormattedTable = {
columns: [] as any[], columns: [],
rows: [] as any[], rows: [],
}; };
// Columns, in order. // Columns, in order.
@ -185,7 +197,7 @@ export class CoreGradesHelperProvider {
} }
for (const colName in columns) { for (const colName in columns) {
if (typeof normalRow[colName] != 'undefined') { if (normalRow && typeof normalRow[colName] != 'undefined') {
formatted.columns.push({ formatted.columns.push({
name: colName, name: colName,
colspan: colName == 'gradeitem' ? maxDepth : 1, colspan: colName == 'gradeitem' ? maxDepth : 1,
@ -561,10 +573,7 @@ export class CoreGradesHelperProvider {
* @param text HTML where the image will be rendered. * @param text HTML where the image will be rendered.
* @return Row object with the image. * @return Row object with the image.
*/ */
protected setRowIcon( protected setRowIcon<T extends CoreGradesFormattedRowCommonData>(row: T, text: string): T {
row: CoreGradesFormattedRowForTable | CoreGradesFormattedRow,
text: string,
): CoreGradesFormattedRowForTable {
text = text.replace('%2F', '/').replace('%2f', '/'); text = text.replace('%2F', '/').replace('%2f', '/');
if (text.indexOf('/agg_mean') > -1) { if (text.indexOf('/agg_mean') > -1) {
@ -683,10 +692,6 @@ export class CoreGradesHelperProvider {
export const CoreGradesHelper = makeSingleton(CoreGradesHelperProvider); export const CoreGradesHelper = makeSingleton(CoreGradesHelperProvider);
// @todo formatted data types.
export type CoreGradesFormattedRowForTable = any;
export type CoreGradesFormattedTableColumn = any;
export type CoreGradesFormattedItem = CoreGradesGradeItem & { export type CoreGradesFormattedItem = CoreGradesGradeItem & {
weight?: string; // Weight. weight?: string; // Weight.
grade?: string; // The grade formatted. grade?: string; // The grade formatted.
@ -696,15 +701,13 @@ export type CoreGradesFormattedItem = CoreGradesGradeItem & {
average?: string; // Grade average. average?: string; // Grade average.
}; };
export type CoreGradesFormattedRow = { export type CoreGradesFormattedRowCommonData = {
icon?: string; icon?: string;
link?: string | false;
rowclass?: string; rowclass?: string;
itemtype?: string; itemtype?: string;
image?: string; image?: string;
itemmodule?: string; itemmodule?: string;
rowspan?: number; rowspan?: number;
itemname?: string; // The item returned data.
weight?: string; // Weight column. weight?: string; // Weight column.
grade?: string; // Grade column. grade?: string; // Grade column.
range?: string;// Range column. range?: string;// Range column.
@ -716,20 +719,26 @@ export type CoreGradesFormattedRow = {
contributiontocoursetotal?: string; // Contributiontocoursetotal column. 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 = { export type CoreGradesFormattedTable = {
columns: CoreGradesFormattedTableColumn[]; columns: CoreGradesFormattedTableColumn[];
rows: CoreGradesFormattedTableRow[]; rows: CoreGradesFormattedTableRow[];
}; };
export type CoreGradesFormattedTableRowFilled = {
// @todo complete types. export type CoreGradesFormattedTableRow = CoreGradesFormattedRowCommonData & {
id: number; id?: number;
itemtype: 'category' | 'leader'; colspan?: number;
grade: unknown; gradeitem?: string; // The item returned data.
percentage: unknown;
}; };
type CoreGradesFormattedTableRowEmpty ={
// export type CoreGradesFormattedTableColumn = {
name: string;
colspan: number;
hiddenPhone: boolean;
}; };
/** /**

View File

@ -515,64 +515,53 @@ export type CoreGradesTable = {
* Grade table data item. * Grade table data item.
*/ */
export type CoreGradesTableRow = { 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. class: string; // Class.
colspan: number; // Col span. colspan: number; // Col span.
content: string; // Cell content. content: string; // Cell content.
celltype: string; // Cell type. celltype: string; // Cell type.
id: string; // Id. id: string; // Id.
}; // The item returned data. };
leader?: {
/**
* Grade table leader column.
*/
export type CoreGradesTableLeaderColumn = {
class: string; // Class. class: string; // Class.
rowspan: number; // Row span. rowspan: number; // Row span.
}; // The item returned data. content: undefined; // The WS doesn't return this data, but we declare it to make it coherent with the other columns.
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.
}; };
/**
* Grade table column.
*/
export type CoreGradesTableColumn = CoreGradesTableCommonColumn | CoreGradesTableItemNameColumn | CoreGradesTableLeaderColumn;
/** /**
* Grade overview data. * Grade overview data.
*/ */

View File

@ -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 LIBRARIES_CACHEDASSETS_TABLE_NAME = 'h5p_libraries_cachedassets'; // H5P cached library assets.
export const SITE_SCHEMA: CoreSiteSchema = { export const SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreH5PProvider', name: 'CoreH5PProvider',
version: 1, version: 2,
canBeCleared: [ canBeCleared: [
CONTENT_TABLE_NAME, CONTENT_TABLE_NAME,
LIBRARIES_TABLE_NAME, LIBRARIES_TABLE_NAME,

View File

@ -20,6 +20,8 @@ import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreNavigator } from '@services/navigator'; 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. * 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[]); const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]);
// Remove protocol from the url to show more url text. // 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.siteUrl = site.siteUrl.replace(/^https?:\/\//, '');
site.badge = 0; site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0;
// @todo: getSiteCounter.
return site; return site;
}); }));
this.showDelete = false; this.showDelete = false;
} }
@ -76,9 +77,9 @@ export class CoreLoginSitesPage implements OnInit {
async deleteSite(e: Event, site: CoreSiteBasicInfo): Promise<void> { async deleteSite(e: Event, site: CoreSiteBasicInfo): Promise<void> {
e.stopPropagation(); 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 { try {
await CoreDomUtils.showDeleteConfirm('core.login.confirmdeletesite', { sitename: siteName }); await CoreDomUtils.showDeleteConfirm('core.login.confirmdeletesite', { sitename: siteName });

View File

@ -33,7 +33,6 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreObject } from '@singletons/object';
/** /**
* Helper provider that provides some common features regarding authentication. * Helper provider that provides some common features regarding authentication.
@ -428,7 +427,7 @@ export class CoreLoginHelperProvider {
return ['/login/credentials', { siteUrl: url }]; return ['/login/credentials', { siteUrl: url }];
} }
return ['/login/site', CoreObject.withoutEmpty({ showKeyboard: showKeyboard })]; return ['/login/site', { showKeyboard }];
} }
/** /**

View File

@ -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-tab-bar slot="bottom" [hidden]="hidden">
<ion-spinner *ngIf="!loaded"></ion-spinner> <ion-spinner *ngIf="!loaded"></ion-spinner>

View File

@ -15,6 +15,7 @@
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { IonTabs } from '@ionic/angular'; import { IonTabs } from '@ionic/angular';
import { BackButtonEvent } from '@ionic/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
@ -50,6 +51,11 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
protected pendingRedirect?: CoreRedirectPayload; protected pendingRedirect?: CoreRedirectPayload;
protected urlToOpen?: string; protected urlToOpen?: string;
protected keyboardObserver?: CoreEventObserver; protected keyboardObserver?: CoreEventObserver;
protected resizeFunction: () => void;
protected backButtonFunction: (event: BackButtonEvent) => void;
protected selectHistory: string[] = [];
protected selectedTab?: string;
protected firstSelectedTab?: string;
@ViewChild('mainTabs') mainTabs?: IonTabs; @ViewChild('mainTabs') mainTabs?: IonTabs;
@ -57,7 +63,10 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected changeDetector: ChangeDetectorRef, protected changeDetector: ChangeDetectorRef,
protected router: Router, protected router: Router,
) {} ) {
this.resizeFunction = this.initHandlers.bind(this);
this.backButtonFunction = this.backButtonClicked.bind(this);
}
/** /**
* Initialize the component. * 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()) { if (CoreApp.isIOS()) {
// In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done. // 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 { ngOnDestroy(): void {
this.subscription?.unsubscribe(); this.subscription?.unsubscribe();
this.redirectObs?.off(); this.redirectObs?.off();
window.removeEventListener('resize', this.initHandlers.bind(this)); window.removeEventListener('resize', this.resizeFunction);
document.removeEventListener('ionBackButton', this.backButtonFunction);
this.keyboardObserver?.off(); 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();
});
}
} }

View File

@ -146,10 +146,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
* @param item Item to open. * @param item Item to open.
*/ */
openItem(item: CoreMainMenuCustomItem): void { openItem(item: CoreMainMenuCustomItem): void {
// @todo CoreNavigator.navigateToSitePath('CoreViewerIframePage', {title: item.label, url: item.url}); CoreNavigator.navigateToSitePath('viewer/iframe', { params: { title: item.label, url: item.url } });
// eslint-disable-next-line no-console
console.error('openItem not implemented', item);
} }
/** /**

View File

@ -819,7 +819,6 @@ export class CoreQuestionHelperProvider {
if (span.innerHTML) { if (span.innerHTML) {
// There's a hidden feedback. Mark the icon as tappable. // There's a hidden feedback. Mark the icon as tappable.
// The click listener is only added if treatCorrectnessIconsClicks is called. // 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', ''); icon.setAttribute('tappable', '');
} }
}); });
@ -843,9 +842,7 @@ export class CoreQuestionHelperProvider {
contextInstanceId?: number, contextInstanceId?: number,
courseId?: number, courseId?: number,
): void { ): void {
const icons = <HTMLElement[]> Array.from(element.querySelectorAll('ion-icon.questioncorrectnessicon[tappable]'));
// @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 title = Translate.instant('core.question.feedback'); const title = Translate.instant('core.question.feedback');
icons.forEach((icon) => { icons.forEach((icon) => {

View File

@ -153,8 +153,6 @@ export class CoreSettingsGeneralPage {
/** /**
* Called when the analytics setting is enabled or disabled. * Called when the analytics setting is enabled or disabled.
*
* @todo
*/ */
async analyticsEnabledChanged(): Promise<void> { async analyticsEnabledChanged(): Promise<void> {
await CorePushNotifications.enableAnalytics(this.analyticsEnabled); await CorePushNotifications.enableAnalytics(this.analyticsEnabled);

View 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 },
],
};
}
}

View File

@ -9,10 +9,11 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <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-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="handlers.loaded">
<ion-list> <ion-list>
<ion-item *ngIf="siteInfo" class="ion-text-wrap"> <ion-item *ngIf="siteInfo" class="ion-text-wrap">
<ion-label> <ion-label>
@ -25,7 +26,7 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item-divider><ion-label></ion-label></ion-item-divider> <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})" (click)="openHandler('CoreSharedFilesListPage', {manage: true, siteId: siteId, hideSitePicker: true})"
[title]="'core.sharedfiles.sharedfiles' | translate" [title]="'core.sharedfiles.sharedfiles' | translate"
[class.core-selected-item]="'CoreSharedFilesListPage' == selectedPage" detail> [class.core-selected-item]="'CoreSharedFilesListPage' == selectedPage" detail>
@ -34,11 +35,11 @@
<h2>{{ 'core.sharedfiles.sharedfiles' | translate }}</h2> <h2>{{ 'core.sharedfiles.sharedfiles' | translate }}</h2>
</ion-label> </ion-label>
<ion-badge slot="end">{{ iosSharedFiles }}</ion-badge> <ion-badge slot="end">{{ iosSharedFiles }}</ion-badge>
</ion-item> </ion-item> -->
<ion-item *ngFor="let handler of handlers" [ngClass]="['core-settings-handler', handler.class]" <ion-item *ngFor="let handler of handlers.items" [ngClass]="['core-settings-handler', handler.class]"
(click)="openHandler(handler.page, handler.params)" [title]="handler.title | translate" detail [title]="handler.title | translate" detail="true" (click)="handlers.select(handler)"
[class.core-selected-item]="handler.page == selectedPage"> [class.core-selected-item]="handlers.isSelected(handler)">
<ion-icon [name]="handler.icon" slot="start" *ngIf="handler.icon"> <ion-icon [name]="handler.icon" slot="start" *ngIf="handler.icon">
</ion-icon> </ion-icon>
<ion-label> <ion-label>
@ -77,4 +78,5 @@
</ion-card> </ion-card>
</ion-list> </ion-list>
</core-loading> </core-loading>
</core-split-view>
</ion-content> </ion-content>

View File

@ -12,26 +12,51 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { Injector, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, ROUTES, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreSitePreferencesPage } from './site'; 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: '', path: '',
component: CoreSitePreferencesPage, component: CoreSitePreferencesPage,
}, },
...routes.siblings,
]; ];
const tabletRoutes: Routes = [
{
path: '',
component: CoreSitePreferencesPage,
children: routes.siblings,
},
];
return [
...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
];
}
@NgModule({ @NgModule({
providers: [
{ provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] },
],
declarations: [ declarations: [
CoreSitePreferencesPage, CoreSitePreferencesPage,
], ],
imports: [ imports: [
RouterModule.forChild(routes),
CoreSharedModule, CoreSharedModule,
], ],
exports: [RouterModule],
}) })
export class CoreSitePreferencesPageModule {} export class CoreSitePreferencesPageModule {}

View File

@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnDestroy, OnInit } from '@angular/core'; import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { Params } from '@angular/router'; import { ActivatedRouteSnapshot, Params } from '@angular/router';
import { IonRefresher } from '@ionic/angular'; 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 { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@singletons/events';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
@ -26,7 +26,8 @@ import { CoreApp } from '@services/app';
import { CoreSiteInfo } from '@classes/site'; import { CoreSiteInfo } from '@classes/site';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator'; 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. * Page that displays the list of site settings pages.
@ -35,12 +36,13 @@ import { CoreScreen } from '@services/screen';
selector: 'page-core-site-preferences', selector: 'page-core-site-preferences',
templateUrl: 'site.html', templateUrl: 'site.html',
}) })
export class CoreSitePreferencesPage implements OnInit, OnDestroy { export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
handlers: CoreSettingsSitePreferencesManager;
isIOS: boolean; isIOS: boolean;
selectedPage?: string;
handlers: CoreSettingsHandlerData[] = [];
siteId: string; siteId: string;
siteInfo?: CoreSiteInfo; siteInfo?: CoreSiteInfo;
siteName?: string; siteName?: string;
@ -50,7 +52,6 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy {
spaceUsage: 0, spaceUsage: 0,
}; };
loaded = false;
iosSharedFiles = 0; iosSharedFiles = 0;
protected sitesObserver: CoreEventObserver; protected sitesObserver: CoreEventObserver;
protected isDestroyed = false; protected isDestroyed = false;
@ -59,6 +60,7 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy {
this.isIOS = CoreApp.isIOS(); this.isIOS = CoreApp.isIOS();
this.siteId = CoreSites.getCurrentSiteId(); this.siteId = CoreSites.getCurrentSiteId();
this.handlers = new CoreSettingsSitePreferencesManager(CoreSitePreferencesPage);
this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, (data: CoreEventSiteUpdatedData) => { this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, (data: CoreEventSiteUpdatedData) => {
if (data.siteId == this.siteId) { if (data.siteId == this.siteId) {
@ -70,30 +72,29 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy {
/** /**
* View loaded. * View loaded.
*/ */
ngOnInit(): void { async ngAfterViewInit(): Promise<void> {
// @todo this.selectedPage = route.snapshot.paramMap.get('page') || undefined; const pageToOpen = CoreNavigator.getRouteParam('page');
this.fetchData().finally(() => { try {
this.loaded = true; await this.fetchData();
} finally {
if (this.selectedPage) { const handler = pageToOpen ? this.handlers.items.find(handler => handler.page == pageToOpen) : undefined;
this.openHandler(this.selectedPage);
} else if (CoreScreen.isTablet) { if (handler) {
if (this.isIOS) { this.handlers.select(handler);
// @todo this.handlers.watchSplitViewOutlet(this.splitView);
// this.openHandler('CoreSharedFilesListPage', { manage: true, siteId: this.siteId, hideSitePicker: true }); } else {
} else if (this.handlers.length > 0) { this.handlers.start(this.splitView);
this.openHandler(this.handlers[0].page, this.handlers[0].params);
} }
} }
});
} }
/** /**
* Fetch Data. * Fetch Data.
*/ */
protected async fetchData(): Promise<void> { protected async fetchData(): Promise<void> {
this.handlers = CoreSettingsDelegate.getHandlers(); this.handlers.setItems(CoreSettingsDelegate.getHandlers());
const currentSite = CoreSites.getCurrentSite(); const currentSite = CoreSites.getCurrentSite();
this.siteInfo = currentSite!.getInfo(); 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. * 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;
}
}

View File

@ -40,7 +40,6 @@ export class CoreTagListComponent {
fromContextId: tag.taginstancecontextid, fromContextId: tag.taginstancecontextid,
}; };
// @todo: Check split view to navigate on the outlet if any.
CoreNavigator.navigateToSitePath('/tag/index', { params, preferCurrentTab: false }); CoreNavigator.navigateToSitePath('/tag/index', { params, preferCurrentTab: false });
} }

View File

@ -37,7 +37,6 @@ export interface CoreTagAreaHandler extends CoreDelegateHandler {
* Get the component to use to display items. * Get the component to use to display items.
* *
* @return The component (or promise resolved with component) to use, undefined if not found. * @return The component (or promise resolved with component) to use, undefined if not found.
* @todo, check return types.
*/ */
getComponent(): Type<unknown> | Promise<Type<unknown>>; getComponent(): Type<unknown> | Promise<Type<unknown>>;
} }
@ -85,7 +84,6 @@ export class CoreTagAreaDelegateService extends CoreDelegate<CoreTagAreaHandler>
* @param component Component name. * @param component Component name.
* @param itemType Item type. * @param itemType Item type.
* @return The component (or promise resolved with component) to use, undefined if not found. * @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> { async getComponent(component: string, itemType: string): Promise<Type<unknown> | undefined> {
const type = component + '/' + itemType; const type = component + '/' + itemType;

View File

@ -35,6 +35,7 @@ import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuplo
import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreCourses } from '@features/courses/services/courses';
@Component({ @Component({
selector: 'page-core-user-profile', selector: 'page-core-user-profile',
@ -239,8 +240,8 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
async refreshUser(event?: CustomEvent<IonRefresher>): Promise<void> { async refreshUser(event?: CustomEvent<IonRefresher>): Promise<void> {
await CoreUtils.ignoreErrors(Promise.all([ await CoreUtils.ignoreErrors(Promise.all([
CoreUser.invalidateUserCache(this.userId), CoreUser.invalidateUserCache(this.userId),
// @todo this.coursesProvider.invalidateUserNavigationOptions(), CoreCourses.invalidateUserNavigationOptions(),
// this.coursesProvider.invalidateUserAdministrationOptions() CoreCourses.invalidateUserAdministrationOptions(),
])); ]));
await this.fetchUser(); await this.fetchUser();
@ -260,8 +261,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
* Open the page with the user details. * Open the page with the user details.
*/ */
openUserDetails(): void { openUserDetails(): void {
// @todo: Navigate out of split view if this page is in the right pane. CoreNavigator.navigateToSitePath('user/about', {
CoreNavigator.navigate('../about', {
params: { params: {
courseId: this.courseId, courseId: this.courseId,
userId: this.userId, userId: this.userId,
@ -276,7 +276,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
* @param handler Handler that was clicked. * @param handler Handler that was clicked.
*/ */
handlerClicked(event: Event, handler: CoreUserProfileHandlerData): void { 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); handler.action(event, this.user!, this.courseId);
} }

View File

@ -20,7 +20,8 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreUserProfile } from './user'; import { CoreUserProfile } from './user';
import { makeSingleton } from '@singletons'; 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. * Interface that all user profile handlers must implement.
@ -241,9 +242,22 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async calculateUserHandlers(user: CoreUserProfile, courseId?: number): Promise<void> { protected async calculateUserHandlers(user: CoreUserProfile, courseId?: number): Promise<void> {
// @todo: Get Course admin/nav options. let navOptions: CoreCourseUserAdminOrNavOptionIndexed | undefined;
let navOptions; let admOptions: CoreCourseUserAdminOrNavOptionIndexed | undefined;
let admOptions;
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]; const userData = this.userHandlers[user.id];
userData.handlers = []; userData.handlers = [];

View File

@ -759,7 +759,7 @@ export class CoreUserProvider {
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved if success. * @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 = [ const preferences = [
{ {
type: name, type: name,
@ -780,7 +780,7 @@ export class CoreUserProvider {
* @return Promise resolved if success. * @return Promise resolved if success.
*/ */
async updateUserPreferences( async updateUserPreferences(
preferences: { type: string; value: string }[], preferences: { type: string; value: string | undefined }[],
disableNotifications?: boolean, disableNotifications?: boolean,
userId?: number, userId?: number,
siteId?: string, siteId?: string,

View File

@ -15,16 +15,19 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreViewerImageComponent } from './image/image';
import { CoreViewerTextComponent } from './text/text'; import { CoreViewerTextComponent } from './text/text';
@NgModule({ @NgModule({
declarations: [ declarations: [
CoreViewerImageComponent,
CoreViewerTextComponent, CoreViewerTextComponent,
], ],
imports: [ imports: [
CoreSharedModule, CoreSharedModule,
], ],
exports: [ exports: [
CoreViewerImageComponent,
CoreViewerTextComponent, CoreViewerTextComponent,
], ],
}) })

View 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>

View File

@ -0,0 +1,9 @@
:host {
.core-zoom-pane {
height: 100%;
img {
max-width: none;
}
}
}

View 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();
}
}

View 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>

View 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 {}

View 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;
}
}
}

View 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 {}

View File

@ -13,11 +13,21 @@
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; 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'; import { CoreViewerComponentsModule } from './components/components.module';
const routes: Routes = [
{
path: 'viewer',
loadChildren: () => import('./viewer-lazy.module').then(m => m.CoreViewerLazyModule),
},
];
@NgModule({ @NgModule({
imports: [ imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
CoreViewerComponentsModule, CoreViewerComponentsModule,
], ],
}) })

View File

@ -17,7 +17,6 @@ import { CanActivate, CanLoad, UrlTree } from '@angular/router';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { Router } from '@singletons'; import { Router } from '@singletons';
import { CoreObject } from '@singletons/object';
import { CoreConstants } from '../constants'; import { CoreConstants } from '../constants';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@ -62,10 +61,10 @@ export class CoreRedirectGuard implements CanLoad, CanActivate {
); );
const route = Router.parseUrl('/main'); const route = Router.parseUrl('/main');
route.queryParams = CoreObject.withoutEmpty({ route.queryParams = {
redirectPath: redirect.page, redirectPath: redirect.page,
redirectParams: redirect.params, redirectParams: redirect.params,
}); };
return loggedIn ? route : true; return loggedIn ? route : true;
} }
@ -78,10 +77,10 @@ export class CoreRedirectGuard implements CanLoad, CanActivate {
// Redirect to non-site path. // Redirect to non-site path.
const route = Router.parseUrl(redirect.page); const route = Router.parseUrl(redirect.page);
route.queryParams = CoreObject.withoutEmpty({ route.queryParams = {
redirectPath: redirect.page, redirectPath: redirect.page,
redirectParams: redirect.params, redirectParams: redirect.params,
}); };
return route; return route;
} finally { } finally {

View File

@ -56,7 +56,6 @@ export class CoreAppProvider {
protected isKeyboardShown = false; protected isKeyboardShown = false;
protected keyboardOpening = false; protected keyboardOpening = false;
protected keyboardClosing = false; protected keyboardClosing = false;
protected backActions: {callback: () => boolean; priority: number}[] = [];
protected forceOffline = false; protected forceOffline = false;
protected redirect?: CoreRedirectData; protected redirect?: CoreRedirectData;
@ -68,11 +67,6 @@ export class CoreAppProvider {
this.schemaVersionsManager = new Promise(resolve => this.resolveSchemaVersionsManager = resolve); this.schemaVersionsManager = new Promise(resolve => this.resolveSchemaVersionsManager = resolve);
this.db = CoreDB.getDB(DBNAME); this.db = CoreDB.getDB(DBNAME);
this.logger = CoreLogger.getInstance('CoreAppProvider'); 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 * Register a back button action.
* platform's back button, also referred to as the "hardware" back button. * This function is deprecated and no longer works. You should now use Ionic events directly, please see:
* This event is only used within Cordova apps running on Android and * https://ionicframework.com/docs/developing/hardware-back-button
* 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.
* *
* Registering a hardware back button action and setting a priority allows * @param callback Called when the back button is pressed.
* apps to control which action should be called when the hardware back * @param priority Priority.
* 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).
* @return A function that, when called, will unregister the back button action. * @return A function that, when called, will unregister the back button action.
* @deprecated since 3.9.5
*/ */
registerBackButtonAction(callback: () => boolean, priority: number = 0): () => boolean { // eslint-disable-next-line @typescript-eslint/no-unused-vars
const action = { callback, priority }; registerBackButtonAction(callback: () => boolean, priority = 0): () => boolean {
return () => false;
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);
};
} }
/** /**

View File

@ -19,6 +19,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { CoreWSExternalWarning } from '@services/ws'; import { CoreWSExternalWarning } from '@services/ws';
import { CoreCourses } from '@features/courses/services/courses';
const ROOT_CACHE_KEY = 'mmGroups:'; const ROOT_CACHE_KEY = 'mmGroups:';
@ -242,8 +243,11 @@ export class CoreGroupsProvider {
return this.getUserGroupsInCourse(0, siteId); return this.getUserGroupsInCourse(0, siteId);
} }
// @todo Get courses. const courses = <CoreCourseBase[]> await CoreCourses.getUserCourses(false, siteId);
return <CoreGroup[]>[];
courses.push({ id: site.getSiteHomeId() }); // Add site home.
return this.getUserGroups(courses, siteId);
} }
/** /**

View File

@ -156,8 +156,6 @@ export class CoreLangProvider {
// Use british english when parent english is loaded. // Use british english when parent english is loaded.
moment.locale(language == 'en' ? 'en-gb' : language); moment.locale(language == 'en' ? 'en-gb' : language);
// @todo: Set data for ion-datetime.
this.currentLanguage = language; this.currentLanguage = language;
try { try {
@ -275,6 +273,42 @@ export class CoreLangProvider {
return this.fallbackLanguage; 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. * Get the full list of translations for a certain language.
* *

View File

@ -127,7 +127,7 @@ export class CoreNavigatorService {
const url: string[] = [/^[./]/.test(path) ? path : `./${path}`]; const url: string[] = [/^[./]/.test(path) ? path : `./${path}`];
const navigationOptions: NavigationOptions = CoreObject.withoutEmpty({ const navigationOptions: NavigationOptions = CoreObject.withoutEmpty({
animated: options.animated, 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(), relativeTo: path.startsWith('/') ? null : this.getCurrentRoute(),
}); });

View File

@ -18,6 +18,7 @@ import { CoreConfig } from '@services/config';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreH5P } from '@features/h5p/services/h5p';
const VERSION_APPLIED = 'version_applied'; const VERSION_APPLIED = 'version_applied';
@ -42,13 +43,13 @@ export class CoreUpdateManagerProvider {
* @return Promise resolved when the update process finishes. * @return Promise resolved when the update process finishes.
*/ */
async load(): Promise<void> { async load(): Promise<void> {
const promises = []; const promises: Promise<unknown>[] = [];
const versionCode = CoreConstants.CONFIG.versioncode; const versionCode = CoreConstants.CONFIG.versioncode;
const versionApplied = await CoreConfig.get<number>(VERSION_APPLIED, 0); const versionApplied = await CoreConfig.get<number>(VERSION_APPLIED, 0);
if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) { if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) {
// @todo: H5P update. promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes());
} }
try { try {

View File

@ -31,11 +31,20 @@ import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreCanceledError } from '@classes/errors/cancelederror';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { CoreSilentError } from '@classes/errors/silenterror'; import { CoreSilentError } from '@classes/errors/silenterror';
import {
import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons'; makeSingleton,
Translate,
AlertController,
LoadingController,
ToastController,
PopoverController,
ModalController,
} from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreFileSizeSum } from '@services/plugin-file-delegate';
import { CoreNetworkError } from '@classes/errors/network-error'; 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. * "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('data-original-title', content);
el.setAttribute('title', ''); el.setAttribute('title', '');
el.addEventListener('click', () => { el.addEventListener('click', async (ev: Event) => {
// @todo 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 message Modal message.
* @param buttons Buttons to pass to the modal. * @param buttons Buttons to pass to the modal.
* @param placeholder Placeholder of the input element if any. * @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 async showTextareaPrompt(
showTextareaPrompt(title: string, message: string, buttons: (string | unknown)[], placeholder?: string): Promise<unknown> { title: string,
// @todo message: string,
return Promise.resolve(); 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 componentId An ID to use in conjunction with the component.
* @param fullScreen Whether the modal should be full screen. * @param fullScreen Whether the modal should be full screen.
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars async viewImage(
viewImage(image: string, title?: string | null, component?: string, componentId?: string | number, fullScreen?: boolean): void { image: string,
// @todo 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();
} }
/** /**

View File

@ -25,10 +25,11 @@ import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils'; 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 { CoreLogger } from '@singletons/logger';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
import { CoreWindow } from '@singletons/window'; import { CoreWindow } from '@singletons/window';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
/** /**
* Possible types of frame elements. * 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. // 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'); this.addOfflineWarning(element, src, isSubframe);
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';
}
// If the network changes, check it again. // If the network changes, check it again.
const subscription = Network.onConnect().subscribe(() => { const subscription = Network.onConnect().subscribe(() => {
@ -124,6 +111,77 @@ export class CoreIframeUtilsProvider {
return false; 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. * Given an element, return the content window and document.
* Please notice that the element should be an iframe, embed or similar. * Please notice that the element should be an iframe, embed or similar.

View File

@ -252,4 +252,4 @@ export const NavController = makeSingleton(NavControllerService);
export const Router = makeSingleton(RouterService, ['routerState', 'url']); export const Router = makeSingleton(RouterService, ['routerState', 'url']);
// Convert external libraries injectables. // Convert external libraries injectables.
export const Translate = makeSingleton(TranslateService, ['onLangChange']); export const Translate = makeSingleton(TranslateService, ['onLangChange', 'translations']);

View File

@ -1,5 +1,7 @@
/** Format Text - Show more styles. */ /** Format Text - Show more styles. */
/** Styles of elements inside the directive should be placed in format-text.scss */ /** Styles of elements inside the directive should be placed in format-text.scss */
@import "~theme/globals";
core-format-text { core-format-text {
user-select: text; user-select: text;
word-break: break-word; 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);
}
} }

View File

@ -95,7 +95,8 @@ ion-button.button-small ion-icon.faicon[slot] {
} }
// Ionic alert. // 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; position: relative;
content: " "; content: " ";
background: url("/assets/fonts/font-awesome/solid/wifi.svg") no-repeat 50% 50%; 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%; 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; right: unset;
left: -15%; left: -15%;
} }
@ -442,3 +444,7 @@ ion-button.core-button-select {
.core-monospaced { .core-monospaced {
font-family: Andale Mono,Monaco,Courier New,DejaVu Sans Mono,monospace; font-family: Andale Mono,Monaco,Courier New,DejaVu Sans Mono,monospace;
} }
.core-iframe-offline-disabled {
display: none !important;
}