Merge pull request #3902 from crazyserver/MOBILE-4266
MOBILE-4266 site: Add site theme class to html tagsmain
commit
667ba02986
|
@ -13,22 +13,11 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { AppComponent } from '@/app/app.component';
|
import { AppComponent } from '@/app/app.component';
|
||||||
import { CoreEvents } from '@singletons/events';
|
|
||||||
import { CoreLang, CoreLangProvider } from '@services/lang';
|
|
||||||
|
|
||||||
import { mockSingleton, renderComponent } from '@/testing/utils';
|
import { renderComponent } from '@/testing/utils';
|
||||||
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
|
|
||||||
let langProvider: CoreLangProvider;
|
|
||||||
let navigator: CoreNavigatorService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
navigator = mockSingleton(CoreNavigator, ['navigate']);
|
|
||||||
langProvider = mockSingleton(CoreLang, ['clearCustomStrings']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render', async () => {
|
it('should render', async () => {
|
||||||
const fixture = await renderComponent(AppComponent);
|
const fixture = await renderComponent(AppComponent);
|
||||||
|
|
||||||
|
@ -36,14 +25,4 @@ describe('AppComponent', () => {
|
||||||
expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy();
|
expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cleans up on logout', async () => {
|
|
||||||
const fixture = await renderComponent(AppComponent);
|
|
||||||
|
|
||||||
fixture.componentInstance.ngOnInit();
|
|
||||||
CoreEvents.trigger(CoreEvents.LOGOUT);
|
|
||||||
|
|
||||||
expect(langProvider.clearCustomStrings).toHaveBeenCalled();
|
|
||||||
expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,33 +14,20 @@
|
||||||
|
|
||||||
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { IonRouterOutlet } from '@ionic/angular';
|
import { IonRouterOutlet } from '@ionic/angular';
|
||||||
import { BackButtonEvent, ScrollDetail } from '@ionic/core';
|
import { BackButtonEvent } from '@ionic/core';
|
||||||
|
|
||||||
import { CoreLang } from '@services/lang';
|
|
||||||
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { SplashScreen } from '@singletons';
|
||||||
import { NgZone, SplashScreen } from '@singletons';
|
|
||||||
import { CoreNetwork } from '@services/network';
|
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreSites } from '@services/sites';
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSubscriptions } from '@singletons/subscriptions';
|
import { CoreSubscriptions } from '@singletons/subscriptions';
|
||||||
import { CoreWindow } from '@singletons/window';
|
import { CoreWindow } from '@singletons/window';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreConstants } from '@/core/constants';
|
|
||||||
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
|
||||||
import { CoreDom } from '@singletons/dom';
|
|
||||||
import { CorePlatform } from '@services/platform';
|
import { CorePlatform } from '@services/platform';
|
||||||
import { CoreUrl } from '@singletons/url';
|
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { CorePromisedValue } from '@classes/promised-value';
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
import { register } from 'swiper/element/bundle';
|
import { register } from 'swiper/element/bundle';
|
||||||
|
|
||||||
const MOODLE_SITE_URL_PREFIX = 'url-';
|
|
||||||
const MOODLE_VERSION_PREFIX = 'version-';
|
|
||||||
const MOODLEAPP_VERSION_PREFIX = 'moodleapp-';
|
|
||||||
|
|
||||||
register();
|
register();
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -59,43 +46,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const win = <any> window;
|
const win = <any> window;
|
||||||
CoreDomUtils.toggleModeClass('ionic7', true, { includeLegacy: true });
|
|
||||||
CoreDomUtils.toggleModeClass('development', CoreConstants.BUILD.isDevelopment);
|
|
||||||
this.addVersionClass(MOODLEAPP_VERSION_PREFIX, CoreConstants.CONFIG.versionname.replace('-dev', ''));
|
|
||||||
|
|
||||||
CoreEvents.on(CoreEvents.LOGOUT, async () => {
|
|
||||||
// Unload lang custom strings.
|
|
||||||
CoreLang.clearCustomStrings();
|
|
||||||
|
|
||||||
// Remove version classes from body.
|
|
||||||
this.removeModeClasses([MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX]);
|
|
||||||
|
|
||||||
// Go to sites page when user is logged out.
|
|
||||||
await CoreNavigator.navigate('/login/sites', { reset: true });
|
|
||||||
|
|
||||||
if (CoreSitePlugins.hasSitePluginsLoaded) {
|
|
||||||
// Temporary fix. Reload the page to unload all plugins.
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to scroll to add style when scroll is not 0.
|
|
||||||
win.addEventListener('ionScroll', async ({ detail, target }: CustomEvent<ScrollDetail>) => {
|
|
||||||
if ((target as HTMLElement).tagName != 'ION-CONTENT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const content = (target as HTMLIonContentElement);
|
|
||||||
|
|
||||||
const page = content.closest('.ion-page');
|
|
||||||
if (!page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
page.querySelector<HTMLIonHeaderElement>('ion-header')?.classList.toggle('core-header-shadow', detail.scrollTop > 0);
|
|
||||||
|
|
||||||
const scrollElement = await content.getScrollElement();
|
|
||||||
content.classList.toggle('core-footer-shadow', !CoreDom.scrollIsBottom(scrollElement));
|
|
||||||
});
|
|
||||||
|
|
||||||
CorePlatform.resume.subscribe(() => {
|
CorePlatform.resume.subscribe(() => {
|
||||||
// Wait a second before setting it to false since in iOS there could be some frozen WS calls.
|
// Wait a second before setting it to false since in iOS there could be some frozen WS calls.
|
||||||
|
@ -117,53 +67,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
CoreWindow.open(url);
|
CoreWindow.open(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
CoreEvents.on(CoreEvents.LOGIN, async (data) => {
|
|
||||||
if (data.siteId) {
|
|
||||||
const site = await CoreSites.getSite(data.siteId);
|
|
||||||
const info = site.getInfo();
|
|
||||||
if (info) {
|
|
||||||
// Add version classes to body.
|
|
||||||
this.removeModeClasses([MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX]);
|
|
||||||
|
|
||||||
this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(info.release || ''));
|
|
||||||
this.addSiteUrlClass(info.siteurl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadCustomStrings();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Site config is checked in login.
|
|
||||||
CoreEvents.on(CoreEvents.LOGIN_SITE_CHECKED, (data) => {
|
|
||||||
this.addSiteUrlClass(data.config.httpswwwroot);
|
|
||||||
});
|
|
||||||
|
|
||||||
CoreEvents.on(CoreEvents.SITE_UPDATED, async (data) => {
|
|
||||||
if (data.siteId === CoreSites.getCurrentSiteId()) {
|
|
||||||
this.loadCustomStrings();
|
|
||||||
|
|
||||||
// Add version classes to body.
|
|
||||||
this.removeModeClasses([MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX]);
|
|
||||||
|
|
||||||
this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(data.release || ''));
|
|
||||||
this.addSiteUrlClass(data.siteurl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
CoreEvents.on(CoreEvents.SITE_ADDED, (data) => {
|
|
||||||
if (data.siteId === CoreSites.getCurrentSiteId()) {
|
|
||||||
this.loadCustomStrings();
|
|
||||||
|
|
||||||
// Add version classes to body.
|
|
||||||
this.removeModeClasses([MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX]);
|
|
||||||
|
|
||||||
this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(data.release || ''));
|
|
||||||
this.addSiteUrlClass(data.siteurl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onPlatformReady();
|
|
||||||
|
|
||||||
// Quit app with back button.
|
// Quit app with back button.
|
||||||
document.addEventListener('ionBackButton', (event: BackButtonEvent) => {
|
document.addEventListener('ionBackButton', (event: BackButtonEvent) => {
|
||||||
// This callback should have the lowest priority in the app.
|
// This callback should have the lowest priority in the app.
|
||||||
|
@ -244,121 +147,4 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Async init function on platform ready.
|
|
||||||
*/
|
|
||||||
protected async onPlatformReady(): Promise<void> {
|
|
||||||
await CorePlatform.ready();
|
|
||||||
|
|
||||||
this.logger.debug('Platform is ready');
|
|
||||||
|
|
||||||
// Refresh online status when changes.
|
|
||||||
CoreNetwork.onChange().subscribe(() => {
|
|
||||||
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
|
||||||
NgZone.run(() => {
|
|
||||||
const isOnline = CoreNetwork.isOnline();
|
|
||||||
const hadOfflineMessage = CoreDomUtils.hasModeClass('core-offline');
|
|
||||||
|
|
||||||
CoreDomUtils.toggleModeClass('core-offline', !isOnline, { includeLegacy: true });
|
|
||||||
|
|
||||||
if (isOnline && hadOfflineMessage) {
|
|
||||||
CoreDomUtils.toggleModeClass('core-online', true, { includeLegacy: true });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
CoreDomUtils.toggleModeClass('core-online', false, { includeLegacy: true });
|
|
||||||
}, 3000);
|
|
||||||
} else if (!isOnline) {
|
|
||||||
CoreDomUtils.toggleModeClass('core-online', false, { includeLegacy: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const isOnline = CoreNetwork.isOnline();
|
|
||||||
CoreDomUtils.toggleModeClass('core-offline', !isOnline, { includeLegacy: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load custom lang strings. This cannot be done inside the lang provider because it causes circular dependencies.
|
|
||||||
*/
|
|
||||||
protected loadCustomStrings(): void {
|
|
||||||
const currentSite = CoreSites.getCurrentSite();
|
|
||||||
|
|
||||||
if (currentSite) {
|
|
||||||
CoreLang.loadCustomStringsFromSite(currentSite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to add version to html classes.
|
|
||||||
*
|
|
||||||
* @param prefix Prefix to add to the class.
|
|
||||||
* @param release Current release number of the site.
|
|
||||||
*/
|
|
||||||
protected addVersionClass(prefix: string, release: string): void {
|
|
||||||
const parts = release.split('.', 3);
|
|
||||||
|
|
||||||
parts[1] = parts[1] || '0';
|
|
||||||
parts[2] = parts[2] || '0';
|
|
||||||
|
|
||||||
CoreDomUtils.toggleModeClass(prefix + parts[0], true, { includeLegacy: true });
|
|
||||||
CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1], true, { includeLegacy: true });
|
|
||||||
CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1] + '-' + parts[2], true, { includeLegacy: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to remove all mode classes form body.
|
|
||||||
*
|
|
||||||
* @param prefixes Prefixes of the class mode to be removed.
|
|
||||||
*/
|
|
||||||
protected removeModeClasses(prefixes: string[]): void {
|
|
||||||
for (const modeClass of CoreDomUtils.getModeClasses()) {
|
|
||||||
if (!prefixes.some((prefix) => modeClass.startsWith(prefix))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreDomUtils.toggleModeClass(modeClass, false, { includeLegacy: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the provided URL into a CSS class that be used within the page.
|
|
||||||
* This is primarily used to add the siteurl to the body tag as a CSS class.
|
|
||||||
* Extracted from LMS url_to_class_name function.
|
|
||||||
*
|
|
||||||
* @param url Url.
|
|
||||||
* @returns Class name
|
|
||||||
*/
|
|
||||||
protected urlToClassName(url: string): string {
|
|
||||||
const parsedUrl = CoreUrl.parse(url);
|
|
||||||
|
|
||||||
if (!parsedUrl) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
let className = parsedUrl.domain?.replace(/\./g, '-') || '';
|
|
||||||
|
|
||||||
if (parsedUrl.port) {
|
|
||||||
className += `--${parsedUrl.port}`;
|
|
||||||
}
|
|
||||||
if (parsedUrl.path) {
|
|
||||||
const leading = new RegExp('^/+');
|
|
||||||
const trailing = new RegExp('/+$');
|
|
||||||
const path = parsedUrl.path.replace(leading, '').replace(trailing, '');
|
|
||||||
if (path) {
|
|
||||||
className += '--' + path.replace(/\//g, '-') || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return className;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to add site url to html classes.
|
|
||||||
*/
|
|
||||||
protected addSiteUrlClass(siteUrl: string): void {
|
|
||||||
const className = this.urlToClassName(siteUrl);
|
|
||||||
|
|
||||||
CoreDomUtils.toggleModeClass(MOODLE_SITE_URL_PREFIX + className, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1402,7 +1402,7 @@ export type CoreCourseSearchedData = CoreCourseBasicSearchedData & {
|
||||||
enablecompletion?: number; // Completion enabled? 1: yes 0: no.
|
enablecompletion?: number; // Completion enabled? 1: yes 0: no.
|
||||||
completionnotify?: number; // 1: yes 0: no.
|
completionnotify?: number; // 1: yes 0: no.
|
||||||
lang?: string; // Forced course language.
|
lang?: string; // Forced course language.
|
||||||
theme?: string; // Fame of the forced theme.
|
theme?: string; // Name of the forced theme.
|
||||||
marker?: number; // Current course marker.
|
marker?: number; // Current course marker.
|
||||||
legacyfiles?: number; // If legacy files are enabled.
|
legacyfiles?: number; // If legacy files are enabled.
|
||||||
calendartype?: string; // Calendar type.
|
calendartype?: string; // Calendar type.
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
// (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 { CoreHTMLClasses } from '@singletons/html-classes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General App initializer.
|
||||||
|
*/
|
||||||
|
export default async function(): Promise<void> {
|
||||||
|
CoreHTMLClasses.initialize();
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/h
|
||||||
import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2';
|
import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
import { CoreSites } from './sites';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Service to handle language features, like changing the current language.
|
* Service to handle language features, like changing the current language.
|
||||||
|
@ -380,14 +381,22 @@ export class CoreLangProvider {
|
||||||
/**
|
/**
|
||||||
* Loads custom strings obtained from site.
|
* Loads custom strings obtained from site.
|
||||||
*
|
*
|
||||||
* @param currentSite Current site object.
|
* @param currentSite Current site object. If not defined, use current site.
|
||||||
*/
|
*/
|
||||||
loadCustomStringsFromSite(currentSite: CoreSite): void {
|
loadCustomStringsFromSite(currentSite?: CoreSite): void {
|
||||||
|
currentSite = currentSite ?? CoreSites.getCurrentSite();
|
||||||
|
|
||||||
|
if (!currentSite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const customStrings = currentSite.getStoredConfig('tool_mobile_customlangstrings');
|
const customStrings = currentSite.getStoredConfig('tool_mobile_customlangstrings');
|
||||||
|
|
||||||
if (customStrings !== undefined) {
|
if (customStrings === undefined) {
|
||||||
this.loadCustomStrings(customStrings);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadCustomStrings(customStrings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,8 +15,9 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CorePlatform } from '@services/platform';
|
import { CorePlatform } from '@services/platform';
|
||||||
import { Network } from '@awesome-cordova-plugins/network/ngx';
|
import { Network } from '@awesome-cordova-plugins/network/ngx';
|
||||||
import { makeSingleton } from '@singletons';
|
import { NgZone, makeSingleton } from '@singletons';
|
||||||
import { Observable, Subject, merge } from 'rxjs';
|
import { Observable, Subject, merge } from 'rxjs';
|
||||||
|
import { CoreDomUtils } from './utils/dom';
|
||||||
|
|
||||||
export enum CoreNetworkConnection {
|
export enum CoreNetworkConnection {
|
||||||
UNKNOWN = 'unknown',
|
UNKNOWN = 'unknown',
|
||||||
|
@ -92,6 +93,40 @@ export class CoreNetworkService extends Network {
|
||||||
this.fireObservable();
|
this.fireObservable();
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.onPlaformReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the service when the platform is ready.
|
||||||
|
*/
|
||||||
|
async onPlaformReady(): Promise<void> {
|
||||||
|
await CorePlatform.ready();
|
||||||
|
|
||||||
|
// Refresh online status when changes.
|
||||||
|
CoreNetwork.onChange().subscribe(() => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.run(() => {
|
||||||
|
const isOnline = this.isOnline();
|
||||||
|
|
||||||
|
const hadOfflineMessage = CoreDomUtils.hasModeClass('core-offline');
|
||||||
|
|
||||||
|
CoreDomUtils.toggleModeClass('core-offline', !isOnline, { includeLegacy: true });
|
||||||
|
|
||||||
|
if (isOnline && hadOfflineMessage) {
|
||||||
|
CoreDomUtils.toggleModeClass('core-online', true, { includeLegacy: true });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
CoreDomUtils.toggleModeClass('core-online', false, { includeLegacy: true });
|
||||||
|
}, 3000);
|
||||||
|
} else if (!isOnline) {
|
||||||
|
CoreDomUtils.toggleModeClass('core-online', false, { includeLegacy: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOnline = this.isOnline();
|
||||||
|
CoreDomUtils.toggleModeClass('core-offline', !isOnline, { includeLegacy: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -124,8 +159,15 @@ export class CoreNetworkService extends Network {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = this.connectionType;
|
// We cannot use navigator.onLine because it has issues in some devices.
|
||||||
|
// See https://bugs.chromium.org/p/chromium/issues/detail?id=811122
|
||||||
|
if (!CorePlatform.isAndroid()) {
|
||||||
|
this.online = navigator.onLine;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = this.connectionType;
|
||||||
let online = type !== null && type !== CoreNetworkConnection.NONE && type !== CoreNetworkConnection.UNKNOWN;
|
let online = type !== null && type !== CoreNetworkConnection.NONE && type !== CoreNetworkConnection.UNKNOWN;
|
||||||
|
|
||||||
// Double check we are not online because we cannot rely 100% in Cordova APIs.
|
// Double check we are not online because we cannot rely 100% in Cordova APIs.
|
||||||
|
|
|
@ -43,7 +43,7 @@ import {
|
||||||
} from '@services/database/sites';
|
} from '@services/database/sites';
|
||||||
import { CoreArray } from '../singletons/array';
|
import { CoreArray } from '../singletons/array';
|
||||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
import { CoreRedirectPayload } from './navigator';
|
import { CoreNavigator, CoreRedirectPayload } from './navigator';
|
||||||
import { CoreSitesFactory } from './sites-factory';
|
import { CoreSitesFactory } from './sites-factory';
|
||||||
import { CoreText } from '@singletons/text';
|
import { CoreText } from '@singletons/text';
|
||||||
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
||||||
|
@ -66,6 +66,7 @@ import { CoreCacheManager } from '@services/cache-manager';
|
||||||
import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from '@classes/sites/unauthenticated-site';
|
import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from '@classes/sites/unauthenticated-site';
|
||||||
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { CoreHTMLClasses } from '@singletons/html-classes';
|
||||||
|
|
||||||
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
|
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
|
||||||
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id';
|
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id';
|
||||||
|
@ -111,6 +112,7 @@ export class CoreSitesProvider {
|
||||||
* Initialize.
|
* Initialize.
|
||||||
*/
|
*/
|
||||||
initialize(): void {
|
initialize(): void {
|
||||||
|
// Initialize general site events.
|
||||||
CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => {
|
CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => {
|
||||||
if (!siteId || !(siteId in this.siteTables)) {
|
if (!siteId || !(siteId in this.siteTables)) {
|
||||||
return;
|
return;
|
||||||
|
@ -125,6 +127,57 @@ export class CoreSitesProvider {
|
||||||
delete this.siteTables[siteId];
|
delete this.siteTables[siteId];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.LOGOUT, async () => {
|
||||||
|
// Unload lang custom strings.
|
||||||
|
CoreLang.clearCustomStrings();
|
||||||
|
|
||||||
|
// Remove version classes from body.
|
||||||
|
CoreHTMLClasses.removeSiteClasses();
|
||||||
|
|
||||||
|
// Go to sites page when user is logged out.
|
||||||
|
await CoreNavigator.navigate('/login/sites', { reset: true });
|
||||||
|
|
||||||
|
if (CoreSitePlugins.hasSitePluginsLoaded) {
|
||||||
|
// Temporary fix. Reload the page to unload all plugins.
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.LOGIN, async (data) => {
|
||||||
|
if (data.siteId) {
|
||||||
|
const site = await CoreSites.getSite(data.siteId);
|
||||||
|
const info = site.getInfo();
|
||||||
|
if (info) {
|
||||||
|
CoreHTMLClasses.addSiteClasses(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreLang.loadCustomStringsFromSite();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Site config is checked in login.
|
||||||
|
CoreEvents.on(CoreEvents.LOGIN_SITE_CHECKED, (data) => {
|
||||||
|
CoreHTMLClasses.addSiteUrlClass(data.config.httpswwwroot);
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.SITE_UPDATED, async (data) => {
|
||||||
|
if (data.siteId !== CoreSites.getCurrentSiteId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreLang.loadCustomStringsFromSite();
|
||||||
|
CoreHTMLClasses.addSiteClasses(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.SITE_ADDED, (data) => {
|
||||||
|
if (data.siteId !== CoreSites.getCurrentSiteId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreLang.loadCustomStringsFromSite();
|
||||||
|
CoreHTMLClasses.addSiteClasses(data);
|
||||||
|
});
|
||||||
|
|
||||||
CoreCacheManager.registerInvalidateListener(() => this.invalidateCaches());
|
CoreCacheManager.registerInvalidateListener(() => this.invalidateCaches());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
// (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 { CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreLang, CoreLangProvider } from '@services/lang';
|
||||||
|
|
||||||
|
import { mock, mockSingleton } from '@/testing/utils';
|
||||||
|
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { Http } from '@singletons';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { CoreSite } from '@classes/sites/site';
|
||||||
|
import { CoreHTMLClasses } from '@singletons/html-classes';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
|
describe('CoreSitesProvider', () => {
|
||||||
|
|
||||||
|
let langProvider: CoreLangProvider;
|
||||||
|
beforeEach(() => {
|
||||||
|
langProvider = mockSingleton(CoreLang, mock({ getCurrentLanguage: async () => 'en' , clearCustomStrings: () => null }));
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
mockSingleton(Http, { get: () => of(null as any) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans up on logout', async () => {
|
||||||
|
const navigator: CoreNavigatorService = mockSingleton(CoreNavigator, ['navigate']);
|
||||||
|
|
||||||
|
CoreSites.initialize();
|
||||||
|
CoreEvents.trigger(CoreEvents.LOGOUT);
|
||||||
|
|
||||||
|
expect(langProvider.clearCustomStrings).toHaveBeenCalled();
|
||||||
|
expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds ionic platform and theme classes', async () => {
|
||||||
|
const siteUrl = 'https://campus.example.edu';
|
||||||
|
const themeName = 'mytheme';
|
||||||
|
const themeName2 = 'anothertheme';
|
||||||
|
|
||||||
|
CoreHTMLClasses.initialize();
|
||||||
|
CoreSites.initialize();
|
||||||
|
|
||||||
|
expect(document.documentElement.classList.contains('ionic7')).toBe(true);
|
||||||
|
|
||||||
|
const site = mock(new CoreSite('42', siteUrl, 'token', { info: {
|
||||||
|
sitename: 'Example Campus',
|
||||||
|
username: 'admin',
|
||||||
|
firstname: 'Admin',
|
||||||
|
lastname: 'User',
|
||||||
|
fullname: 'Admin User',
|
||||||
|
lang: 'en',
|
||||||
|
userid: 1,
|
||||||
|
siteurl: siteUrl,
|
||||||
|
userpictureurl: '',
|
||||||
|
theme: themeName,
|
||||||
|
functions: [],
|
||||||
|
} }));
|
||||||
|
|
||||||
|
mockSingleton(CoreSites, {
|
||||||
|
getSite: () => Promise.resolve(site),
|
||||||
|
getCurrentSiteId: () => '42',
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreEvents.LOGIN, {}, '42');
|
||||||
|
// Wait the event to be processed.
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(true);
|
||||||
|
expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(false);
|
||||||
|
|
||||||
|
if (site.infos) {
|
||||||
|
site.infos.theme = themeName2;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreEvents.SITE_UPDATED, site.infos , '42');
|
||||||
|
|
||||||
|
// Wait the event to be processed.
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(true);
|
||||||
|
expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(false);
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreEvents.LOGOUT);
|
||||||
|
|
||||||
|
expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(false);
|
||||||
|
expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(false);
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreEvents.SITE_ADDED, site.infos , '42');
|
||||||
|
|
||||||
|
// Wait the event to be processed.
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
expect(document.documentElement.classList.contains('theme-site-'+themeName2)).toBe(true);
|
||||||
|
expect(document.documentElement.classList.contains('theme-site-'+themeName)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,163 @@
|
||||||
|
// (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 { CoreSiteInfo, CoreSiteInfoResponse } from '@classes/sites/unauthenticated-site';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUrl } from './url';
|
||||||
|
import { CoreConstants } from '../constants';
|
||||||
|
import { ScrollDetail } from '@ionic/angular';
|
||||||
|
import { CoreDom } from './dom';
|
||||||
|
|
||||||
|
const MOODLE_SITE_URL_PREFIX = 'url-';
|
||||||
|
const MOODLE_VERSION_PREFIX = 'version-';
|
||||||
|
const MOODLEAPP_VERSION_PREFIX = 'moodleapp-';
|
||||||
|
const MOODLE_SITE_THEME_PREFIX = 'theme-site-';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton with helper functions to manage HTML classes.
|
||||||
|
*/
|
||||||
|
export class CoreHTMLClasses {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize HTML classes.
|
||||||
|
*/
|
||||||
|
static initialize(): void {
|
||||||
|
CoreDomUtils.toggleModeClass('ionic7', true);
|
||||||
|
CoreDomUtils.toggleModeClass('development', CoreConstants.BUILD.isDevelopment);
|
||||||
|
CoreHTMLClasses.addVersionClass(MOODLEAPP_VERSION_PREFIX, CoreConstants.CONFIG.versionname.replace('-dev', ''));
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const win = <any> window;
|
||||||
|
|
||||||
|
// Listen to scroll to add style when scroll is not 0.
|
||||||
|
win.addEventListener('ionScroll', async ({ detail, target }: CustomEvent<ScrollDetail>) => {
|
||||||
|
if ((target as HTMLElement).tagName != 'ION-CONTENT') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = (target as HTMLIonContentElement);
|
||||||
|
|
||||||
|
const page = content.closest('.ion-page');
|
||||||
|
if (!page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
page.querySelector<HTMLIonHeaderElement>('ion-header')?.classList.toggle('core-header-shadow', detail.scrollTop > 0);
|
||||||
|
|
||||||
|
const scrollElement = await content.getScrollElement();
|
||||||
|
content.classList.toggle('core-footer-shadow', !CoreDom.scrollIsBottom(scrollElement));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to add version to html classes.
|
||||||
|
*
|
||||||
|
* @param prefix Prefix to add to the class.
|
||||||
|
* @param release Current release number of the site.
|
||||||
|
*/
|
||||||
|
static addVersionClass(prefix: string, release: string): void {
|
||||||
|
const parts = release.split('.', 3);
|
||||||
|
|
||||||
|
parts[1] = parts[1] || '0';
|
||||||
|
parts[2] = parts[2] || '0';
|
||||||
|
|
||||||
|
CoreDomUtils.toggleModeClass(prefix + parts[0], true, { includeLegacy: true });
|
||||||
|
CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1], true, { includeLegacy: true });
|
||||||
|
CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1] + '-' + parts[2], true, { includeLegacy: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to remove all mode classes form body.
|
||||||
|
*
|
||||||
|
* @param prefixes Prefixes of the class mode to be removed.
|
||||||
|
*/
|
||||||
|
protected static removeModeClasses(prefixes: string[]): void {
|
||||||
|
for (const modeClass of CoreDomUtils.getModeClasses()) {
|
||||||
|
if (!prefixes.some((prefix) => modeClass.startsWith(prefix))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.toggleModeClass(modeClass, false, { includeLegacy: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to add site classes to html.
|
||||||
|
*
|
||||||
|
* @param siteInfo Site Info.
|
||||||
|
*/
|
||||||
|
static addSiteClasses(siteInfo: CoreSiteInfo | CoreSiteInfoResponse): void {
|
||||||
|
// Add version classes to html tag.
|
||||||
|
this.removeSiteClasses();
|
||||||
|
|
||||||
|
this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(siteInfo.release || ''));
|
||||||
|
this.addSiteUrlClass(siteInfo.siteurl);
|
||||||
|
|
||||||
|
if (siteInfo.theme) {
|
||||||
|
CoreDomUtils.toggleModeClass(MOODLE_SITE_THEME_PREFIX + siteInfo.theme, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to remove all site mode classes form html.
|
||||||
|
*/
|
||||||
|
static removeSiteClasses(): void {
|
||||||
|
// Remove version classes from html tag.
|
||||||
|
this.removeModeClasses(
|
||||||
|
[MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX, MOODLE_SITE_THEME_PREFIX],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the provided URL into a CSS class that be used within the page.
|
||||||
|
* This is primarily used to add the siteurl to the body tag as a CSS class.
|
||||||
|
* Extracted from LMS url_to_class_name function.
|
||||||
|
*
|
||||||
|
* @param url Url.
|
||||||
|
* @returns Class name
|
||||||
|
*/
|
||||||
|
protected static urlToClassName(url: string): string {
|
||||||
|
const parsedUrl = CoreUrl.parse(url);
|
||||||
|
|
||||||
|
if (!parsedUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let className = parsedUrl.domain?.replace(/\./g, '-') || '';
|
||||||
|
|
||||||
|
if (parsedUrl.port) {
|
||||||
|
className += `--${parsedUrl.port}`;
|
||||||
|
}
|
||||||
|
if (parsedUrl.path) {
|
||||||
|
const leading = new RegExp('^/+');
|
||||||
|
const trailing = new RegExp('/+$');
|
||||||
|
const path = parsedUrl.path.replace(leading, '').replace(trailing, '');
|
||||||
|
if (path) {
|
||||||
|
className += '--' + path.replace(/\//g, '-') || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to add site url to html classes.
|
||||||
|
*/
|
||||||
|
static addSiteUrlClass(siteUrl: string): void {
|
||||||
|
const className = this.urlToClassName(siteUrl);
|
||||||
|
|
||||||
|
CoreDomUtils.toggleModeClass(MOODLE_SITE_URL_PREFIX + className, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue