MOBILE-3565 mainmenu: Initial implementation of main menu
parent
df4dfa6eb5
commit
a332faa81e
|
@ -29,6 +29,10 @@ const routes: Routes = [
|
|||
path: 'settings',
|
||||
loadChildren: () => import('./core/settings/settings.module').then( m => m.CoreAppSettingsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'mainmenu',
|
||||
loadChildren: () => import('./core/mainmenu/mainmenu.module').then( m => m.CoreMainMenuModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreLangProvider } from '@services/lang';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
||||
|
@ -24,7 +26,8 @@ import { CoreEvents } from '@singletons/events';
|
|||
export class AppComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private langProvider: CoreLangProvider,
|
||||
protected langProvider: CoreLangProvider,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -34,16 +37,13 @@ export class AppComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
CoreEvents.on(CoreEvents.LOGOUT, () => {
|
||||
// Go to sites page when user is logged out.
|
||||
// Due to DeepLinker, we need to use the ViewCtrl instead of name.
|
||||
// Otherwise some pages are re-created when they shouldn't.
|
||||
// TODO
|
||||
// CoreApp.instance.getRootNavController().setRoot(CoreLoginSitesPage);
|
||||
this.navCtrl.navigateRoot('/login/sites');
|
||||
|
||||
// Unload lang custom strings.
|
||||
this.langProvider.clearCustomStrings();
|
||||
|
||||
// Remove version classes from body.
|
||||
// TODO
|
||||
// @todo
|
||||
// this.removeVersionClass();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,6 +23,11 @@ import { CoreLoginSitesPage } from './pages/sites/sites.page';
|
|||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'init',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'init',
|
||||
component: CoreLoginInitPage,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -27,7 +27,6 @@ import { CoreLoginCredentialsPage } from './pages/credentials/credentials.page';
|
|||
import { CoreLoginInitPage } from './pages/init/init.page';
|
||||
import { CoreLoginSitePage } from './pages/site/site.page';
|
||||
import { CoreLoginSitesPage } from './pages/sites/sites.page';
|
||||
import { CoreLoginHelperProvider } from './services/helper';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -47,8 +46,5 @@ import { CoreLoginHelperProvider } from './services/helper';
|
|||
CoreLoginSitePage,
|
||||
CoreLoginSitesPage,
|
||||
],
|
||||
providers: [
|
||||
CoreLoginHelperProvider,
|
||||
],
|
||||
})
|
||||
export class CoreLoginModule {}
|
||||
|
|
|
@ -243,7 +243,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
|
|||
|
||||
this.siteId = id;
|
||||
|
||||
await CoreLoginHelper.instance.goToSiteInitialPage(undefined, undefined, undefined, undefined, this.urlToOpen);
|
||||
await CoreLoginHelper.instance.goToSiteInitialPage({ urlToOpen: this.urlToOpen });
|
||||
} catch (error) {
|
||||
CoreLoginHelper.instance.treatUserTokenError(siteUrl, error, username, password);
|
||||
|
||||
|
|
|
@ -15,9 +15,13 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreApp, CoreRedirectData } from '@services/app';
|
||||
import { CoreInit } from '@services/init';
|
||||
import { SplashScreen } from '@singletons/core.singletons';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { CoreSite } from '@/app/classes/site';
|
||||
import { CoreSites } from '@/app/services/sites';
|
||||
import { CoreLoginHelper, CoreLoginHelperProvider } from '../../services/helper';
|
||||
|
||||
/**
|
||||
* Page that displays a "splash screen" while the app is being initialized.
|
||||
|
@ -40,55 +44,75 @@ export class CoreLoginInitPage implements OnInit {
|
|||
|
||||
// Check if there was a pending redirect.
|
||||
const redirectData = CoreApp.instance.getRedirect();
|
||||
|
||||
if (redirectData.siteId) {
|
||||
// Unset redirect data.
|
||||
CoreApp.instance.storeRedirect('', '', {});
|
||||
|
||||
// Only accept the redirect if it was stored less than 20 seconds ago.
|
||||
if (redirectData.timemodified && Date.now() - redirectData.timemodified < 20000) {
|
||||
// if (redirectData.siteId != CoreConstants.NO_SITE_ID) {
|
||||
// // The redirect is pointing to a site, load it.
|
||||
// return this.sitesProvider.loadSite(redirectData.siteId, redirectData.page, redirectData.params)
|
||||
// .then((loggedIn) => {
|
||||
|
||||
// if (loggedIn) {
|
||||
// return this.loginHelper.goToSiteInitialPage(this.navCtrl, redirectData.page, redirectData.params,
|
||||
// { animate: false });
|
||||
// }
|
||||
// }).catch(() => {
|
||||
// // Site doesn't exist.
|
||||
// return this.loadPage();
|
||||
// });
|
||||
// } else {
|
||||
// // No site to load, open the page.
|
||||
// return this.loginHelper.goToNoSitePage(this.navCtrl, redirectData.page, redirectData.params);
|
||||
// }
|
||||
}
|
||||
await this.handleRedirect(redirectData);
|
||||
} else {
|
||||
await this.loadPage();
|
||||
}
|
||||
|
||||
await this.loadPage();
|
||||
|
||||
// If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen.
|
||||
setTimeout(() => {
|
||||
SplashScreen.instance.hide();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat redirect data.
|
||||
*
|
||||
* @param redirectData Redirect data.
|
||||
*/
|
||||
protected async handleRedirect(redirectData: CoreRedirectData): Promise<void> {
|
||||
// Unset redirect data.
|
||||
CoreApp.instance.storeRedirect('', '', {});
|
||||
|
||||
// Only accept the redirect if it was stored less than 20 seconds ago.
|
||||
if (redirectData.timemodified && Date.now() - redirectData.timemodified < 20000) {
|
||||
if (redirectData.siteId != CoreConstants.NO_SITE_ID) {
|
||||
// The redirect is pointing to a site, load it.
|
||||
try {
|
||||
const loggedIn = await CoreSites.instance.loadSite(
|
||||
redirectData.siteId!,
|
||||
redirectData.page,
|
||||
redirectData.params,
|
||||
);
|
||||
|
||||
if (!loggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
return CoreLoginHelper.instance.goToSiteInitialPage({
|
||||
redirectPage: redirectData.page,
|
||||
redirectParams: redirectData.params,
|
||||
});
|
||||
} catch (error) {
|
||||
// Site doesn't exist.
|
||||
return this.loadPage();
|
||||
}
|
||||
} else {
|
||||
// No site to load, open the page.
|
||||
return CoreLoginHelper.instance.goToNoSitePage(redirectData.page, redirectData.params);
|
||||
}
|
||||
}
|
||||
|
||||
return this.loadPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the right page.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadPage(): Promise<void> {
|
||||
// if (this.sitesProvider.isLoggedIn()) {
|
||||
// if (this.loginHelper.isSiteLoggedOut()) {
|
||||
// return this.sitesProvider.logout().then(() => {
|
||||
// return this.loadPage();
|
||||
// });
|
||||
// }
|
||||
if (CoreSites.instance.isLoggedIn()) {
|
||||
if (CoreLoginHelper.instance.isSiteLoggedOut()) {
|
||||
await CoreSites.instance.logout();
|
||||
|
||||
// return this.loginHelper.goToSiteInitialPage();
|
||||
// }
|
||||
return this.loadPage();
|
||||
}
|
||||
|
||||
return CoreLoginHelper.instance.goToSiteInitialPage();
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateRoot('/login/sites');
|
||||
}
|
||||
|
|
|
@ -34,11 +34,14 @@ import { CoreWSError } from '@classes/errors/wserror';
|
|||
import { makeSingleton, Translate } from '@singletons/core.singletons';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { CoreUrl } from '@singletons/url';
|
||||
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
|
||||
|
||||
/**
|
||||
* Helper provider that provides some common features regarding authentication.
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreLoginHelperProvider {
|
||||
|
||||
static readonly OPEN_COURSE = 'open_course';
|
||||
|
@ -448,7 +451,7 @@ export class CoreLoginHelperProvider {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
goToNoSitePage(navCtrl: NavController, page: string, params?: Params): Promise<any> {
|
||||
goToNoSitePage(page?: string, params?: Params): Promise<any> {
|
||||
// @todo
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -456,17 +459,11 @@ export class CoreLoginHelperProvider {
|
|||
/**
|
||||
* Go to the initial page of a site depending on 'userhomepage' setting.
|
||||
*
|
||||
* @param navCtrl NavController to use. Defaults to app root NavController.
|
||||
* @param page Name of the page to load after loading the main page.
|
||||
* @param params Params to pass to the page.
|
||||
* @param options Navigation options.
|
||||
* @param url URL to open once the main menu is loaded.
|
||||
* @param options Options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
goToSiteInitialPage(navCtrl?: NavController, page?: string, params?: Params, options?: any, url?: string): Promise<any> {
|
||||
// @todo
|
||||
return Promise.resolve();
|
||||
goToSiteInitialPage(options?: OpenMainMenuOptions): Promise<void> {
|
||||
return this.openMainMenu(options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -664,17 +661,32 @@ export class CoreLoginHelperProvider {
|
|||
/**
|
||||
* Open the main menu, loading a certain page.
|
||||
*
|
||||
* @param navCtrl NavController.
|
||||
* @param page Name of the page to load.
|
||||
* @param params Params to pass to the page.
|
||||
* @param options Navigation options.
|
||||
* @param url URL to open once the main menu is loaded.
|
||||
* @param options Options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected openMainMenu(navCtrl: NavController, page: string, params: Params, options?: any, url?: string): Promise<any> {
|
||||
// @todo
|
||||
return Promise.resolve();
|
||||
protected async openMainMenu(options?: OpenMainMenuOptions): Promise<void> {
|
||||
|
||||
// Due to DeepLinker, we need to remove the path from the URL before going to main menu.
|
||||
// IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
|
||||
// @todo this.location.replaceState('');
|
||||
|
||||
if (options?.redirectPage == CoreLoginHelperProvider.OPEN_COURSE) {
|
||||
// Load the main menu first, and then open the course.
|
||||
try {
|
||||
await this.navCtrl.navigateRoot('/mainmenu');
|
||||
} finally {
|
||||
// @todo: Open course.
|
||||
}
|
||||
} else {
|
||||
// Open the main menu.
|
||||
const queryParams: Params = Object.assign({}, options);
|
||||
delete queryParams.navigationOptions;
|
||||
|
||||
await this.navCtrl.navigateRoot('/mainmenu', {
|
||||
queryParams,
|
||||
...options?.navigationOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1375,3 +1387,10 @@ type StoredLoginLaunchData = {
|
|||
pageParams: Params;
|
||||
ssoUrlParams: CoreUrlParams;
|
||||
};
|
||||
|
||||
type OpenMainMenuOptions = {
|
||||
redirectPage?: string; // Route of the page to open in main menu. If not defined, default tab will be selected.
|
||||
redirectParams?: Params; // Params to pass to the selected tab if any.
|
||||
urlToOpen?: string; // URL to open once the main menu is loaded.
|
||||
navigationOptions?: NavigationOptions; // Navigation options.
|
||||
};
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"changesite": "Change site",
|
||||
"help": "Help",
|
||||
"logout": "Log out",
|
||||
"website": "Website"
|
||||
}
|
|
@ -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 { CoreMainMenuPage } from './pages/menu/menu.page';
|
||||
import { CoreMainMenuMorePage } from './pages/more/more.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreMainMenuPage,
|
||||
children: [
|
||||
{
|
||||
path: 'more',
|
||||
component: CoreMainMenuMorePage,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreMainMenuRoutingModule {}
|
|
@ -0,0 +1,42 @@
|
|||
// (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 { CommonModule } from '@angular/common';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@/app/components/components.module';
|
||||
import { CoreDirectivesModule } from '@/app/directives/directives.module';
|
||||
|
||||
import { CoreMainMenuRoutingModule } from './mainmenu-routing.module';
|
||||
import { CoreMainMenuPage } from './pages/menu/menu.page';
|
||||
import { CoreMainMenuMorePage } from './pages/more/more.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
CoreMainMenuRoutingModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreMainMenuPage,
|
||||
CoreMainMenuMorePage,
|
||||
],
|
||||
})
|
||||
export class CoreMainMenuModule {}
|
|
@ -0,0 +1,23 @@
|
|||
<ion-tabs #mainTabs [hidden]="!showTabs"> <!-- [loaded]="loaded" -->
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button tab="redirect" [disabled]="true" [hidden]="true"></ion-tab-button> <!-- [show]="false" [root]="redirectPage" [rootParams]="redirectParams" -->
|
||||
|
||||
<ion-tab-button *ngFor="let tab of tabs" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide"> <!-- [rootParams]="tab.pageParams" [tabBadge]="tab.badge" class="{{tab.class}}" [enabled]="!tab.hide" [show]="!tab.hide" -->
|
||||
<core-icon [name]="tab.icon"></core-icon>
|
||||
<ion-label>{{ tab.title | translate }}</ion-label>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="more" layout="label-hide">
|
||||
<core-icon name="fa-bars"></core-icon>
|
||||
<ion-label>{{ 'core.more' | translate }}</ion-label>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
</ion-tabs>
|
||||
<div class="core-network-message" [hidden]="!showTabs">
|
||||
<div class="core-online-message">
|
||||
{{ "core.youreonline" | translate }}
|
||||
</div>
|
||||
<div class="core-offline-message">
|
||||
{{ "core.youreoffline" | translate }}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,225 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { NavController } from '@ionic/angular';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreEvents, CoreEventObserver, CoreEventLoadPageMainMenuData } from '@singletons/events';
|
||||
import { CoreMainMenu } from '../../services/mainmenu';
|
||||
import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/delegate';
|
||||
|
||||
/**
|
||||
* Page that displays the main menu of the app.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-mainmenu',
|
||||
templateUrl: 'menu.html',
|
||||
styleUrls: ['menu.scss'],
|
||||
})
|
||||
export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||
|
||||
tabs: CoreMainMenuHandlerToDisplay[] = [];
|
||||
allHandlers?: CoreMainMenuHandlerToDisplay[];
|
||||
loaded = false;
|
||||
redirectPage?: string;
|
||||
redirectParams?: Params;
|
||||
showTabs = false;
|
||||
tabsPlacement = 'bottom';
|
||||
|
||||
protected subscription?: Subscription;
|
||||
protected redirectObs?: CoreEventObserver;
|
||||
protected pendingRedirect?: CoreEventLoadPageMainMenuData;
|
||||
protected urlToOpen?: string;
|
||||
protected mainMenuId: number;
|
||||
protected keyboardObserver?: CoreEventObserver;
|
||||
|
||||
@ViewChild('mainTabs') mainTabs?: any; // CoreIonTabsComponent;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
protected navCtrl: NavController,
|
||||
protected menuDelegate: CoreMainMenuDelegate,
|
||||
protected changeDetector: ChangeDetectorRef,
|
||||
) {
|
||||
this.mainMenuId = CoreApp.instance.getMainMenuId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (!CoreSites.instance.isLoggedIn()) {
|
||||
this.navCtrl.navigateRoot('/login/init');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.route.queryParams.subscribe(params => {
|
||||
const redirectPage = params['redirectPage'];
|
||||
if (redirectPage) {
|
||||
this.pendingRedirect = {
|
||||
redirectPage: redirectPage,
|
||||
redirectParams: params['redirectParams'],
|
||||
};
|
||||
}
|
||||
|
||||
this.urlToOpen = params['urlToOpen'];
|
||||
});
|
||||
|
||||
this.showTabs = true;
|
||||
|
||||
this.redirectObs = CoreEvents.on(CoreEvents.LOAD_PAGE_MAIN_MENU, (data: CoreEventLoadPageMainMenuData) => {
|
||||
if (!this.loaded) {
|
||||
// View isn't ready yet, wait for it to be ready.
|
||||
this.pendingRedirect = data;
|
||||
} else {
|
||||
delete this.pendingRedirect;
|
||||
this.handleRedirect(data);
|
||||
}
|
||||
});
|
||||
|
||||
this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => {
|
||||
// Remove the handlers that should only appear in the More menu.
|
||||
this.allHandlers = handlers.filter((handler) => !handler.onlyInMore);
|
||||
|
||||
this.initHandlers();
|
||||
|
||||
if (this.loaded && this.pendingRedirect) {
|
||||
// Wait for tabs to be initialized and then handle the redirect.
|
||||
setTimeout(() => {
|
||||
if (this.pendingRedirect) {
|
||||
this.handleRedirect(this.pendingRedirect);
|
||||
delete this.pendingRedirect;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', this.initHandlers.bind(this));
|
||||
|
||||
if (CoreApp.instance.isIOS()) {
|
||||
// In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done.
|
||||
// Init handlers again once keyboard is closed since the resize event doesn't have the updated height.
|
||||
this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, (kbHeight: number) => {
|
||||
if (kbHeight === 0) {
|
||||
this.initHandlers();
|
||||
|
||||
// If the device is slow it can take a bit more to update the window height. Retry in a few ms.
|
||||
setTimeout(() => {
|
||||
this.initHandlers();
|
||||
}, 250);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CoreApp.instance.setMainMenuOpen(this.mainMenuId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init handlers on change (size or handlers).
|
||||
*/
|
||||
initHandlers(): void {
|
||||
if (this.allHandlers) {
|
||||
this.tabsPlacement = CoreMainMenu.instance.getTabPlacement();
|
||||
|
||||
const handlers = this.allHandlers.slice(0, CoreMainMenu.instance.getNumItems()); // Get main handlers.
|
||||
|
||||
// Re-build the list of tabs. If a handler is already in the list, use existing object to prevent re-creating the tab.
|
||||
const newTabs: CoreMainMenuHandlerToDisplay[] = [];
|
||||
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
const handler = handlers[i];
|
||||
|
||||
// Check if the handler is already in the tabs list. If so, use it.
|
||||
const tab = this.tabs.find((tab) => tab.title == handler.title && tab.icon == handler.icon);
|
||||
|
||||
tab ? tab.hide = false : null;
|
||||
handler.hide = false;
|
||||
|
||||
newTabs.push(tab || handler);
|
||||
}
|
||||
|
||||
// Maintain tab in phantom mode in case is not visible.
|
||||
const selectedTab = this.mainTabs?.getSelected();
|
||||
if (selectedTab) {
|
||||
const oldTab = this.tabs.find((tab) => tab.page == selectedTab.root && tab.icon == selectedTab.tabIcon);
|
||||
|
||||
if (oldTab) {
|
||||
// Check if the selected handler is visible.
|
||||
const isVisible = newTabs.some((newTab) => oldTab.title == newTab.title && oldTab.icon == newTab.icon);
|
||||
|
||||
if (!isVisible) {
|
||||
oldTab.hide = true;
|
||||
newTabs.push(oldTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.tabs = newTabs;
|
||||
|
||||
// Sort them by priority so new handlers are in the right position.
|
||||
this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||
|
||||
this.loaded = this.menuDelegate.areHandlersLoaded();
|
||||
}
|
||||
|
||||
if (this.urlToOpen) {
|
||||
// There's a content link to open.
|
||||
// @todo: Treat URL.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a redirect.
|
||||
*
|
||||
* @param data Data received.
|
||||
*/
|
||||
protected handleRedirect(data: CoreEventLoadPageMainMenuData): void {
|
||||
// Check if the redirect page is the root page of any of the tabs.
|
||||
const i = this.tabs.findIndex((tab) => tab.page == data.redirectPage);
|
||||
|
||||
if (i >= 0) {
|
||||
// Tab found. Set the params.
|
||||
this.tabs[i].pageParams = Object.assign({}, data.redirectParams);
|
||||
} else {
|
||||
// Tab not found, use a phantom tab.
|
||||
this.redirectPage = data.redirectPage;
|
||||
this.redirectParams = data.redirectParams;
|
||||
}
|
||||
|
||||
// Force change detection, otherwise sometimes the tab was selected before the params were applied.
|
||||
this.changeDetector.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
// Let the tab load the params before navigating.
|
||||
this.mainTabs?.selectTabRootByIndex(i + 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
this.redirectObs?.off();
|
||||
window.removeEventListener('resize', this.initHandlers.bind(this));
|
||||
CoreApp.instance.setMainMenuOpen(this.mainMenuId, false);
|
||||
this.keyboardObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
ion-icon.tab-button-icon {
|
||||
text-overflow: unset;
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
transition: margin 500ms ease-in-out, transform 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.ion-md-fa-graduation-cap,
|
||||
.ion-ios-fa-graduation-cap,
|
||||
.ion-ios-fa-graduation-cap-outline,
|
||||
.ion-fa-graduation-cap {
|
||||
// @todo @extend .fa-graduation-cap;
|
||||
// @todo @extend .fa;
|
||||
font-size: 21px;
|
||||
height: 21px;
|
||||
|
||||
}
|
||||
|
||||
.ion-ios-fa-graduation-cap-outline {
|
||||
color: transparent;
|
||||
-webkit-text-stroke-width: 0.8px;
|
||||
// @todo -webkit-text-stroke-color: $tabs-tab-color-inactive;
|
||||
font-size: 23px;
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
.ion-md-fa-newspaper-o,
|
||||
.ion-ios-fa-newspaper-o,
|
||||
.ion-ios-fa-newspaper-o-outline,
|
||||
.ion-fa-newspaper-o {
|
||||
// @todo @extend .fa-newspaper-o;
|
||||
// @todo @extend .fa;
|
||||
font-size: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.ion-ios-fa-newspaper-o-outline {
|
||||
font-size: 23px;
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
.core-network-message {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
transition: all 500ms ease-in-out;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.core-online-message,
|
||||
.core-offline-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
.core-online ion-app.app-root page-core-mainmenu,
|
||||
.core-offline ion-app.app-root page-core-mainmenu {
|
||||
|
||||
core-ion-tabs[tabsplacement="bottom"] ion-icon.tab-button-icon {
|
||||
margin-bottom: $core-network-message-height / 2;
|
||||
|
||||
&.icon-ios {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.core-network-message {
|
||||
visibility: visible;
|
||||
height: $core-network-message-height;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.core-offline ion-app.app-root page-core-mainmenu .core-offline-message,
|
||||
.core-online ion-app.app-root page-core-mainmenu .core-online-message {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.core-online ion-app.app-root page-core-mainmenu .core-network-message {
|
||||
background: $green;
|
||||
}
|
||||
|
||||
.core-offline ion-app.app-root page-core-mainmenu .core-network-message {
|
||||
background: $red;
|
||||
}*/
|
|
@ -0,0 +1,84 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
<ion-title><core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"></core-format-text></ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item *ngIf="siteInfo" class="ion-text-wrap"> <!-- @todo core-user-link [userId]="siteInfo.userid" -->
|
||||
<ion-avatar slot="start"></ion-avatar> <!-- @todo core-user-avatar [user]="siteInfo" -->
|
||||
<ion-label>
|
||||
<h2>{{siteInfo.fullname}}</h2>
|
||||
<p><core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true"></core-format-text></p>
|
||||
<p>{{ siteUrl }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
|
||||
<ion-spinner></ion-spinner>
|
||||
</ion-item>
|
||||
<ion-item *ngFor="let handler of handlers" [ngClass]="['core-moremenu-handler', handler.class || '']" (click)="openHandler(handler)" title="{{ handler.title | translate }}" detail="true">
|
||||
<core-icon [name]="handler.icon" slot="start"></core-icon>
|
||||
<ion-label>
|
||||
<h2>{{ handler.title | translate}}</h2>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge">{{handler.badge}}</ion-badge>
|
||||
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading"></ion-spinner>
|
||||
</ion-item>
|
||||
<div *ngFor="let item of customItems" class="core-moremenu-customitem">
|
||||
<ion-item *ngIf="item.type != 'embedded'" [href]="item.url" title="{{item.label}}"> <!-- @todo core-link [capture]="item.type == 'app'" [inApp]="item.type == 'inappbrowser'" -->
|
||||
<core-icon [name]="item.icon" slot="start"></core-icon>
|
||||
<ion-label>
|
||||
<h2>{{item.label}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="item.type == 'embedded'" (click)="openItem(item)" title="{{item.label}}">
|
||||
<core-icon [name]="item.icon" slot="start"></core-icon>
|
||||
<ion-label>
|
||||
<h2>{{item.label}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<ion-item *ngIf="showScanQR" (click)="scanQR()">
|
||||
<core-icon name="fa-qrcode" slot="start" aria-hidden="true"></core-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.scanqr' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="showWeb" [href]="siteInfo.siteurl" core-link autoLogin="yes" title="{{ 'core.mainmenu.website' | translate }}">
|
||||
<ion-icon name="globe" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.mainmenu.website' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="showHelp" [href]="docsUrl" core-link autoLogin="no" title="{{ 'core.mainmenu.help' | translate }}">
|
||||
<ion-icon name="help-buoy" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.mainmenu.help' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item (click)="openSitePreferences()" title="{{ 'core.settings.preferences' | translate }}">
|
||||
<core-icon name="fa-wrench" slot="start"></core-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.preferences' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item (click)="logout()" title="{{ logoutLabel | translate }}">
|
||||
<ion-icon name="log-out" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ logoutLabel | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item (click)="openAppSettings()" title="{{ 'core.settings.appsettings' | translate }}">
|
||||
<core-icon name="fa-cogs" slot="start"></core-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.appsettings' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
|
@ -0,0 +1,182 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreSiteInfo } from '@classes/site';
|
||||
import { CoreLoginHelper } from '@core/login/services/helper';
|
||||
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/delegate';
|
||||
import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
||||
/**
|
||||
* Page that displays the main menu of the app.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-mainmenu-more',
|
||||
templateUrl: 'more.html',
|
||||
styleUrls: ['more.scss'],
|
||||
})
|
||||
export class CoreMainMenuMorePage implements OnInit, OnDestroy {
|
||||
|
||||
handlers?: CoreMainMenuHandlerData[];
|
||||
allHandlers?: CoreMainMenuHandlerData[];
|
||||
handlersLoaded = false;
|
||||
siteInfo?: CoreSiteInfo;
|
||||
siteName?: string;
|
||||
logoutLabel?: string;
|
||||
showScanQR: boolean;
|
||||
showWeb?: boolean;
|
||||
showHelp?: boolean;
|
||||
docsUrl?: string;
|
||||
customItems?: CoreMainMenuCustomItem[];
|
||||
siteUrl?: string;
|
||||
|
||||
protected subscription!: Subscription;
|
||||
protected langObserver: CoreEventObserver;
|
||||
protected updateSiteObserver: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected menuDelegate: CoreMainMenuDelegate,
|
||||
) {
|
||||
|
||||
this.langObserver = CoreEvents.on(CoreEvents.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this));
|
||||
this.updateSiteObserver = CoreEvents.on(
|
||||
CoreEvents.SITE_UPDATED,
|
||||
this.loadSiteInfo.bind(this),
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
this.loadSiteInfo();
|
||||
this.showScanQR = CoreUtils.instance.canScanQR() &&
|
||||
!CoreSites.instance.getCurrentSite()?.isFeatureDisabled('CoreMainMenuDelegate_QrReader');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize component.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Load the handlers.
|
||||
this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => {
|
||||
this.allHandlers = handlers;
|
||||
|
||||
this.initHandlers();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', this.initHandlers.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
window.removeEventListener('resize', this.initHandlers.bind(this));
|
||||
this.langObserver?.off();
|
||||
this.updateSiteObserver?.off();
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init handlers on change (size or handlers).
|
||||
*/
|
||||
initHandlers(): void {
|
||||
if (!this.allHandlers) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the main handlers not to display them in this view.
|
||||
const mainHandlers = this.allHandlers
|
||||
.filter((handler) => !handler.onlyInMore)
|
||||
.slice(0, CoreMainMenu.instance.getNumItems());
|
||||
|
||||
// Get only the handlers that don't appear in the main view.
|
||||
this.handlers = this.allHandlers.filter((handler) => mainHandlers.indexOf(handler) == -1);
|
||||
|
||||
this.handlersLoaded = this.menuDelegate.areHandlersLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the site info required by the view.
|
||||
*/
|
||||
protected async loadSiteInfo(): Promise<void> {
|
||||
const currentSite = CoreSites.instance.getCurrentSite();
|
||||
|
||||
if (!currentSite) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.siteInfo = currentSite.getInfo();
|
||||
this.siteName = currentSite.getSiteName();
|
||||
this.siteUrl = currentSite.getURL();
|
||||
this.logoutLabel = CoreLoginHelper.instance.getLogoutLabel(currentSite);
|
||||
this.showWeb = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_website');
|
||||
this.showHelp = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_help');
|
||||
|
||||
this.docsUrl = await currentSite.getDocsUrl();
|
||||
|
||||
this.customItems = await CoreMainMenu.instance.getCustomMenuItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a handler.
|
||||
*
|
||||
* @param handler Handler to open.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
openHandler(handler: CoreMainMenuHandlerData): void {
|
||||
// @todo
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an embedded custom item.
|
||||
*
|
||||
* @param item Item to open.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
openItem(item: CoreMainMenuCustomItem): void {
|
||||
// @todo
|
||||
}
|
||||
|
||||
/**
|
||||
* Open app settings page.
|
||||
*/
|
||||
openAppSettings(): void {
|
||||
// @todo
|
||||
}
|
||||
|
||||
/**
|
||||
* Open site settings page.
|
||||
*/
|
||||
openSitePreferences(): void {
|
||||
// @todo
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan and treat a QR code.
|
||||
*/
|
||||
async scanQR(): Promise<void> {
|
||||
// Scan for a QR code.
|
||||
// @todo
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the user.
|
||||
*/
|
||||
logout(): void {
|
||||
CoreSites.instance.logout();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
$core-more-icon: $gray-darker !default;
|
||||
$core-more-background-ios: $list-ios-background-color !default;
|
||||
$core-more-background-md: $list-md-background-color !default;
|
||||
$core-more-activated-background-ios: color-shade($core-more-background-ios) !default;
|
||||
$core-more-activated-background-md: color-shade($core-more-background-md) !default;
|
||||
$core-more-divider-ios: $item-ios-divider-background !default;
|
||||
$core-more-divider-md: $item-md-divider-background !default;
|
||||
$core-more-border-ios: $list-ios-border-color !default;
|
||||
$core-more-border-md: $list-md-border-color !default;
|
||||
$core-more-color-ios: $list-ios-text-color!default;
|
||||
$core-more-color-md: $list-md-text-color !default;
|
||||
|
||||
.item-block {
|
||||
&.item-ios {
|
||||
background-color: $core-more-background-ios;
|
||||
color: $core-more-color-ios;
|
||||
p {
|
||||
color: $core-more-color-ios;
|
||||
}
|
||||
|
||||
.item-inner {
|
||||
border-bottom: $hairlines-width solid $core-more-border-ios;
|
||||
}
|
||||
}
|
||||
&.item-md {
|
||||
background-color: $core-more-background-md;
|
||||
color: $core-more-color-md;
|
||||
p {
|
||||
color: $core-more-color-md;
|
||||
}
|
||||
|
||||
.item-inner {
|
||||
border-bottom: 1px solid $core-more-border-md;
|
||||
}
|
||||
}
|
||||
|
||||
&.activated {
|
||||
&.item-ios {
|
||||
background-color: $core-more-activated-background-ios;
|
||||
}
|
||||
&.item-md {
|
||||
background-color: $core-more-activated-background-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
color: $core-more-icon;
|
||||
}
|
||||
|
||||
.item-divider {
|
||||
&.item-ios {
|
||||
background-color: $core-more-divider-ios;
|
||||
}
|
||||
|
||||
&.item-md {
|
||||
background-color: $core-more-divider-md;
|
||||
border-bottom: $core-more-border-md;
|
||||
}
|
||||
}
|
||||
|
||||
@include darkmode() {
|
||||
ion-icon {
|
||||
color: $core-dark-text-color;
|
||||
}
|
||||
|
||||
.item-divider {
|
||||
&.item-ios,
|
||||
&.item-md {
|
||||
color: $core-dark-text-color;
|
||||
background-color: $core-dark-item-divider-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.item-block {
|
||||
&.item-ios,
|
||||
&.item-md {
|
||||
color: $core-dark-text-color;
|
||||
background-color: $core-dark-item-bg-color;
|
||||
p {
|
||||
color: $core-dark-text-color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.activated {
|
||||
&.item-ios {
|
||||
background-color: $core-more-activated-background-ios;
|
||||
}
|
||||
&.item-md {
|
||||
background-color: $core-more-activated-background-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,175 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { Subject, BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
||||
/**
|
||||
* Interface that all main menu handlers must implement.
|
||||
*/
|
||||
export interface CoreMainMenuHandler extends CoreDelegateHandler {
|
||||
/**
|
||||
* The highest priority is displayed first.
|
||||
*/
|
||||
priority?: number;
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data.
|
||||
*/
|
||||
getDisplayData(): CoreMainMenuHandlerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data needed to render a main menu handler. It's returned by the handler.
|
||||
*/
|
||||
export interface CoreMainMenuHandlerData {
|
||||
/**
|
||||
* Name of the page to load for the handler.
|
||||
*/
|
||||
page: string;
|
||||
|
||||
/**
|
||||
* Title to display for the handler.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Name of the icon to display for the handler.
|
||||
*/
|
||||
icon: string; // Name of the icon to display in the tab.
|
||||
|
||||
/**
|
||||
* Class to add to the displayed handler.
|
||||
*/
|
||||
class?: string;
|
||||
|
||||
/**
|
||||
* If the handler has badge to show or not.
|
||||
*/
|
||||
showBadge?: boolean;
|
||||
|
||||
/**
|
||||
* Text to display on the badge. Only used if showBadge is true.
|
||||
*/
|
||||
badge?: string;
|
||||
|
||||
/**
|
||||
* If true, the badge number is being loaded. Only used if showBadge is true.
|
||||
*/
|
||||
loading?: boolean;
|
||||
|
||||
/**
|
||||
* Params to pass to the page.
|
||||
*/
|
||||
pageParams?: Params;
|
||||
|
||||
/**
|
||||
* Whether the handler should only appear in More menu.
|
||||
*/
|
||||
onlyInMore?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data returned by the delegate for each handler.
|
||||
*/
|
||||
export interface CoreMainMenuHandlerToDisplay extends CoreMainMenuHandlerData {
|
||||
/**
|
||||
* Name of the handler.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Priority of the handler.
|
||||
*/
|
||||
priority?: number;
|
||||
|
||||
/**
|
||||
* Hide tab. Used then resizing.
|
||||
*/
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin
|
||||
* and notify an update in the data.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreMainMenuDelegate extends CoreDelegate {
|
||||
|
||||
protected loaded = false;
|
||||
protected siteHandlers: Subject<CoreMainMenuHandlerToDisplay[]> = new BehaviorSubject<CoreMainMenuHandlerToDisplay[]>([]);
|
||||
protected featurePrefix = 'CoreMainMenuDelegate_';
|
||||
|
||||
constructor() {
|
||||
super('CoreMainMenuDelegate', true);
|
||||
|
||||
CoreEvents.on(CoreEvents.LOGOUT, this.clearSiteHandlers.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if handlers are loaded.
|
||||
*
|
||||
* @return True if handlers are loaded, false otherwise.
|
||||
*/
|
||||
areHandlersLoaded(): boolean {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear current site handlers. Reserved for core use.
|
||||
*/
|
||||
protected clearSiteHandlers(): void {
|
||||
this.loaded = false;
|
||||
this.siteHandlers.next([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the handlers for the current site.
|
||||
*
|
||||
* @return An observable that will receive the handlers.
|
||||
*/
|
||||
getHandlers(): Subject<CoreMainMenuHandlerToDisplay[]> {
|
||||
return this.siteHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update handlers Data.
|
||||
*/
|
||||
updateData(): void {
|
||||
const displayData: CoreMainMenuHandlerToDisplay[] = [];
|
||||
|
||||
for (const name in this.enabledHandlers) {
|
||||
const handler = <CoreMainMenuHandler> this.enabledHandlers[name];
|
||||
const data = <CoreMainMenuHandlerToDisplay> handler.getDisplayData();
|
||||
|
||||
data.name = name;
|
||||
data.priority = handler.priority;
|
||||
}
|
||||
|
||||
// Sort them by priority.
|
||||
displayData.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||
|
||||
this.loaded = true;
|
||||
this.siteHandlers.next(displayData);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreLang } from '@services/lang';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from './delegate';
|
||||
import { makeSingleton } from '@singletons/core.singletons';
|
||||
|
||||
/**
|
||||
* Service that provides some features regarding Main Menu.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreMainMenuProvider {
|
||||
|
||||
static readonly NUM_MAIN_HANDLERS = 4;
|
||||
static readonly ITEM_MIN_WIDTH = 72; // Min with of every item, based on 5 items on a 360 pixel wide screen.
|
||||
|
||||
protected tablet = false;
|
||||
|
||||
constructor(protected menuDelegate: CoreMainMenuDelegate) {
|
||||
this.tablet = !!(window?.innerWidth && window.innerWidth >= 576 && window.innerHeight >= 576);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current main menu handlers.
|
||||
*
|
||||
* @return Promise resolved with the current main menu handlers.
|
||||
*/
|
||||
getCurrentMainMenuHandlers(): Promise<CoreMainMenuHandlerToDisplay[]> {
|
||||
const deferred = CoreUtils.instance.promiseDefer<CoreMainMenuHandlerToDisplay[]>();
|
||||
|
||||
const subscription = this.menuDelegate.getHandlers().subscribe((handlers) => {
|
||||
subscription?.unsubscribe();
|
||||
|
||||
// Remove the handlers that should only appear in the More menu.
|
||||
handlers = handlers.filter(handler => !handler.onlyInMore);
|
||||
|
||||
// Return main handlers.
|
||||
deferred.resolve(handlers.slice(0, this.getNumItems()));
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of custom menu items for a certain site.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return List of custom menu items.
|
||||
*/
|
||||
async getCustomMenuItems(siteId?: string): Promise<CoreMainMenuCustomItem[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
const itemsString = site.getStoredConfig('tool_mobile_custommenuitems');
|
||||
const map: CustomMenuItemsMap = {};
|
||||
const result: CoreMainMenuCustomItem[] = [];
|
||||
|
||||
let position = 0; // Position of each item, to keep the same order as it's configured.
|
||||
|
||||
if (!itemsString || typeof itemsString != 'string') {
|
||||
// Setting not valid.
|
||||
return result;
|
||||
}
|
||||
|
||||
// Add items to the map.
|
||||
const items = itemsString.split(/(?:\r\n|\r|\n)/);
|
||||
items.forEach((item) => {
|
||||
const values = item.split('|');
|
||||
const label = values[0] ? values[0].trim() : values[0];
|
||||
const url = values[1] ? values[1].trim() : values[1];
|
||||
const type = values[2] ? values[2].trim() : values[2];
|
||||
const lang = (values[3] ? values[3].trim() : values[3]) || 'none';
|
||||
let icon = values[4] ? values[4].trim() : values[4];
|
||||
|
||||
if (!label || !url || !type) {
|
||||
// Invalid item, ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
const id = url + '#' + type;
|
||||
if (!icon) {
|
||||
// Icon not defined, use default one.
|
||||
icon = type == 'embedded' ? 'fa-square-o' : 'fa-link'; // @todo: Find a better icon for embedded.
|
||||
}
|
||||
|
||||
if (!map[id]) {
|
||||
// New entry, add it to the map.
|
||||
map[id] = {
|
||||
url: url,
|
||||
type: type,
|
||||
position: position,
|
||||
labels: {},
|
||||
};
|
||||
position++;
|
||||
}
|
||||
|
||||
map[id].labels[lang.toLowerCase()] = {
|
||||
label: label,
|
||||
icon: icon,
|
||||
};
|
||||
});
|
||||
|
||||
if (!position) {
|
||||
// No valid items found, stop.
|
||||
return result;
|
||||
}
|
||||
|
||||
const currentLang = await CoreLang.instance.getCurrentLanguage();
|
||||
|
||||
const fallbackLang = CoreConstants.CONFIG.default_lang || 'en';
|
||||
|
||||
// Get the right label for each entry and add it to the result.
|
||||
for (const id in map) {
|
||||
const entry = map[id];
|
||||
let data = entry.labels[currentLang] || entry.labels[currentLang + '_only'] ||
|
||||
entry.labels.none || entry.labels[fallbackLang];
|
||||
|
||||
if (!data) {
|
||||
// No valid label found, get the first one that is not "_only".
|
||||
for (const lang in entry.labels) {
|
||||
if (lang.indexOf('_only') == -1) {
|
||||
data = entry.labels[lang];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
// No valid label, ignore this entry.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result[entry.position] = {
|
||||
url: entry.url,
|
||||
type: entry.type,
|
||||
label: data.label,
|
||||
icon: data.icon,
|
||||
};
|
||||
}
|
||||
|
||||
// Remove undefined values.
|
||||
return result.filter((entry) => typeof entry != 'undefined');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of items to be shown on the main menu bar.
|
||||
*
|
||||
* @return Number of items depending on the device width.
|
||||
*/
|
||||
getNumItems(): number {
|
||||
if (!this.isResponsiveMainMenuItemsDisabledInCurrentSite() && window && window.innerWidth) {
|
||||
let numElements: number;
|
||||
|
||||
if (this.tablet) {
|
||||
// Tablet, menu will be displayed vertically.
|
||||
numElements = Math.floor(window.innerHeight / CoreMainMenuProvider.ITEM_MIN_WIDTH);
|
||||
} else {
|
||||
numElements = Math.floor(window.innerWidth / CoreMainMenuProvider.ITEM_MIN_WIDTH);
|
||||
|
||||
// Set a maximum elements to show and skip more button.
|
||||
numElements = numElements >= 5 ? 5 : numElements;
|
||||
}
|
||||
|
||||
// Set a mínimum elements to show and skip more button.
|
||||
return numElements > 1 ? numElements - 1 : 1;
|
||||
}
|
||||
|
||||
return CoreMainMenuProvider.NUM_MAIN_HANDLERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tabs placement depending on the device size.
|
||||
*
|
||||
* @return Tabs placement including side value.
|
||||
*/
|
||||
getTabPlacement(): string {
|
||||
const tablet = !!(window.innerWidth && window.innerWidth >= 576 && (window.innerHeight >= 576 ||
|
||||
((CoreApp.instance.isKeyboardVisible() || CoreApp.instance.isKeyboardOpening()) && window.innerHeight >= 200)));
|
||||
|
||||
if (tablet != this.tablet) {
|
||||
this.tablet = tablet;
|
||||
|
||||
// @todo Resize so content margins can be updated.
|
||||
}
|
||||
|
||||
return tablet ? 'side' : 'bottom';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a certain page is the root of a main menu handler currently displayed.
|
||||
*
|
||||
* @param page Name of the page.
|
||||
* @param pageParams Page params.
|
||||
* @return Promise resolved with boolean: whether it's the root of a main menu handler.
|
||||
*/
|
||||
async isCurrentMainMenuHandler(pageName: string): Promise<boolean> {
|
||||
const handlers = await this.getCurrentMainMenuHandlers();
|
||||
|
||||
const handler = handlers.find((handler) => handler.page == pageName);
|
||||
|
||||
return !!handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if responsive main menu items is disabled in the current site.
|
||||
*
|
||||
* @return Whether it's disabled.
|
||||
*/
|
||||
protected isResponsiveMainMenuItemsDisabledInCurrentSite(): boolean {
|
||||
const site = CoreSites.instance.getCurrentSite();
|
||||
|
||||
return !!site?.isFeatureDisabled('NoDelegate_ResponsiveMainMenuItems');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreMainMenu extends makeSingleton(CoreMainMenuProvider) {}
|
||||
|
||||
/**
|
||||
* Custom main menu item.
|
||||
*/
|
||||
export interface CoreMainMenuCustomItem {
|
||||
/**
|
||||
* Type of the item: app, inappbrowser, browser or embedded.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Url of the item.
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* Label to display for the item.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Name of the icon to display for the item.
|
||||
*/
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of custom menu items.
|
||||
*/
|
||||
type CustomMenuItemsMap = Record<string, {
|
||||
url: string;
|
||||
type: string;
|
||||
position: number;
|
||||
labels: {
|
||||
[lang: string]: {
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
||||
};
|
||||
}>;
|
|
@ -1312,7 +1312,7 @@ export class CoreSitesProvider {
|
|||
async logout(): Promise<void> {
|
||||
await this.dbReady;
|
||||
|
||||
let siteId;
|
||||
let siteId: string | undefined;
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
if (this.currentSite) {
|
||||
|
|
|
@ -208,3 +208,11 @@ export type CoreEventLoadingChangedData = {
|
|||
loaded: boolean;
|
||||
uniqueId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to LOAD_PAGE_MAIN_MENU event.
|
||||
*/
|
||||
export type CoreEventLoadPageMainMenuData = {
|
||||
redirectPage: string;
|
||||
redirectParams?: Params;
|
||||
};
|
||||
|
|
|
@ -304,6 +304,10 @@
|
|||
"core.browser": "Browser",
|
||||
"core.copiedtoclipboard": "Text copied to clipboard",
|
||||
"core.login.yourenteredsite": "Connect to your site",
|
||||
"core.mainmenu.changesite": "Change site",
|
||||
"core.mainmenu.help": "Help",
|
||||
"core.mainmenu.logout": "Log out",
|
||||
"core.mainmenu.website": "Website",
|
||||
"core.no": "No",
|
||||
"core.offline": "Offline",
|
||||
"core.ok": "OK",
|
||||
|
|
Loading…
Reference in New Issue