Skip to content

Commit

Permalink
MOBILE-4632 database: Load styles from site plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
crazyserver committed Jul 15, 2024
1 parent 3400d18 commit 16f1194
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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>


Expand Down
5 changes: 2 additions & 3 deletions src/addons/mod/data/pages/edit/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ <h1>
[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>
Expand Down
5 changes: 2 additions & 3 deletions src/addons/mod/data/pages/entry/entry.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ <h1>
[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"
Expand Down
2 changes: 2 additions & 0 deletions src/core/components/components.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import { CoreSitesListComponent } from './sites-list/sites-list';
CoreShowPasswordComponent,
CoreSitePickerComponent,
CoreSplitViewComponent,
// eslint-disable-next-line deprecation/deprecation
CoreStyleComponent,
CoreSwipeSlidesComponent,
CoreTabComponent,
Expand Down Expand Up @@ -155,6 +156,7 @@ import { CoreSitesListComponent } from './sites-list/sites-list';
CoreShowPasswordComponent,
CoreSitePickerComponent,
CoreSplitViewComponent,
// eslint-disable-next-line deprecation/deprecation
CoreStyleComponent,
CoreSwipeSlidesComponent,
CoreTabComponent,
Expand Down
30 changes: 3 additions & 27 deletions src/core/components/style/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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',
Expand All @@ -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');
}

}
45 changes: 39 additions & 6 deletions src/core/features/compile/components/compile-html/compile-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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));
}
}

Expand All @@ -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;
}

Expand All @@ -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) => {
Expand All @@ -163,6 +172,30 @@ 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) {
if (!this.cssCode.includes(':host')) {
this.cssCode = CoreDom.prefixCSS(this.cssCode, ':host ::ng-deep', ':host');
}

}
}

/**
* Get a class that defines the dynamic component.
*
Expand Down
8 changes: 7 additions & 1 deletion src/core/features/compile/services/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,19 +175,25 @@ 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>(
template: string,
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 = [
Expand Down
2 changes: 1 addition & 1 deletion src/core/features/siteplugins/classes/call-ws-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class CoreSitePluginsCallWSBaseDirective implements OnInit, OnDestroy {
}

/**
* Directive destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.invalidateObserver?.unsubscribe();
Expand Down
34 changes: 34 additions & 0 deletions src/core/services/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
39 changes: 39 additions & 0 deletions src/core/singletons/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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');
}

}

/**
Expand Down

0 comments on commit 16f1194

Please sign in to comment.