From eb9935a1d76b4c68efe4ee41b1b67f0fa698f330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 19 Jun 2024 16:43:48 +0200 Subject: [PATCH] MOBILE-4632 database: Load styles from site plugins --- .../index/addon-mod-data-index.html | 4 +- src/addons/mod/data/pages/edit/edit.html | 5 +-- src/addons/mod/data/pages/entry/entry.html | 5 +-- src/core/components/components.module.ts | 2 + src/core/components/style/style.ts | 30 ++----------- .../components/compile-html/compile-html.ts | 44 ++++++++++++++++--- src/core/features/compile/services/compile.ts | 8 +++- .../siteplugins/classes/call-ws-directive.ts | 2 +- src/core/services/platform.ts | 34 ++++++++++++++ src/core/singletons/dom.ts | 39 ++++++++++++++++ 10 files changed, 129 insertions(+), 44 deletions(-) diff --git a/src/addons/mod/data/components/index/addon-mod-data-index.html b/src/addons/mod/data/components/index/addon-mod-data-index.html index d71ce8244..d521f8e60 100644 --- a/src/addons/mod/data/components/index/addon-mod-data-index.html +++ b/src/addons/mod/data/components/index/addon-mod-data-index.html @@ -75,9 +75,7 @@
- - - +
diff --git a/src/addons/mod/data/pages/edit/edit.html b/src/addons/mod/data/pages/edit/edit.html index df520ec87..c27206afb 100644 --- a/src/addons/mod/data/pages/edit/edit.html +++ b/src/addons/mod/data/pages/edit/edit.html @@ -21,10 +21,9 @@ [courseId]="database?.course" />
- -
- +
diff --git a/src/addons/mod/data/pages/entry/entry.html b/src/addons/mod/data/pages/entry/entry.html index 0c8d7026e..a29f4fe43 100644 --- a/src/addons/mod/data/pages/entry/entry.html +++ b/src/addons/mod/data/pages/entry/entry.html @@ -28,9 +28,8 @@ [courseId]="courseId" />
- - - +
tag. @@ -23,6 +24,7 @@ import { Component, ElementRef, Input, OnChanges } from '@angular/core'; * Example: * * + * @deprecated since 4.5.0. Not needed anymore, core-compile-html accepts now CSS code. */ @Component({ selector: 'core-style', @@ -41,37 +43,11 @@ export class CoreStyleComponent implements OnChanges { ngOnChanges(): void { if (this.element && this.element.nativeElement) { const style = document.createElement('style'); - style.innerHTML = this.prefixCSS(this.css, this.prefix); + style.innerHTML = CoreDom.prefixCSS(this.css, this.prefix); this.element.nativeElement.innerHTML = ''; this.element.nativeElement.appendChild(style); } } - /** - * Add a prefix to all rules in a CSS string. - * - * @param css CSS code to be prefixed. - * @param prefix Prefix css selector. - * @returns Prefixed CSS. - */ - protected prefixCSS(css: string, prefix: string): string { - if (!css) { - return ''; - } - - if (!prefix) { - return css; - } - - // Remove comments first. - let regExp = /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm; - css = css.replace(regExp, ''); - - // Add prefix. - regExp = /([^]*?)({[^]*?}|,)/g; - - return css.replace(regExp, prefix + ' $1 $2'); - } - } diff --git a/src/core/features/compile/components/compile-html/compile-html.ts b/src/core/features/compile/components/compile-html/compile-html.ts index b657d3d8e..10c0c3d2a 100644 --- a/src/core/features/compile/components/compile-html/compile-html.ts +++ b/src/core/features/compile/components/compile-html/compile-html.ts @@ -38,6 +38,8 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CoreCompile } from '@features/compile/services/compile'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { CoreWS } from '@services/ws'; +import { CoreDom } from '@singletons/dom'; /** * This component has a behaviour similar to $compile for AngularJS. Given an HTML code, it will compile it so all its @@ -64,6 +66,8 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { @Input() text!: string; // The HTML text to display. @Input() javascript?: string; // The Javascript to execute in the component. @Input() jsData?: Record; // Data to pass to the fake component. + @Input() cssCode?: string; // The styles to apply. + @Input() stylesPath?: string; // The styles URL to apply (only if cssCode is not set). @Input() extraImports: unknown[] = []; // Extra import modules. @Input() extraProviders: Type[] = []; // Extra providers. @Input() forceCompile = false; // Set it to true to force compile even if the text/javascript hasn't changed. @@ -101,12 +105,13 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { // Check if there's any change in the jsData object. const changes = this.differ.diff(this.jsData || {}); - if (changes) { - this.setInputData(); + if (!changes) { + return; + } + this.setInputData(); - if (this.componentInstance.ngOnChanges) { - this.componentInstance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes)); - } + if (this.componentInstance.ngOnChanges) { + this.componentInstance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes)); } } @@ -116,7 +121,8 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { async ngOnChanges(changes: Record): Promise { // Only compile if text/javascript has changed or the forceCompile flag has been set to true. if (this.text === undefined || - !(changes.text || changes.javascript || (changes.forceCompile && CoreUtils.isTrueOrOne(this.forceCompile)))) { + !(changes.text || changes.javascript || changes.cssCode || changes.stylesPath || + (changes.forceCompile && CoreUtils.isTrueOrOne(this.forceCompile)))) { return; } @@ -132,11 +138,14 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { // Create the component. if (this.container) { + await this.loadCSSCode(); + this.componentRef = await CoreCompile.createAndCompileComponent( this.text, componentClass, this.container, this.extraImports, + this.cssCode, ); this.element.addEventListener('submit', (event) => { @@ -163,6 +172,29 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { this.componentRef?.destroy(); } + /** + * Retrieve the CSS code from the stylesPath if not loaded yet. + */ + protected async loadCSSCode(): Promise { + // Do not allow (yet) to load CSS code to a component that doesn't have text. + if (!this.text) { + this.cssCode = ''; + + return; + } + + if (this.stylesPath && !this.cssCode) { + this.cssCode = await CoreUtils.ignoreErrors(CoreWS.getText(this.stylesPath)); + } + + // Prepend all CSS rules with :host to avoid conflicts. + if (!this.cssCode || this.cssCode.includes(':host')) { + return; + } + + this.cssCode = CoreDom.prefixCSS(this.cssCode, ':host ::ng-deep', ':host'); + } + /** * Get a class that defines the dynamic component. * diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 2d084e4ab..63849d3d0 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -175,6 +175,7 @@ export class CoreCompileProvider { * @param componentClass The JS class of the component. * @param viewContainerRef View container reference to inject the component. * @param extraImports Extra imported modules if needed and not imported by this class. + * @param styles CSS code to apply to the component. * @returns Promise resolved with the component reference. */ async createAndCompileComponent( @@ -182,12 +183,17 @@ export class CoreCompileProvider { componentClass: Type, viewContainerRef: ViewContainerRef, extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any + styles?: string, ): Promise | undefined> { // Import the Angular compiler to be able to compile components in runtime. await import('@angular/compiler'); // Create the component using the template and the class. - const component = Component({ template, host: { 'compiled-component-id': String(this.componentId++) } })(componentClass); + const component = Component({ + template, + host: { 'compiled-component-id': String(this.componentId++) }, + styles, + })(componentClass); const lazyImports = await Promise.all(this.LAZY_IMPORTS.map(getModules => getModules())); const imports = [ diff --git a/src/core/features/siteplugins/classes/call-ws-directive.ts b/src/core/features/siteplugins/classes/call-ws-directive.ts index f908c4ed4..f17b7d8a4 100644 --- a/src/core/features/siteplugins/classes/call-ws-directive.ts +++ b/src/core/features/siteplugins/classes/call-ws-directive.ts @@ -127,7 +127,7 @@ export class CoreSitePluginsCallWSBaseDirective implements OnInit, OnDestroy { } /** - * Directive destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.invalidateObserver?.unsubscribe(); diff --git a/src/core/services/platform.ts b/src/core/services/platform.ts index c08089d06..61b1d7194 100644 --- a/src/core/services/platform.ts +++ b/src/core/services/platform.ts @@ -22,6 +22,8 @@ import { Device, makeSingleton } from '@singletons'; @Injectable({ providedIn: 'root' }) export class CorePlatformService extends Platform { + private static cssNesting?: boolean; + /** * Get platform major version number. * @@ -117,6 +119,38 @@ export class CorePlatformService extends Platform { return 'WebAssembly' in window; } + /** + * Check if the browser supports CSS nesting. + * + * @returns Whether the browser supports CSS nesting. + */ + supportsCSSNesting(): boolean { + if (CorePlatformService.cssNesting !== undefined) { + return CorePlatformService.cssNesting; + } + + // Add nested CSS to DOM and check if it's supported. + const style = document.createElement('style'); + style.innerHTML = 'div.nested { &.css { color: red; } }'; + document.head.appendChild(style); + + // Add an element to check if the nested CSS is applied. + const div = document.createElement('div'); + div.className = 'nested css'; + document.body.appendChild(div); + + const color = window.getComputedStyle(div).color; + + // Check if color is red. + CorePlatformService.cssNesting = color === 'rgb(255, 0, 0)'; + + // Clean the DOM. + document.head.removeChild(style); + document.body.removeChild(div); + + return CorePlatformService.cssNesting; + } + } export const CorePlatform = makeSingleton(CorePlatformService); diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index f52d8e5f5..ff22077c0 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -16,6 +16,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver } from '@singletons/events'; +import { CorePlatform } from '@services/platform'; /** * Singleton with helper functions for dom. @@ -684,6 +685,44 @@ export class CoreDom { return element; } + /** + * Prefix CSS rules. + * + * @param css CSS code. + * @param prefix Prefix to add to CSS rules. + * @param prefixIfNested Prefix to add to CSS rules if nested. It may happend we need different prefixes. + * Ie: If nested is supported ::ng-deep is not needed. + * @returns Prefixed CSS. + */ + static prefixCSS(css: string, prefix: string, prefixIfNested?: string): string { + if (!css) { + return ''; + } + + if (!prefix) { + return css; + } + + // Check if browser supports CSS nesting. + const supportsNesting = CorePlatform.supportsCSSNesting(); + if (supportsNesting) { + prefixIfNested = prefixIfNested ?? prefix; + + // Wrap the CSS with the prefix. + return `${prefixIfNested} { ${css} }`; + } + + // Fallback. + // Remove comments first. + let regExp = /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm; + css = css.replace(regExp, ''); + + // Add prefix. + regExp = /([^]*?)({[^]*?}|,)/g; + + return css.replace(regExp, prefix + ' $1 $2'); + } + } /**