From 2f21241a47983b152a48c4bbf9b5ef17d4aab62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 7 Jun 2021 10:22:27 +0200 Subject: [PATCH 1/2] MOBILE-3320 core: Fix some status and toolbar styles --- src/core/components/show-password/show-password.ts | 2 +- .../course/components/format/core-course-format.html | 4 ++-- .../features/settings/pages/deviceinfo/deviceinfo.html | 2 +- src/core/features/settings/services/settings-helper.ts | 6 ++++-- src/core/services/app.ts | 7 +++++-- src/core/singletons/colors.ts | 2 +- src/theme/theme.light.scss | 9 ++++++--- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts index 01337e88d..56faad272 100644 --- a/src/core/components/show-password/show-password.ts +++ b/src/core/components/show-password/show-password.ts @@ -105,7 +105,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit { */ protected setData(): void { this.label = this.shown ? 'core.hide' : 'core.show'; - this.iconName = this.shown ? 'eye-off' : 'eye'; + this.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye'; if (this.input) { this.input.type = this.shown ? 'text' : 'password'; } diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html index 3995c6be0..59eec9907 100644 --- a/src/core/features/course/components/format/core-course-format.html +++ b/src/core/features/course/components/format/core-course-format.html @@ -93,14 +93,14 @@ - - diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.html b/src/core/features/settings/pages/deviceinfo/deviceinfo.html index b8bea0208..640405293 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.html +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.html @@ -8,7 +8,7 @@ - + diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts index 88772158d..7144656c3 100644 --- a/src/core/features/settings/services/settings-helper.ts +++ b/src/core/features/settings/services/settings-helper.ts @@ -434,12 +434,14 @@ export class CoreSettingsHelperProvider { setColorScheme(colorScheme: CoreColorScheme): void { if (colorScheme == CoreColorScheme.SYSTEM && this.prefersDark) { // Listen for changes to the prefers-color-scheme media query. - this.prefersDark.addEventListener('change', this.toggleDarkModeListener); + this.prefersDark.addEventListener && + this.prefersDark.addEventListener('change', this.toggleDarkModeListener); this.toggleDarkMode(this.prefersDark.matches); } else { // Stop listening to changes. - this.prefersDark?.removeEventListener('change', this.toggleDarkModeListener); + this.prefersDark?.removeEventListener && + this.prefersDark?.removeEventListener('change', this.toggleDarkModeListener); this.toggleDarkMode(colorScheme == CoreColorScheme.DARK); } diff --git a/src/core/services/app.ts b/src/core/services/app.ts index 27dc93587..a44ba3c64 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -642,9 +642,12 @@ export class CoreAppProvider { color = CoreColors.getColorHex(color); } - // Make darker on Android. + // Make darker on Android, except white. if (this.isAndroid()) { - color = CoreColors.darker(color); + const rgb = CoreColors.hexToRGB(color); + if (rgb.red != 255 || rgb.green != 255 || rgb.blue != 255) { + color = CoreColors.darker(color); + } } this.logger.debug(`Set status bar color ${color}`); diff --git a/src/core/singletons/colors.ts b/src/core/singletons/colors.ts index 5f3e7718f..eea420a06 100644 --- a/src/core/singletons/colors.ts +++ b/src/core/singletons/colors.ts @@ -93,7 +93,7 @@ export class CoreColors { * @param color Hexadec RGB Color. * @return RGB color components. */ - protected static hexToRGB(color: string): ColorComponents { + static hexToRGB(color: string): ColorComponents { if (color.charAt(0) == '#') { color = color.substr(1); } diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index c3595b37c..363c93576 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -111,15 +111,14 @@ --core-header-toolbar-border-width: #{$toolbar-border-width}; --core-header-toolbar-border-color: #{$toolbar-border-color}; --core-header-toolbar-color: #{$toolbar-color}; - ion-header ion-toolbar, - ion-header.header-ios ion-toolbar:last-of-type { + ion-header ion-toolbar { --color: var(--core-header-toolbar-color); --background: var(--core-header-toolbar-background); --border-width: 0 0 var(--core-header-toolbar-border-width) 0; --border-color: var(--core-header-toolbar-border-color); ion-button { - --ion-toolbar-color: transparent; + --ion-toolbar-color: var(--core-header-toolbar-color); --color: var(--core-header-toolbar-color); } @@ -129,6 +128,10 @@ } } + ion-header.header-ios ion-toolbar:last-of-type { + --border-width: 0 0 var(--core-header-toolbar-border-width) 0; + } + ion-searchbar { --background: var(--ion-item-background); .searchbar-input { From 72b6057783d41ba4ebe3873f68e8510ba4fd055d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 7 Jun 2021 13:52:25 +0200 Subject: [PATCH 2/2] MOBILE-3670 remotethemes: Create a general framework for styling the app --- .../remotethemes/remotethemes.module.ts | 12 +- .../services/remotethemes-handler.ts | 148 +++++++ src/core/features/compile/services/compile.ts | 4 +- src/core/features/features.module.ts | 2 + .../features/styles/services/styles.ts} | 419 +++++++++--------- src/core/features/styles/styles.module.ts | 34 ++ 6 files changed, 401 insertions(+), 218 deletions(-) create mode 100644 src/addons/remotethemes/services/remotethemes-handler.ts rename src/{addons/remotethemes/services/remotethemes.ts => core/features/styles/services/styles.ts} (51%) create mode 100644 src/core/features/styles/styles.module.ts diff --git a/src/addons/remotethemes/remotethemes.module.ts b/src/addons/remotethemes/remotethemes.module.ts index 322d7744e..74666bff4 100644 --- a/src/addons/remotethemes/remotethemes.module.ts +++ b/src/addons/remotethemes/remotethemes.module.ts @@ -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[] = [ - 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[] = [ multi: true, deps: [], useFactory: () => async () => { - await AddonRemoteThemes.initialize(); + CoreStyles.registerStyleHandler(AddonRemoteThemesHandler.instance); }, }, ], diff --git a/src/addons/remotethemes/services/remotethemes-handler.ts b/src/addons/remotethemes/services/remotethemes-handler.ts new file mode 100644 index 000000000..b92f7b8d1 --- /dev/null +++ b/src/addons/remotethemes/services/remotethemes-handler.ts @@ -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 { + return true; + } + + /** + * @inheritDoc + */ + async getStyle(siteId: string, config?: CoreSitePublicConfigResponse): Promise { + 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 { + 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 { + + 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); diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 958de26c1..929371c91 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -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. diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index a8d883e7e..534890791 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -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, diff --git a/src/addons/remotethemes/services/remotethemes.ts b/src/core/features/styles/services/styles.ts similarity index 51% rename from src/addons/remotethemes/services/remotethemes.ts rename to src/core/features/styles/services/styles.ts index e8fb945ef..b0854fe00 100644 --- a/src/addons/remotethemes/services/remotethemes.ts +++ b/src/core/features/styles/services/styles.ts @@ -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; + + /** + * 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; +} + +/** + * 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 { 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 { + let contents = ''; + + const enabled = await handler.isEnabled(siteId, config); + if (enabled) { + contents = (await handler.getStyle(siteId, config)).trim(); + } + + const hash = 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 { + protected async addSite(siteId?: string): Promise { 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. ( 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 = 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 { - - 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 { + protected async load(siteId?: string, disabled = false): Promise { 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 = 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 { - 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 { - return this.loadTmpStyles(config.mobilecssurl); + protected async loadTmpStyles(config: CoreSitePublicConfigResponse): Promise { + // 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 { + protected async preloadCurrentSite(): Promise { const siteId = await CoreUtils.ignoreErrors(CoreSites.getStoredCurrentSiteId()); if (!siteId) { @@ -427,7 +422,7 @@ export class AddonRemoteThemesProvider { * * @return Promise resolved when loaded. */ - async preloadSites(): Promise { + protected async preloadSites(): Promise { 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); diff --git a/src/core/features/styles/styles.module.ts b/src/core/features/styles/styles.module.ts new file mode 100644 index 000000000..f085b0d67 --- /dev/null +++ b/src/core/features/styles/styles.module.ts @@ -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[] = [ + CoreStylesService, +]; + +@NgModule({ + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: async () => { + await CoreStyles.initialize(); + }, + }, + ], +}) +export class CoreStylesModule {}