forked from CIT/Vmeda.Online
		
	MOBILE-3670 remotethemes: Create a general framework for styling the app
This commit is contained in:
		
							parent
							
								
									2f21241a47
								
							
						
					
					
						commit
						72b6057783
					
				@ -12,13 +12,9 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
 | 
			
		||||
import { AddonRemoteThemes, AddonRemoteThemesProvider } from './services/remotethemes';
 | 
			
		||||
 | 
			
		||||
// List of providers (without handlers).
 | 
			
		||||
export const ADDON_REMOTETHEMES_SERVICES: Type<unknown>[] = [
 | 
			
		||||
    AddonRemoteThemesProvider,
 | 
			
		||||
];
 | 
			
		||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
 | 
			
		||||
import { CoreStyles } from '@features/styles/services/styles';
 | 
			
		||||
import { AddonRemoteThemesHandler } from './services/remotethemes-handler';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    providers: [
 | 
			
		||||
@ -27,7 +23,7 @@ export const ADDON_REMOTETHEMES_SERVICES: Type<unknown>[] = [
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => async () => {
 | 
			
		||||
                await AddonRemoteThemes.initialize();
 | 
			
		||||
                CoreStyles.registerStyleHandler(AddonRemoteThemesHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										148
									
								
								src/addons/remotethemes/services/remotethemes-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								src/addons/remotethemes/services/remotethemes-handler.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,148 @@
 | 
			
		||||
// (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 { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreSitePublicConfigResponse } from '@classes/site';
 | 
			
		||||
import { CoreFile } from '@services/file';
 | 
			
		||||
import { CoreFilepool } from '@services/filepool';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreWS } from '@services/ws';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreStyleHandler, CoreStylesService } from '@features/styles/services/styles';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
 | 
			
		||||
const SEPARATOR_35 = /\/\*\*? *3\.5(\.0)? *styles? *\*\//i; // A comment like "/* 3.5 styles */".
 | 
			
		||||
const COMPONENT = 'mmaRemoteStyles';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to handle remote themes.
 | 
			
		||||
 * A remote theme is a CSS sheet stored in the site that allows customising the Mobile app.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonRemoteThemesHandlerService implements CoreStyleHandler {
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
 | 
			
		||||
    name = 'mobilecssurl';
 | 
			
		||||
    priority = 1000;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.logger = CoreLogger.getInstance('AddonRemoteThemes');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritDoc
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    async isEnabled(siteId: string, config?: CoreSitePublicConfigResponse): Promise<boolean> {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritDoc
 | 
			
		||||
     */
 | 
			
		||||
    async getStyle(siteId: string, config?: CoreSitePublicConfigResponse): Promise<string> {
 | 
			
		||||
        if (siteId == CoreStylesService.TMP_SITE_ID) {
 | 
			
		||||
            if (!config) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Config received, it's a temp site.
 | 
			
		||||
            return await this.get35Styles(config.mobilecssurl);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
        const infos = site.getInfo();
 | 
			
		||||
 | 
			
		||||
        if (!infos?.mobilecssurl) {
 | 
			
		||||
            if (infos?.mobilecssurl === '') {
 | 
			
		||||
                // CSS URL is empty. Delete downloaded files (if any).
 | 
			
		||||
                CoreFilepool.removeFilesByComponent(siteId, COMPONENT, 1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let fileUrl = infos.mobilecssurl;
 | 
			
		||||
 | 
			
		||||
        if (CoreFile.isAvailable()) {
 | 
			
		||||
            // The file system is available. Download the file and remove old CSS files if needed.
 | 
			
		||||
            fileUrl = await this.downloadFileAndRemoveOld(siteId, fileUrl);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Loading styles from: ', fileUrl);
 | 
			
		||||
 | 
			
		||||
        // Get the CSS content using HTTP because we will treat the styles before saving them in the file.
 | 
			
		||||
        const style = await this.get35Styles(fileUrl);
 | 
			
		||||
 | 
			
		||||
        if (style != '') {
 | 
			
		||||
            // Treat the CSS.
 | 
			
		||||
            CoreUtils.ignoreErrors(
 | 
			
		||||
                CoreFilepool.treatCSSCode(siteId, fileUrl, style, COMPONENT, 2),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return style;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the CSS code has a separator for 3.5 styles. If it does, get only the styles after the separator.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url Url to get the code from.
 | 
			
		||||
     * @return The filtered styles.
 | 
			
		||||
     */
 | 
			
		||||
    protected async get35Styles(url?: string): Promise<string> {
 | 
			
		||||
        if (!url) {
 | 
			
		||||
            return '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const cssCode = await CoreWS.getText(url);
 | 
			
		||||
 | 
			
		||||
        const separatorPos = cssCode.search(SEPARATOR_35);
 | 
			
		||||
        if (separatorPos > -1) {
 | 
			
		||||
            return cssCode.substr(separatorPos).replace(SEPARATOR_35, '');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return cssCode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Downloads a CSS file and remove old files if needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @param url File URL.
 | 
			
		||||
     * @return Promise resolved when the file is downloaded.
 | 
			
		||||
     */
 | 
			
		||||
    protected async downloadFileAndRemoveOld(siteId: string, url: string): Promise<string> {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Check if the file is downloaded.
 | 
			
		||||
            const state = await CoreFilepool.getFileStateByUrl(siteId, url);
 | 
			
		||||
 | 
			
		||||
            if (state == CoreConstants.NOT_DOWNLOADED) {
 | 
			
		||||
                // File not downloaded, URL has changed or first time. Delete downloaded CSS files.
 | 
			
		||||
                await CoreFilepool.removeFilesByComponent(siteId, COMPONENT, 1);
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // An error occurred while getting state (shouldn't happen). Don't delete downloaded file.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CoreFilepool.downloadUrl(siteId, url, false, COMPONENT, 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonRemoteThemesHandler = makeSingleton(AddonRemoteThemesHandlerService);
 | 
			
		||||
@ -63,6 +63,7 @@ import { CORE_SEARCH_SERVICES } from '@features/search/search.module';
 | 
			
		||||
import { CORE_SETTINGS_SERVICES } from '@features/settings/settings.module';
 | 
			
		||||
import { CORE_SITEHOME_SERVICES } from '@features/sitehome/sitehome.module';
 | 
			
		||||
import { CORE_TAG_SERVICES } from '@features/tag/tag.module';
 | 
			
		||||
import { CORE_STYLE_SERVICES } from '@features/styles/styles.module';
 | 
			
		||||
import { CORE_USER_SERVICES } from '@features/user/user.module';
 | 
			
		||||
import { CORE_XAPI_SERVICES } from '@features/xapi/xapi.module';
 | 
			
		||||
import { CoreSitePluginsProvider } from '@features/siteplugins/services/siteplugins';
 | 
			
		||||
@ -146,7 +147,6 @@ import { ADDON_MOD_WORKSHOP_SERVICES } from '@addons/mod/workshop/workshop.modul
 | 
			
		||||
import { ADDON_NOTES_SERVICES } from '@addons/notes/notes.module';
 | 
			
		||||
import { ADDON_NOTIFICATIONS_SERVICES } from '@addons/notifications/notifications.module';
 | 
			
		||||
import { ADDON_PRIVATEFILES_SERVICES } from '@addons/privatefiles/privatefiles.module';
 | 
			
		||||
import { ADDON_REMOTETHEMES_SERVICES } from '@addons/remotethemes/remotethemes.module';
 | 
			
		||||
 | 
			
		||||
// Import some addon modules that define components, directives and pipes. Only import the important ones.
 | 
			
		||||
import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module';
 | 
			
		||||
@ -277,6 +277,7 @@ export class CoreCompileProvider {
 | 
			
		||||
            ...CORE_SITEHOME_SERVICES,
 | 
			
		||||
            CoreSitePluginsProvider,
 | 
			
		||||
            ...CORE_TAG_SERVICES,
 | 
			
		||||
            ...CORE_STYLE_SERVICES,
 | 
			
		||||
            ...CORE_USER_SERVICES,
 | 
			
		||||
            ...CORE_XAPI_SERVICES,
 | 
			
		||||
            ...IONIC_NATIVE_SERVICES,
 | 
			
		||||
@ -311,7 +312,6 @@ export class CoreCompileProvider {
 | 
			
		||||
            ...ADDON_NOTES_SERVICES,
 | 
			
		||||
            ...ADDON_NOTIFICATIONS_SERVICES,
 | 
			
		||||
            ...ADDON_PRIVATEFILES_SERVICES,
 | 
			
		||||
            ...ADDON_REMOTETHEMES_SERVICES,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ import { CoreSettingsModule } from './settings/settings.module';
 | 
			
		||||
import { CoreSharedFilesModule } from './sharedfiles/sharedfiles.module';
 | 
			
		||||
import { CoreSiteHomeModule } from './sitehome/sitehome.module';
 | 
			
		||||
import { CoreSitePluginsModule } from './siteplugins/siteplugins.module';
 | 
			
		||||
import { CoreStylesModule } from './styles/styles.module';
 | 
			
		||||
import { CoreTagModule } from './tag/tag.module';
 | 
			
		||||
import { CoreUserModule } from './user/user.module';
 | 
			
		||||
import { CoreViewerModule } from './viewer/viewer.module';
 | 
			
		||||
@ -64,6 +65,7 @@ import { CoreXAPIModule } from './xapi/xapi.module';
 | 
			
		||||
        CoreSiteHomeModule,
 | 
			
		||||
        CoreSitePluginsModule,
 | 
			
		||||
        CoreTagModule,
 | 
			
		||||
        CoreStylesModule,
 | 
			
		||||
        CoreUserModule,
 | 
			
		||||
        CoreViewerModule,
 | 
			
		||||
        CoreXAPIModule,
 | 
			
		||||
 | 
			
		||||
@ -13,41 +13,74 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { Md5 } from 'ts-md5/dist/md5';
 | 
			
		||||
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreSitePublicConfigResponse } from '@classes/site';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreFile } from '@services/file';
 | 
			
		||||
import { CoreFilepool } from '@services/filepool';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreWS } from '@services/ws';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
 | 
			
		||||
const SEPARATOR_35 = /\/\*\*? *3\.5(\.0)? *styles? *\*\//i; // A comment like "/* 3.5 styles */".
 | 
			
		||||
export const TMP_SITE_ID = 'tmpsite';
 | 
			
		||||
import { Md5 } from 'ts-md5';
 | 
			
		||||
import { CoreLogger } from '../../../singletons/logger';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to handle remote themes. A remote theme is a CSS sheet stored in the site that allows customising the Mobile app.
 | 
			
		||||
 * Interface that all style handlers must implement.
 | 
			
		||||
 */
 | 
			
		||||
export interface CoreStyleHandler {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Source name.
 | 
			
		||||
     */
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Priority of application.
 | 
			
		||||
     */
 | 
			
		||||
    priority: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wether the handler should be enabled for the site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site Id.
 | 
			
		||||
     * @param config Site public config for temp sites.
 | 
			
		||||
     * @return Wether the handler should be enabled for the site.
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled(siteId: string, config?: CoreSitePublicConfigResponse): boolean | Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the style for the site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site Id.
 | 
			
		||||
     * @param config Site public config for temp sites.
 | 
			
		||||
     * @return CSS to apply.
 | 
			
		||||
     */
 | 
			
		||||
    getStyle(siteId?: string, config?: CoreSitePublicConfigResponse): string | Promise<string>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Singleton with helper functions to style the app.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonRemoteThemesProvider {
 | 
			
		||||
 | 
			
		||||
    static readonly COMPONENT = 'mmaRemoteStyles';
 | 
			
		||||
export class CoreStylesService {
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected stylesEls: {[siteId: string]: { element: HTMLStyleElement; hash: string }} = {};
 | 
			
		||||
 | 
			
		||||
    protected stylesEls: {
 | 
			
		||||
        [siteId: string]: {
 | 
			
		||||
            [sourceName: string]: string; // Hashes
 | 
			
		||||
        };
 | 
			
		||||
    } = {};
 | 
			
		||||
 | 
			
		||||
    protected styleHandlers: CoreStyleHandler[] = [];
 | 
			
		||||
 | 
			
		||||
    static readonly TMP_SITE_ID = 'tmpsite';
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.logger = CoreLogger.getInstance('AddonRemoteThemes');
 | 
			
		||||
        this.logger = CoreLogger.getInstance('CoreStyles');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize remote themes.
 | 
			
		||||
     * Initialize styles.
 | 
			
		||||
     */
 | 
			
		||||
    async initialize(): Promise<void> {
 | 
			
		||||
        this.listenEvents();
 | 
			
		||||
@ -59,6 +92,18 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
        await this.preloadSites();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a new style handler.
 | 
			
		||||
     *
 | 
			
		||||
     * @param styleHandler Style handler to be registered.
 | 
			
		||||
     */
 | 
			
		||||
    registerStyleHandler(styleHandler: CoreStyleHandler): void {
 | 
			
		||||
        this.styleHandlers.push(styleHandler);
 | 
			
		||||
 | 
			
		||||
        // Sort them by priority, greatest go last because style loaded last it's more important.
 | 
			
		||||
        this.styleHandlers = this.styleHandlers.sort((a, b) => a.priority! >= b.priority! ? 1 : -1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Listen events.
 | 
			
		||||
     */
 | 
			
		||||
@ -79,10 +124,10 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
                // User has logged in, remove tmp styles and enable loaded styles.
 | 
			
		||||
                if (data.siteId == CoreSites.getCurrentSiteId()) {
 | 
			
		||||
                    this.unloadTmpStyles();
 | 
			
		||||
                    this.enable(data.siteId);
 | 
			
		||||
                    this.enableSiteStyles(data.siteId);
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                this.logger.error('Error adding remote styles for new site', error);
 | 
			
		||||
                this.logger.error('Error adding styles for new site', error);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -98,7 +143,7 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
        // Enable styles of current site on login.
 | 
			
		||||
        CoreEvents.on(CoreEvents.LOGIN, (data) => {
 | 
			
		||||
            this.unloadTmpStyles();
 | 
			
		||||
            this.enable(data.siteId);
 | 
			
		||||
            this.enableSiteStyles(data.siteId);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Disable added styles on logout.
 | 
			
		||||
@ -113,7 +158,7 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
 | 
			
		||||
        // Load temporary styles when site config is checked in login.
 | 
			
		||||
        CoreEvents.on(CoreEvents.LOGIN_SITE_CHECKED, (data) => {
 | 
			
		||||
            this.loadTmpStylesForSiteConfig(data.config).catch((error) => {
 | 
			
		||||
            this.loadTmpStyles(data.config).catch((error) => {
 | 
			
		||||
                this.logger.error('Error loading tmp styles', error);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
@ -131,20 +176,94 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a style element for a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site Id.
 | 
			
		||||
     * @param disabled Whether the element should be disabled.
 | 
			
		||||
     */
 | 
			
		||||
    protected createStyleElements(siteId: string, disabled: boolean): void {
 | 
			
		||||
        this.stylesEls[siteId] = {};
 | 
			
		||||
 | 
			
		||||
        this.styleHandlers.forEach((handler) => {
 | 
			
		||||
 | 
			
		||||
            const styleElementId = this.getStyleId(siteId, handler.name);
 | 
			
		||||
 | 
			
		||||
            let styleEl: HTMLStyleElement | null = document.head.querySelector(`style#${styleElementId}`);
 | 
			
		||||
 | 
			
		||||
            if (!styleEl) {
 | 
			
		||||
                // Create the style and add it to the header.
 | 
			
		||||
                styleEl = document.createElement('style');
 | 
			
		||||
 | 
			
		||||
                styleEl.setAttribute('id', styleElementId);
 | 
			
		||||
                this.disableStyleElement(styleEl, disabled);
 | 
			
		||||
 | 
			
		||||
                this.stylesEls[siteId][handler.name] = '';
 | 
			
		||||
                document.head.appendChild(styleEl);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the content of an style element.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site Id.
 | 
			
		||||
     * @param handler Style handler.
 | 
			
		||||
     * @param disabled Whether the element should be disabled.
 | 
			
		||||
     * @param config Site public config.
 | 
			
		||||
     * @return New element.
 | 
			
		||||
     */
 | 
			
		||||
    protected async setStyle(
 | 
			
		||||
        siteId: string,
 | 
			
		||||
        handler: CoreStyleHandler,
 | 
			
		||||
        disabled: boolean,
 | 
			
		||||
        config?: CoreSitePublicConfigResponse,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        let contents = '';
 | 
			
		||||
 | 
			
		||||
        const enabled = await handler.isEnabled(siteId, config);
 | 
			
		||||
        if (enabled) {
 | 
			
		||||
            contents = (await handler.getStyle(siteId, config)).trim();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const hash = <string>Md5.hashAsciiStr(contents);
 | 
			
		||||
 | 
			
		||||
        // Update the styles only if they have changed.
 | 
			
		||||
        if (this.stylesEls[siteId!][handler.name] === hash) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const styleElementId = this.getStyleId(siteId, handler.name);
 | 
			
		||||
 | 
			
		||||
        const styleEl: HTMLStyleElement | null = document.head.querySelector(`style#${styleElementId}`);
 | 
			
		||||
 | 
			
		||||
        if (!styleEl) {
 | 
			
		||||
            this.stylesEls[siteId][handler.name] = '';
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        styleEl.innerHTML = contents;
 | 
			
		||||
        this.stylesEls[siteId][handler.name] = hash;
 | 
			
		||||
 | 
			
		||||
        // Adding styles to a style element automatically enables it. Disable it again if needed.
 | 
			
		||||
        this.disableStyleElement(styleEl, disabled);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a style element for a site and load the styles for that element. The style will be disabled.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved when added and loaded.
 | 
			
		||||
     */
 | 
			
		||||
    async addSite(siteId?: string): Promise<void> {
 | 
			
		||||
    protected async addSite(siteId?: string): Promise<void> {
 | 
			
		||||
        if (!siteId || this.stylesEls[siteId]) {
 | 
			
		||||
            // Invalid site ID or style already added.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create the style and add it to the header.
 | 
			
		||||
        this.initSiteStyleElement(siteId, true);
 | 
			
		||||
        this.createStyleElements(siteId, true);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.load(siteId, true);
 | 
			
		||||
@ -156,28 +275,47 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Clear styles added to the DOM, disabling them all.
 | 
			
		||||
     */
 | 
			
		||||
    clear(): void {
 | 
			
		||||
    protected clear(): void {
 | 
			
		||||
        let styles: HTMLStyleElement[] = [];
 | 
			
		||||
        // Disable all the styles.
 | 
			
		||||
        this.disableElementsBySelector('style[id*=mobilecssurl]');
 | 
			
		||||
        this.styleHandlers.forEach((handler) => {
 | 
			
		||||
            styles = styles.concat(Array.from(document.querySelectorAll(`style[id*=${handler.name}]`)));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        styles.forEach((style) => {
 | 
			
		||||
            this.disableStyleElement(style, true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Set StatusBar properties.
 | 
			
		||||
        CoreApp.setStatusBarColor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a style element.
 | 
			
		||||
     * Returns style element Id based on site and source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id ID to set to the element.
 | 
			
		||||
     * @param disabled Whether the element should be disabled.
 | 
			
		||||
     * @return New element.
 | 
			
		||||
     * @param siteId Site Id.
 | 
			
		||||
     * @param sourceName Source or handler name.
 | 
			
		||||
     * @return Element Id.
 | 
			
		||||
     */
 | 
			
		||||
    protected createStyleElement(id: string, disabled: boolean): HTMLStyleElement {
 | 
			
		||||
        const styleEl = document.createElement('style');
 | 
			
		||||
    protected getStyleId(siteId: string, sourceName: string): string {
 | 
			
		||||
        return `${sourceName}-${siteId}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        styleEl.setAttribute('id', id);
 | 
			
		||||
        this.disableElement(styleEl, disabled);
 | 
			
		||||
    /**
 | 
			
		||||
     * Disabled an element based on site and source name.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site Id.
 | 
			
		||||
     * @param sourceName Source or handler name.
 | 
			
		||||
     * @param disable Whether to disable or enable the element.
 | 
			
		||||
     */
 | 
			
		||||
    protected disableStyleElementByName(siteId: string, sourceName: string, disable: boolean): void {
 | 
			
		||||
        const styleElementId = this.getStyleId(siteId, sourceName);
 | 
			
		||||
 | 
			
		||||
        return styleEl;
 | 
			
		||||
        const styleEl: HTMLStyleElement | null = document.head.querySelector(`style#${styleElementId}`);
 | 
			
		||||
 | 
			
		||||
        if (styleEl) {
 | 
			
		||||
            this.disableStyleElement(styleEl, disable);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -186,7 +324,7 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
     * @param element The element to enable or disable.
 | 
			
		||||
     * @param disable Whether to disable or enable the element.
 | 
			
		||||
     */
 | 
			
		||||
    disableElement(element: HTMLStyleElement, disable: boolean): void {
 | 
			
		||||
    protected disableStyleElement(element: HTMLStyleElement, disable: boolean): void {
 | 
			
		||||
        // Setting disabled should be enough, but we also set the attribute so it can be seen in the DOM which ones are disabled.
 | 
			
		||||
        // Cast to any because the HTMLStyleElement type doesn't define the disabled attribute.
 | 
			
		||||
        (<any> element).disabled = !!disable; // eslint-disable-line @typescript-eslint/no-explicit-any
 | 
			
		||||
@ -195,135 +333,24 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
            element.setAttribute('disabled', 'true');
 | 
			
		||||
        } else {
 | 
			
		||||
            element.removeAttribute('disabled');
 | 
			
		||||
 | 
			
		||||
            if (element.innerHTML != '') {
 | 
			
		||||
                CoreApp.setStatusBarColor();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Disable all the style elements based on a query selector.
 | 
			
		||||
     *
 | 
			
		||||
     * @param selector The selector to get the style elements.
 | 
			
		||||
     */
 | 
			
		||||
    protected disableElementsBySelector(selector: string): void {
 | 
			
		||||
        const styles = <HTMLStyleElement[]> Array.from(document.querySelectorAll(selector));
 | 
			
		||||
 | 
			
		||||
        styles.forEach((style) => {
 | 
			
		||||
            this.disableElement(style, true);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Downloads a CSS file and remove old files if needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @param url File URL.
 | 
			
		||||
     * @return Promise resolved when the file is downloaded.
 | 
			
		||||
     */
 | 
			
		||||
    protected async downloadFileAndRemoveOld(siteId: string, url: string): Promise<string> {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Check if the file is downloaded.
 | 
			
		||||
            const state = await CoreFilepool.getFileStateByUrl(siteId, url);
 | 
			
		||||
 | 
			
		||||
            if (state == CoreConstants.NOT_DOWNLOADED) {
 | 
			
		||||
                // File not downloaded, URL has changed or first time. Delete downloaded CSS files.
 | 
			
		||||
                await CoreFilepool.removeFilesByComponent(siteId, AddonRemoteThemesProvider.COMPONENT, 1);
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // An error occurred while getting state (shouldn't happen). Don't delete downloaded file.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CoreFilepool.downloadUrl(siteId, url, false, AddonRemoteThemesProvider.COMPONENT, 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Enable the styles of a certain site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     */
 | 
			
		||||
    enable(siteId?: string): void {
 | 
			
		||||
    protected enableSiteStyles(siteId?: string): void {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (this.stylesEls[siteId]) {
 | 
			
		||||
            this.disableElement(this.stylesEls[siteId].element, false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get remote styles of a certain site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the styles and the URL of the CSS file,
 | 
			
		||||
     *         resolved with undefined if no styles to load.
 | 
			
		||||
     */
 | 
			
		||||
    async get(siteId?: string): Promise<{fileUrl: string; styles: string} | undefined> {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
        const infos = site.getInfo();
 | 
			
		||||
 | 
			
		||||
        if (!infos?.mobilecssurl) {
 | 
			
		||||
            if (infos?.mobilecssurl === '') {
 | 
			
		||||
                // CSS URL is empty. Delete downloaded files (if any).
 | 
			
		||||
                CoreFilepool.removeFilesByComponent(siteId, AddonRemoteThemesProvider.COMPONENT, 1);
 | 
			
		||||
            for (const sourceName in this.stylesEls[siteId]) {
 | 
			
		||||
                this.disableStyleElementByName(siteId, sourceName, false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
            CoreApp.setStatusBarColor();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let fileUrl = infos.mobilecssurl;
 | 
			
		||||
 | 
			
		||||
        if (CoreFile.isAvailable()) {
 | 
			
		||||
            // The file system is available. Download the file and remove old CSS files if needed.
 | 
			
		||||
            fileUrl = await this.downloadFileAndRemoveOld(siteId, fileUrl);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Loading styles from: ', fileUrl);
 | 
			
		||||
 | 
			
		||||
        // Get the CSS content using HTTP because we will treat the styles before saving them in the file.
 | 
			
		||||
        const text = await CoreWS.getText(fileUrl);
 | 
			
		||||
 | 
			
		||||
        return { fileUrl, styles: this.get35Styles(text) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the CSS code has a separator for 3.5 styles. If it does, get only the styles after the separator.
 | 
			
		||||
     *
 | 
			
		||||
     * @param cssCode The CSS code to check.
 | 
			
		||||
     * @return The filtered styles.
 | 
			
		||||
     */
 | 
			
		||||
    protected get35Styles(cssCode: string): string {
 | 
			
		||||
        const separatorPos = cssCode.search(SEPARATOR_35);
 | 
			
		||||
        if (separatorPos > -1) {
 | 
			
		||||
            return cssCode.substr(separatorPos).replace(SEPARATOR_35, '');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return cssCode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Init the style element for a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @param disabled Whether the element should be disabled.
 | 
			
		||||
     */
 | 
			
		||||
    protected initSiteStyleElement(siteId: string, disabled: boolean): void {
 | 
			
		||||
        if (this.stylesEls[siteId]) {
 | 
			
		||||
            // Already initialized, ignore.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create the style and add it to the header.
 | 
			
		||||
        const styleEl = this.createStyleElement('mobilecssurl-' + siteId, disabled);
 | 
			
		||||
 | 
			
		||||
        document.head.appendChild(styleEl);
 | 
			
		||||
        this.stylesEls[siteId] = {
 | 
			
		||||
            element: styleEl,
 | 
			
		||||
            hash: '',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -333,67 +360,28 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
     * @param disabled Whether loaded styles should be disabled.
 | 
			
		||||
     * @return Promise resolved when styles are loaded.
 | 
			
		||||
     */
 | 
			
		||||
    async load(siteId?: string, disabled?: boolean): Promise<void> {
 | 
			
		||||
    protected async load(siteId?: string, disabled = false): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
        disabled = !!disabled;
 | 
			
		||||
 | 
			
		||||
        if (!siteId || !this.stylesEls[siteId]) {
 | 
			
		||||
            throw new CoreError('Cannot load remote styles, site not found: ${siteId}');
 | 
			
		||||
            throw new CoreError('Cannot load styles, site not found: ${siteId}');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Load site', siteId, disabled);
 | 
			
		||||
 | 
			
		||||
        // Enable or disable the styles.
 | 
			
		||||
        this.disableElement(this.stylesEls[siteId].element, disabled);
 | 
			
		||||
 | 
			
		||||
        const data = await this.get(siteId);
 | 
			
		||||
 | 
			
		||||
        if (typeof data == 'undefined') {
 | 
			
		||||
            // Nothing to load.
 | 
			
		||||
            return;
 | 
			
		||||
        for (const sourceName in this.stylesEls[siteId]) {
 | 
			
		||||
            this.disableStyleElementByName(siteId, sourceName, disabled);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const hash = <string> Md5.hashAsciiStr(data.styles);
 | 
			
		||||
        await CoreUtils.allPromises(this.styleHandlers.map(async (handler) => {
 | 
			
		||||
            await this.setStyle(siteId!, handler, !!disabled);
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Update the styles only if they have changed.
 | 
			
		||||
        if (this.stylesEls[siteId].hash !== hash) {
 | 
			
		||||
            this.stylesEls[siteId].element.innerHTML = data.styles;
 | 
			
		||||
            this.stylesEls[siteId].hash = hash;
 | 
			
		||||
 | 
			
		||||
            // Adding styles to a style element automatically enables it. Disable it again.
 | 
			
		||||
            if (disabled) {
 | 
			
		||||
                this.disableElement(this.stylesEls[siteId].element, true);
 | 
			
		||||
            } else {
 | 
			
		||||
                // Set StatusBar properties.
 | 
			
		||||
                CoreApp.setStatusBarColor();
 | 
			
		||||
            }
 | 
			
		||||
        if (!disabled) {
 | 
			
		||||
            // Set StatusBar properties.
 | 
			
		||||
            CoreApp.setStatusBarColor();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Styles have been loaded, now treat the CSS.
 | 
			
		||||
        CoreUtils.ignoreErrors(
 | 
			
		||||
            CoreFilepool.treatCSSCode(siteId, data.fileUrl, data.styles, AddonRemoteThemesProvider.COMPONENT, 2),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load styles for a temporary site. These styles aren't prefetched.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url URL to get the styles from.
 | 
			
		||||
     * @return Promise resolved when loaded.
 | 
			
		||||
     */
 | 
			
		||||
    async loadTmpStyles(url?: string): Promise<void> {
 | 
			
		||||
        if (!url) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let text = await CoreWS.getText(url);
 | 
			
		||||
 | 
			
		||||
        text = this.get35Styles(text);
 | 
			
		||||
 | 
			
		||||
        this.initSiteStyleElement(TMP_SITE_ID, false);
 | 
			
		||||
        this.stylesEls[TMP_SITE_ID].element.innerHTML = text;
 | 
			
		||||
 | 
			
		||||
        CoreApp.setStatusBarColor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -402,8 +390,15 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
     * @param config Site public config.
 | 
			
		||||
     * @return Promise resolved when loaded.
 | 
			
		||||
     */
 | 
			
		||||
    loadTmpStylesForSiteConfig(config: CoreSitePublicConfigResponse): Promise<void> {
 | 
			
		||||
        return this.loadTmpStyles(config.mobilecssurl);
 | 
			
		||||
    protected async loadTmpStyles(config: CoreSitePublicConfigResponse): Promise<void> {
 | 
			
		||||
        // Create the style and add it to the header.
 | 
			
		||||
        this.createStyleElements(CoreStylesService.TMP_SITE_ID, true);
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.allPromises(this.styleHandlers.map(async (handler) => {
 | 
			
		||||
            await this.setStyle(CoreStylesService.TMP_SITE_ID, handler, false, config);
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        CoreApp.setStatusBarColor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -411,7 +406,7 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when loaded.
 | 
			
		||||
     */
 | 
			
		||||
    async preloadCurrentSite(): Promise<void> {
 | 
			
		||||
    protected async preloadCurrentSite(): Promise<void> {
 | 
			
		||||
        const siteId = await CoreUtils.ignoreErrors(CoreSites.getStoredCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        if (!siteId) {
 | 
			
		||||
@ -427,7 +422,7 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when loaded.
 | 
			
		||||
     */
 | 
			
		||||
    async preloadSites(): Promise<void> {
 | 
			
		||||
    protected async preloadSites(): Promise<void> {
 | 
			
		||||
        const ids = await CoreSites.getSitesIds();
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.allPromises(ids.map((siteId) => this.addSite(siteId)));
 | 
			
		||||
@ -438,9 +433,17 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     */
 | 
			
		||||
    removeSite(siteId: string): void {
 | 
			
		||||
    protected removeSite(siteId: string): void {
 | 
			
		||||
        if (siteId && this.stylesEls[siteId]) {
 | 
			
		||||
            document.head.removeChild(this.stylesEls[siteId].element);
 | 
			
		||||
            for (const sourceName in this.stylesEls[siteId]) {
 | 
			
		||||
                const styleElementId = this.getStyleId(siteId, sourceName);
 | 
			
		||||
 | 
			
		||||
                const styleEl: HTMLStyleElement | null = document.head.querySelector(`style#${styleElementId}`);
 | 
			
		||||
 | 
			
		||||
                if (styleEl) {
 | 
			
		||||
                    document.head.removeChild(styleEl);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            delete this.stylesEls[siteId];
 | 
			
		||||
 | 
			
		||||
            CoreApp.setStatusBarColor();
 | 
			
		||||
@ -450,10 +453,10 @@ export class AddonRemoteThemesProvider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Unload styles for a temporary site.
 | 
			
		||||
     */
 | 
			
		||||
    unloadTmpStyles(): void {
 | 
			
		||||
        return this.removeSite(TMP_SITE_ID);
 | 
			
		||||
    protected unloadTmpStyles(): void {
 | 
			
		||||
        return this.removeSite(CoreStylesService.TMP_SITE_ID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonRemoteThemes = makeSingleton(AddonRemoteThemesProvider);
 | 
			
		||||
export const CoreStyles = makeSingleton(CoreStylesService);
 | 
			
		||||
							
								
								
									
										34
									
								
								src/core/features/styles/styles.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/core/features/styles/styles.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core';
 | 
			
		||||
import { CoreStyles, CoreStylesService } from './services/styles';
 | 
			
		||||
 | 
			
		||||
// List of providers (without handlers).
 | 
			
		||||
export const CORE_STYLE_SERVICES: Type<unknown>[] = [
 | 
			
		||||
    CoreStylesService,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            useValue: async () => {
 | 
			
		||||
                await CoreStyles.initialize();
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class CoreStylesModule {}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user