MOBILE-4632 database: Load styles from site plugins

main
Pau Ferrer Ocaña 2024-06-19 16:43:48 +02:00
parent 41879a3526
commit eb9935a1d7
10 changed files with 129 additions and 44 deletions

View File

@ -75,9 +75,7 @@
</ng-container>
<div class="addon-data-contents addon-data-entries addon-data-entries-{{database.id}}" *ngIf="!isEmpty && database">
<core-style [css]="database.csstemplate" prefix="div.addon-data-entries.addon-data-entries-{{database.id}}" />
<core-compile-html [text]="entriesRendered" [jsData]="jsData" [extraImports]="extraImports" />
<core-compile-html [text]="entriesRendered" [jsData]="jsData" [extraImports]="extraImports" [cssCode]="database.csstemplate" />
</div>

View File

@ -21,10 +21,9 @@
[courseId]="database?.course" />
<div class="addon-data-contents addon-data-edit-entry {{cssClass}}" *ngIf="database">
<core-style [css]="database.csstemplate" prefix="div.addon-data-edit-entry.{{cssClass}}" />
<form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl>
<core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports" />
<core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"
[cssCode]="database.csstemplate" />
</form>
</div>
</core-loading>

View File

@ -28,9 +28,8 @@
[courseId]="courseId" />
<div class="addon-data-contents addon-data-entry addon-data-entries-{{database.id}}" *ngIf="database && entry">
<core-style [css]="database.csstemplate" prefix="div.addon-data-entry.addon-data-entries-{{database.id}}" />
<core-compile-html [text]="entryHtml" [jsData]="jsData" [extraImports]="extraImports" (compiling)="setRenderingEntry($event)" />
<core-compile-html [text]="entryHtml" [jsData]="jsData" [extraImports]="extraImports" (compiling)="setRenderingEntry($event)"
[cssCode]="database.csstemplate" />
</div>
<core-rating-rate *ngIf="database && entry && ratingInfo && (!database.approval || entry.approved)" [ratingInfo]="ratingInfo"

View File

@ -100,6 +100,7 @@ import { CoreSitesListComponent } from './sites-list/sites-list';
CoreShowPasswordComponent,
CoreSitePickerComponent,
CoreSplitViewComponent,
// eslint-disable-next-line deprecation/deprecation
CoreStyleComponent,
CoreSwipeSlidesComponent,
CoreTabComponent,
@ -155,6 +156,7 @@ import { CoreSitesListComponent } from './sites-list/sites-list';
CoreShowPasswordComponent,
CoreSitePickerComponent,
CoreSplitViewComponent,
// eslint-disable-next-line deprecation/deprecation
CoreStyleComponent,
CoreSwipeSlidesComponent,
CoreTabComponent,

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, ElementRef, Input, OnChanges } from '@angular/core';
import { CoreDom } from '@singletons/dom';
/**
* Component to add a <style> tag.
@ -23,6 +24,7 @@ import { Component, ElementRef, Input, OnChanges } from '@angular/core';
* Example:
*
* <core-style [css]="'p { color: red; }'" prefix=".custom-rules"></core-style>
* @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');
}
}

View File

@ -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<string, unknown>; // 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<unknown>[] = []; // Extra providers.
@Input() forceCompile = false; // Set it to true to force compile even if the text/javascript hasn't changed.
@ -101,14 +105,15 @@ 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) {
if (!changes) {
return;
}
this.setInputData();
if (this.componentInstance.ngOnChanges) {
this.componentInstance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes));
}
}
}
/**
* @inheritdoc
@ -116,7 +121,8 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> {
// 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<void> {
// 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.
*

View File

@ -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<T = unknown>(
@ -182,12 +183,17 @@ export class CoreCompileProvider {
componentClass: Type<T>,
viewContainerRef: ViewContainerRef,
extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any
styles?: string,
): Promise<ComponentRef<T> | 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 = [

View File

@ -127,7 +127,7 @@ export class CoreSitePluginsCallWSBaseDirective implements OnInit, OnDestroy {
}
/**
* Directive destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.invalidateObserver?.unsubscribe();

View File

@ -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);

View File

@ -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');
}
}
/**