Merge pull request #4123 from crazyserver/MOBILE-4632

Mobile 4632
main
Dani Palou 2024-07-15 16:22:51 +02:00 committed by GitHub
commit 8b34f1f40a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 182 additions and 91 deletions

View File

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

View File

@ -21,10 +21,9 @@
[courseId]="database?.course" /> [courseId]="database?.course" />
<div class="addon-data-contents addon-data-edit-entry {{cssClass}}" *ngIf="database"> <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> <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> </form>
</div> </div>
</core-loading> </core-loading>

View File

@ -28,9 +28,8 @@
[courseId]="courseId" /> [courseId]="courseId" />
<div class="addon-data-contents addon-data-entry addon-data-entries-{{database.id}}" *ngIf="database && entry"> <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)"
[cssCode]="database.csstemplate" />
<core-compile-html [text]="entryHtml" [jsData]="jsData" [extraImports]="extraImports" (compiling)="setRenderingEntry($event)" />
</div> </div>
<core-rating-rate *ngIf="database && entry && ratingInfo && (!database.approval || entry.approved)" [ratingInfo]="ratingInfo" <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, CoreShowPasswordComponent,
CoreSitePickerComponent, CoreSitePickerComponent,
CoreSplitViewComponent, CoreSplitViewComponent,
// eslint-disable-next-line deprecation/deprecation
CoreStyleComponent, CoreStyleComponent,
CoreSwipeSlidesComponent, CoreSwipeSlidesComponent,
CoreTabComponent, CoreTabComponent,
@ -155,6 +156,7 @@ import { CoreSitesListComponent } from './sites-list/sites-list';
CoreShowPasswordComponent, CoreShowPasswordComponent,
CoreSitePickerComponent, CoreSitePickerComponent,
CoreSplitViewComponent, CoreSplitViewComponent,
// eslint-disable-next-line deprecation/deprecation
CoreStyleComponent, CoreStyleComponent,
CoreSwipeSlidesComponent, CoreSwipeSlidesComponent,
CoreTabComponent, CoreTabComponent,

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
import { Component, ElementRef, Input, OnChanges } from '@angular/core'; import { Component, ElementRef, Input, OnChanges } from '@angular/core';
import { CoreDom } from '@singletons/dom';
/** /**
* Component to add a <style> tag. * Component to add a <style> tag.
@ -23,6 +24,7 @@ import { Component, ElementRef, Input, OnChanges } from '@angular/core';
* Example: * Example:
* *
* <core-style [css]="'p { color: red; }'" prefix=".custom-rules"></core-style> * <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({ @Component({
selector: 'core-style', selector: 'core-style',
@ -41,37 +43,11 @@ export class CoreStyleComponent implements OnChanges {
ngOnChanges(): void { ngOnChanges(): void {
if (this.element && this.element.nativeElement) { if (this.element && this.element.nativeElement) {
const style = document.createElement('style'); 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.innerHTML = '';
this.element.nativeElement.appendChild(style); 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 { CoreCompile } from '@features/compile/services/compile';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; 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 * 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() text!: string; // The HTML text to display.
@Input() javascript?: string; // The Javascript to execute in the component. @Input() javascript?: string; // The Javascript to execute in the component.
@Input() jsData?: Record<string, unknown>; // Data to pass to the fake 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() extraImports: unknown[] = []; // Extra import modules.
@Input() extraProviders: Type<unknown>[] = []; // Extra providers. @Input() extraProviders: Type<unknown>[] = []; // Extra providers.
@Input() forceCompile = false; // Set it to true to force compile even if the text/javascript hasn't changed. @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. // Check if there's any change in the jsData object.
const changes = this.differ.diff(this.jsData || {}); const changes = this.differ.diff(this.jsData || {});
if (changes) { if (!changes) {
this.setInputData(); return;
}
this.setInputData();
if (this.componentInstance.ngOnChanges) { if (this.componentInstance.ngOnChanges) {
this.componentInstance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes)); this.componentInstance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes));
}
} }
} }
@ -116,7 +121,8 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> { async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> {
// Only compile if text/javascript has changed or the forceCompile flag has been set to true. // Only compile if text/javascript has changed or the forceCompile flag has been set to true.
if (this.text === undefined || 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; return;
} }
@ -132,11 +138,14 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
// Create the component. // Create the component.
if (this.container) { if (this.container) {
await this.loadCSSCode();
this.componentRef = await CoreCompile.createAndCompileComponent( this.componentRef = await CoreCompile.createAndCompileComponent(
this.text, this.text,
componentClass, componentClass,
this.container, this.container,
this.extraImports, this.extraImports,
this.cssCode,
); );
this.element.addEventListener('submit', (event) => { this.element.addEventListener('submit', (event) => {
@ -163,6 +172,29 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
this.componentRef?.destroy(); 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. * 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 componentClass The JS class of the component.
* @param viewContainerRef View container reference to inject 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 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. * @returns Promise resolved with the component reference.
*/ */
async createAndCompileComponent<T = unknown>( async createAndCompileComponent<T = unknown>(
@ -182,12 +183,17 @@ export class CoreCompileProvider {
componentClass: Type<T>, componentClass: Type<T>,
viewContainerRef: ViewContainerRef, viewContainerRef: ViewContainerRef,
extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any
styles?: string,
): Promise<ComponentRef<T> | undefined> { ): Promise<ComponentRef<T> | undefined> {
// Import the Angular compiler to be able to compile components in runtime. // Import the Angular compiler to be able to compile components in runtime.
await import('@angular/compiler'); await import('@angular/compiler');
// Create the component using the template and the class. // 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 lazyImports = await Promise.all(this.LAZY_IMPORTS.map(getModules => getModules()));
const imports = [ const imports = [

View File

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

View File

@ -13,3 +13,7 @@
// limitations under the License. // limitations under the License.
export const CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT = 'siteplugins_update_course_content'; export const CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT = 'siteplugins_update_course_content';
export const CORE_SITE_PLUGINS_COMPONENT = 'CoreSitePlugins';
export const CORE_SITE_PLUGINS_PATH = 'siteplugins';

View File

@ -62,7 +62,6 @@ import {
CoreSitePluginsContent, CoreSitePluginsContent,
CoreSitePluginsPlugin, CoreSitePluginsPlugin,
CoreSitePluginsHandlerData, CoreSitePluginsHandlerData,
CoreSitePluginsProvider,
CoreSitePluginsCourseOptionHandlerData, CoreSitePluginsCourseOptionHandlerData,
CoreSitePluginsMainMenuHandlerData, CoreSitePluginsMainMenuHandlerData,
CoreSitePluginsCourseModuleHandlerData, CoreSitePluginsCourseModuleHandlerData,
@ -89,8 +88,7 @@ import { CoreUrlUtils } from '@services/utils/url';
import { CorePath } from '@singletons/path'; import { CorePath } from '@singletons/path';
import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate';
import { CoreSitePluginsEnrolHandler } from '../classes/handlers/enrol-handler'; import { CoreSitePluginsEnrolHandler } from '../classes/handlers/enrol-handler';
import { CORE_SITE_PLUGINS_COMPONENT } from '../constants';
const HANDLER_DISABLED = 'core_site_plugins_helper_handler_disabled';
/** /**
* Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site * Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site
@ -102,7 +100,7 @@ const HANDLER_DISABLED = 'core_site_plugins_helper_handler_disabled';
* inside the host DOM element? * inside the host DOM element?
*/ */
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CoreSitePluginsHelperProvider { export class CoreSitePluginsInitService {
protected logger: CoreLogger; protected logger: CoreLogger;
protected courseRestrictHandlers: Record<string, { protected courseRestrictHandlers: Record<string, {
@ -112,21 +110,23 @@ export class CoreSitePluginsHelperProvider {
handler: CoreSitePluginsCourseOptionHandler | CoreSitePluginsUserProfileHandler; handler: CoreSitePluginsCourseOptionHandler | CoreSitePluginsUserProfileHandler;
}> = {}; }> = {};
protected static readonly HANDLER_DISABLED = 'core_site_plugins_helper_handler_disabled';
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreSitePluginsHelperProvider'); this.logger = CoreLogger.getInstance('CoreSitePluginsInit');
} }
/** /**
* Initialize. * Initialize.
*/ */
initialize(): void { init(): void {
// Fetch the plugins on login. // Fetch the plugins on login.
CoreEvents.on(CoreEvents.LOGIN, async (data) => { CoreEvents.on(CoreEvents.LOGIN, async (data) => {
try { try {
const plugins = await CoreUtils.ignoreErrors(CoreSitePlugins.getPlugins(data.siteId)); const plugins = await CoreUtils.ignoreErrors(CoreSitePlugins.getPlugins(data.siteId));
// Plugins fetched, check that site hasn't changed. // Plugins fetched, check that site hasn't changed.
if (data.siteId != CoreSites.getCurrentSiteId() || !plugins?.length) { if (data.siteId !== CoreSites.getCurrentSiteId() || !plugins?.length) {
return; return;
} }
@ -145,7 +145,7 @@ export class CoreSitePluginsHelperProvider {
// Re-load plugins restricted for courses when the list of user courses changes. // Re-load plugins restricted for courses when the list of user courses changes.
CoreEvents.on(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, (data) => { CoreEvents.on(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, (data) => {
if (data && data.siteId && data.siteId == CoreSites.getCurrentSiteId() && data.added && data.added.length) { if (data.siteId && data.siteId === CoreSites.getCurrentSiteId() && data.added.length) {
this.reloadCourseRestrictHandlers(); this.reloadCourseRestrictHandlers();
} }
}); });
@ -160,7 +160,7 @@ export class CoreSitePluginsHelperProvider {
* @param siteId Site ID. If not provided, current site. * @param siteId Site ID. If not provided, current site.
* @returns Promise resolved with the CSS code. * @returns Promise resolved with the CSS code.
*/ */
async downloadStyles( protected async downloadStyles(
plugin: CoreSitePluginsPlugin, plugin: CoreSitePluginsPlugin,
handlerName: string, handlerName: string,
handlerSchema: CoreSitePluginsHandlerData, handlerSchema: CoreSitePluginsHandlerData,
@ -184,7 +184,7 @@ export class CoreSitePluginsHelperProvider {
// Remove the CSS files for this handler that aren't used anymore. Don't block the call for this. // Remove the CSS files for this handler that aren't used anymore. Don't block the call for this.
const files = await CoreUtils.ignoreErrors( const files = await CoreUtils.ignoreErrors(
CoreFilepool.getFilesByComponent(site.getId(), CoreSitePluginsProvider.COMPONENT, componentId), CoreFilepool.getFilesByComponent(site.getId(), CORE_SITE_PLUGINS_COMPONENT, componentId),
); );
files?.forEach((file) => { files?.forEach((file) => {
@ -209,7 +209,7 @@ export class CoreSitePluginsHelperProvider {
site.getId(), site.getId(),
url, url,
false, false,
CoreSitePluginsProvider.COMPONENT, CORE_SITE_PLUGINS_COMPONENT,
componentId, componentId,
0, 0,
undefined, undefined,
@ -270,7 +270,7 @@ export class CoreSitePluginsHelperProvider {
// Create a "fake" instance to hold all the libraries. // Create a "fake" instance to hold all the libraries.
const instance = { const instance = {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
HANDLER_DISABLED: HANDLER_DISABLED, HANDLER_DISABLED: CoreSitePluginsInitService.HANDLER_DISABLED,
}; };
await CoreCompile.loadLibraries(); await CoreCompile.loadLibraries();
@ -285,7 +285,7 @@ export class CoreSitePluginsHelperProvider {
// Now execute the javascript using this instance. // Now execute the javascript using this instance.
result.jsResult = CoreCompile.executeJavascript(instance, result.javascript); result.jsResult = CoreCompile.executeJavascript(instance, result.javascript);
if (result.jsResult == HANDLER_DISABLED) { if (result.jsResult === CoreSitePluginsInitService.HANDLER_DISABLED) {
// The "disabled" field was added in 3.8, this is a workaround for previous versions. // The "disabled" field was added in 3.8, this is a workaround for previous versions.
result.disabled = true; result.disabled = true;
} }
@ -311,10 +311,10 @@ export class CoreSitePluginsHelperProvider {
* Given an addon name and the key of a string, return the full string key (prefixed). * Given an addon name and the key of a string, return the full string key (prefixed).
* *
* @param addon Name of the addon (plugin.addon). * @param addon Name of the addon (plugin.addon).
* @param key The key of the string. * @param key The key of the string. Defaults to pluginname.
* @returns Full string key. * @returns Full string key.
*/ */
protected getPrefixedString(addon: string, key: string): string { protected getPrefixedString(addon: string, key = 'pluginname'): string {
return this.getPrefixForStrings(addon) + key; return this.getPrefixForStrings(addon) + key;
} }
@ -323,7 +323,7 @@ export class CoreSitePluginsHelperProvider {
* *
* @param plugin Data of the plugin. * @param plugin Data of the plugin.
*/ */
loadLangStrings(plugin: CoreSitePluginsPlugin): void { protected loadLangStrings(plugin: CoreSitePluginsPlugin): void {
if (!plugin.parsedLang) { if (!plugin.parsedLang) {
return; return;
} }
@ -339,9 +339,8 @@ export class CoreSitePluginsHelperProvider {
* Load a site plugin. * Load a site plugin.
* *
* @param plugin Data of the plugin. * @param plugin Data of the plugin.
* @returns Promise resolved when loaded.
*/ */
async loadSitePlugin(plugin: CoreSitePluginsPlugin): Promise<void> { protected async loadSitePlugin(plugin: CoreSitePluginsPlugin): Promise<void> {
this.logger.debug('Load site plugin:', plugin); this.logger.debug('Load site plugin:', plugin);
if (!plugin.parsedHandlers && plugin.handlers) { if (!plugin.parsedHandlers && plugin.handlers) {
@ -378,9 +377,8 @@ export class CoreSitePluginsHelperProvider {
* Load site plugins. * Load site plugins.
* *
* @param plugins The plugins to load. * @param plugins The plugins to load.
* @returns Promise resolved when loaded.
*/ */
async loadSitePlugins(plugins: CoreSitePluginsPlugin[]): Promise<void> { protected async loadSitePlugins(plugins: CoreSitePluginsPlugin[]): Promise<void> {
this.courseRestrictHandlers = {}; this.courseRestrictHandlers = {};
await CoreUtils.allPromises(plugins.map(async (plugin) => { await CoreUtils.allPromises(plugins.map(async (plugin) => {
@ -401,7 +399,7 @@ export class CoreSitePluginsHelperProvider {
* @param version Styles version. * @param version Styles version.
* @param siteId Site ID. If not provided, current site. * @param siteId Site ID. If not provided, current site.
*/ */
loadStyles( protected loadStyles(
plugin: CoreSitePluginsPlugin, plugin: CoreSitePluginsPlugin,
handlerName: string, handlerName: string,
fileUrl: string, fileUrl: string,
@ -436,7 +434,7 @@ export class CoreSitePluginsHelperProvider {
// Styles have been loaded, now treat the CSS. // Styles have been loaded, now treat the CSS.
CoreUtils.ignoreErrors( CoreUtils.ignoreErrors(
CoreFilepool.treatCSSCode(siteId, fileUrl, cssCode, CoreSitePluginsProvider.COMPONENT, uniqueName, version), CoreFilepool.treatCSSCode(siteId, fileUrl, cssCode, CORE_SITE_PLUGINS_COMPONENT, uniqueName, version),
); );
} }
@ -446,9 +444,8 @@ export class CoreSitePluginsHelperProvider {
* @param plugin Data of the plugin. * @param plugin Data of the plugin.
* @param handlerName Name of the handler in the plugin. * @param handlerName Name of the handler in the plugin.
* @param handlerSchema Data about the handler. * @param handlerSchema Data about the handler.
* @returns Promise resolved when done.
*/ */
async registerHandler( protected async registerHandler(
plugin: CoreSitePluginsPlugin, plugin: CoreSitePluginsPlugin,
handlerName: string, handlerName: string,
handlerSchema: CoreSitePluginsHandlerData, handlerSchema: CoreSitePluginsHandlerData,
@ -700,7 +697,7 @@ export class CoreSitePluginsHelperProvider {
const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName);
const blockName = (handlerSchema.moodlecomponent || plugin.component).replace('block_', ''); const blockName = (handlerSchema.moodlecomponent || plugin.component).replace('block_', '');
const titleString = handlerSchema.displaydata?.title ?? 'pluginname'; const titleString = handlerSchema.displaydata?.title;
const prefixedTitle = this.getPrefixedString(plugin.addon, titleString); const prefixedTitle = this.getPrefixedString(plugin.addon, titleString);
CoreBlockDelegate.registerHandler( CoreBlockDelegate.registerHandler(
@ -761,7 +758,7 @@ export class CoreSitePluginsHelperProvider {
// Create and register the handler. // Create and register the handler.
const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName);
const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title);
const handler = new CoreSitePluginsCourseOptionHandler( const handler = new CoreSitePluginsCourseOptionHandler(
uniqueName, uniqueName,
prefixedTitle, prefixedTitle,
@ -873,7 +870,7 @@ export class CoreSitePluginsHelperProvider {
// Create and register the handler. // Create and register the handler.
const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName);
const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title);
CoreMainMenuDelegate.registerHandler( CoreMainMenuDelegate.registerHandler(
new CoreSitePluginsMainMenuHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult), new CoreSitePluginsMainMenuHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult),
@ -908,7 +905,7 @@ export class CoreSitePluginsHelperProvider {
// Create and register the handler. // Create and register the handler.
const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName);
const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title);
const processorName = (handlerSchema.moodlecomponent || plugin.component).replace('message_', ''); const processorName = (handlerSchema.moodlecomponent || plugin.component).replace('message_', '');
AddonMessageOutputDelegate.registerHandler( AddonMessageOutputDelegate.registerHandler(
@ -1074,7 +1071,7 @@ export class CoreSitePluginsHelperProvider {
// Create and register the handler. // Create and register the handler.
const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName);
const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title);
CoreSettingsDelegate.registerHandler( CoreSettingsDelegate.registerHandler(
new CoreSitePluginsSettingsHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult), new CoreSitePluginsSettingsHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult),
@ -1109,7 +1106,7 @@ export class CoreSitePluginsHelperProvider {
// Create and register the handler. // Create and register the handler.
const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName);
const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title);
const handler = new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult); const handler = new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult);
CoreUserDelegate.registerHandler(handler); CoreUserDelegate.registerHandler(handler);
@ -1182,8 +1179,6 @@ export class CoreSitePluginsHelperProvider {
/** /**
* Reload the handlers that are restricted to certain courses. * Reload the handlers that are restricted to certain courses.
*
* @returns Promise resolved when done.
*/ */
protected async reloadCourseRestrictHandlers(): Promise<void> { protected async reloadCourseRestrictHandlers(): Promise<void> {
if (!Object.keys(this.courseRestrictHandlers).length) { if (!Object.keys(this.courseRestrictHandlers).length) {
@ -1240,7 +1235,7 @@ export class CoreSitePluginsHelperProvider {
// Create and register the handler. // Create and register the handler.
const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName);
const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title);
CoreMainMenuHomeDelegate.registerHandler( CoreMainMenuHomeDelegate.registerHandler(
new CoreSitePluginsMainMenuHomeHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult), new CoreSitePluginsMainMenuHomeHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult),
@ -1251,4 +1246,4 @@ export class CoreSitePluginsHelperProvider {
} }
export const CoreSitePluginsHelper = makeSingleton(CoreSitePluginsHelperProvider); export const CoreSitePluginsInit = makeSingleton(CoreSitePluginsInitService);

View File

@ -34,9 +34,7 @@ import { CorePlatform } from '@services/platform';
import { CoreEnrolAction, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; import { CoreEnrolAction, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate';
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
import { CoreUserProfileHandlerType } from '@features/user/services/user-delegate'; import { CoreUserProfileHandlerType } from '@features/user/services/user-delegate';
import { CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT } from '../constants'; import { CORE_SITE_PLUGINS_COMPONENT, CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT } from '../constants';
const ROOT_CACHE_KEY = 'CoreSitePlugins:';
/** /**
* Service to provide functionalities regarding site plugins. * Service to provide functionalities regarding site plugins.
@ -44,7 +42,14 @@ const ROOT_CACHE_KEY = 'CoreSitePlugins:';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CoreSitePluginsProvider { export class CoreSitePluginsProvider {
static readonly COMPONENT = 'CoreSitePlugins'; protected static readonly ROOT_CACHE_KEY = 'CoreSitePlugins:';
/**
* @deprecated since 4.5.0. Use CORE_SITE_PLUGINS_COMPONENT instead.
*/
static readonly COMPONENT = CORE_SITE_PLUGINS_COMPONENT;
/**
* @deprecated since 4.5.0. Use CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT instead.
*/
static readonly UPDATE_COURSE_CONTENT = CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT; static readonly UPDATE_COURSE_CONTENT = CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT;
protected logger: CoreLogger; protected logger: CoreLogger;
@ -183,7 +188,7 @@ export class CoreSitePluginsProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getCallWSCommonCacheKey(method: string): string { protected getCallWSCommonCacheKey(method: string): string {
return ROOT_CACHE_KEY + 'ws:' + method; return CoreSitePluginsProvider.ROOT_CACHE_KEY + 'ws:' + method;
} }
/** /**
@ -250,7 +255,8 @@ export class CoreSitePluginsProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getContentCacheKey(component: string, method: string, args: Record<string, unknown>): string { protected getContentCacheKey(component: string, method: string, args: Record<string, unknown>): string {
return ROOT_CACHE_KEY + 'content:' + component + ':' + method + ':' + CoreUtils.sortAndStringify(args); return CoreSitePluginsProvider.ROOT_CACHE_KEY + 'content:' + component + ':' + method +
':' + CoreUtils.sortAndStringify(args);
} }
/** /**
@ -322,7 +328,7 @@ export class CoreSitePluginsProvider {
* @returns Cache key. * @returns Cache key.
*/ */
protected getPluginsCacheKey(): string { protected getPluginsCacheKey(): string {
return ROOT_CACHE_KEY + 'plugins'; return CoreSitePluginsProvider.ROOT_CACHE_KEY + 'plugins';
} }
/** /**

View File

@ -20,12 +20,13 @@ import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-ro
import { CoreMainMenuHomeRoutingModule } from '@features/mainmenu/mainmenu-home-routing.module'; import { CoreMainMenuHomeRoutingModule } from '@features/mainmenu/mainmenu-home-routing.module';
import { CoreSitePreferencesRoutingModule } from '@features/settings/settings-site-routing.module'; import { CoreSitePreferencesRoutingModule } from '@features/settings/settings-site-routing.module';
import { CoreSitePluginsComponentsModule } from './components/components.module'; import { CoreSitePluginsComponentsModule } from './components/components.module';
import { CoreSitePluginsHelper } from './services/siteplugins-helper'; import { CoreSitePluginsInit } from './services/siteplugins-init';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreSitePluginsPluginPage } from '@features/siteplugins/pages/plugin/plugin'; import { CoreSitePluginsPluginPage } from '@features/siteplugins/pages/plugin/plugin';
import { canLeaveGuard } from '@guards/can-leave'; import { canLeaveGuard } from '@guards/can-leave';
import { CoreSitePluginsCourseOptionPage } from '@features/siteplugins/pages/course-option/course-option'; import { CoreSitePluginsCourseOptionPage } from '@features/siteplugins/pages/course-option/course-option';
import { CoreSitePluginsModuleIndexPage } from '@features/siteplugins/pages/module-index/module-index'; import { CoreSitePluginsModuleIndexPage } from '@features/siteplugins/pages/module-index/module-index';
import { CORE_SITE_PLUGINS_PATH } from './constants';
/** /**
* Get site plugins exported objects. * Get site plugins exported objects.
@ -65,7 +66,7 @@ export async function getSitePluginsExportedObjects(): Promise<Record<string, un
const routes: Routes = [ const routes: Routes = [
{ {
path: 'siteplugins/content/:component/:method/:hash', path: `${CORE_SITE_PLUGINS_PATH}/content/:component/:method/:hash`,
component: CoreSitePluginsPluginPage, component: CoreSitePluginsPluginPage,
canDeactivate: [canLeaveGuard], canDeactivate: [canLeaveGuard],
}, },
@ -73,7 +74,7 @@ const routes: Routes = [
const homeRoutes: Routes = [ const homeRoutes: Routes = [
{ {
path: 'siteplugins/homecontent/:component/:method', path: `${CORE_SITE_PLUGINS_PATH}/homecontent/:component/:method`,
component: CoreSitePluginsPluginPage, component: CoreSitePluginsPluginPage,
canDeactivate: [canLeaveGuard], canDeactivate: [canLeaveGuard],
}, },
@ -81,7 +82,7 @@ const homeRoutes: Routes = [
const courseIndexRoutes: Routes = [ const courseIndexRoutes: Routes = [
{ {
path: 'siteplugins/:handlerUniqueName', path: `${CORE_SITE_PLUGINS_PATH}/:handlerUniqueName`,
component: CoreSitePluginsCourseOptionPage, component: CoreSitePluginsCourseOptionPage,
canDeactivate: [canLeaveGuard], canDeactivate: [canLeaveGuard],
}, },
@ -89,7 +90,7 @@ const courseIndexRoutes: Routes = [
const moduleRoutes: Routes = [ const moduleRoutes: Routes = [
{ {
path: 'siteplugins/module/:courseId/:cmId', path: `${CORE_SITE_PLUGINS_PATH}/module/:courseId/:cmId`,
component: CoreSitePluginsModuleIndexPage, component: CoreSitePluginsModuleIndexPage,
canDeactivate: [canLeaveGuard], canDeactivate: [canLeaveGuard],
}, },
@ -114,7 +115,7 @@ const moduleRoutes: Routes = [
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
multi: true, multi: true,
useValue: () => { useValue: () => {
CoreSitePluginsHelper.initialize(); CoreSitePluginsInit.init();
}, },
}, },
], ],

View File

@ -22,6 +22,8 @@ import { Device, makeSingleton } from '@singletons';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CorePlatformService extends Platform { export class CorePlatformService extends Platform {
private static cssNesting?: boolean;
/** /**
* Get platform major version number. * Get platform major version number.
* *
@ -117,6 +119,38 @@ export class CorePlatformService extends Platform {
return 'WebAssembly' in window; 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); export const CorePlatform = makeSingleton(CorePlatformService);

View File

@ -16,6 +16,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CorePlatform } from '@services/platform';
/** /**
* Singleton with helper functions for dom. * Singleton with helper functions for dom.
@ -684,6 +685,44 @@ export class CoreDom {
return element; 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');
}
} }
/** /**