Merge pull request #3865 from dpalou/MOBILE-3947

Mobile 3947
main
Noel De Martin 2023-11-29 10:34:44 +01:00 committed by GitHub
commit 2d141bc104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 150 additions and 100 deletions

View File

@ -14,7 +14,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
const minimatch = require('minimatch');
const { minimatch } = require('minimatch');
const { existsSync, readFileSync, writeFileSync, statSync, renameSync, rmSync } = require('fs');
const { readdir } = require('fs').promises;
const { mkdirSync, copySync } = require('fs-extra');

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { Injectable, ViewContainerRef } from '@angular/core';
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter';
@ -32,10 +32,6 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle
protected template = document.createElement('template'); // A template element to convert HTML to element.
constructor(protected factoryResolver: ComponentFactoryResolver) {
super();
}
/**
* @inheritdoc
*/
@ -95,8 +91,7 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle
const url = placeholder.getAttribute('data-player-src') || '';
// Create the component to display the player.
const factory = this.factoryResolver.resolveComponentFactory(CoreH5PPlayerComponent);
const componentRef = viewContainerRef.createComponent<CoreH5PPlayerComponent>(factory);
const componentRef = viewContainerRef.createComponent<CoreH5PPlayerComponent>(CoreH5PPlayerComponent);
componentRef.instance.src = url;
componentRef.instance.component = component;

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, COMPILER_OPTIONS, NgModule } from '@angular/core';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouteReuseStrategy } from '@angular/router';
@ -28,7 +28,7 @@ import { AddonsModule } from '@addons/addons.module';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { JitCompilerFactory } from '@angular/platform-browser-dynamic';
import { CoreCronDelegate } from '@services/cron';
import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron';
import { moodleTransitionAnimation } from '@classes/page-transition';
@ -71,8 +71,6 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{ provide: COMPILER_OPTIONS, useValue: {}, multi: true },
{ provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] },
{
provide: APP_INITIALIZER,
multi: true,

View File

@ -20,7 +20,6 @@ import {
OnChanges,
DoCheck,
ViewContainerRef,
ComponentFactoryResolver,
ComponentRef,
KeyValueDiffers,
SimpleChange,
@ -70,7 +69,8 @@ export class CoreDynamicComponent<ComponentClass> implements OnChanges, DoCheck
@Input() data?: Record<string | number, unknown>;
// Get the container where to put the dynamic component.
@ViewChild('dynamicComponent', { read: ViewContainerRef }) set dynamicComponent(el: ViewContainerRef) {
@ViewChild('dynamicComponent', { read: ViewContainerRef })
set dynamicComponent(el: ViewContainerRef) {
this.container = el;
// Use a timeout to avoid ExpressionChangedAfterItHasBeenCheckedError.
@ -85,7 +85,6 @@ export class CoreDynamicComponent<ComponentClass> implements OnChanges, DoCheck
protected lastComponent?: Type<unknown>;
constructor(
protected factoryResolver: ComponentFactoryResolver,
differs: KeyValueDiffers,
protected cdr: ChangeDetectorRef,
protected element: ElementRef,
@ -96,7 +95,7 @@ export class CoreDynamicComponent<ComponentClass> implements OnChanges, DoCheck
}
/**
* Detect changes on input properties.
* @inheritdoc
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (changes.component && !this.component) {
@ -110,7 +109,7 @@ export class CoreDynamicComponent<ComponentClass> implements OnChanges, DoCheck
}
/**
* Detect and act upon changes that Angular cant or wont detect on its own (objects and arrays).
* @inheritdoc
*/
ngDoCheck(): void {
if (this.instance) {
@ -172,8 +171,7 @@ export class CoreDynamicComponent<ComponentClass> implements OnChanges, DoCheck
} else {
try {
// Create the component and add it to the container.
const factory = this.factoryResolver.resolveComponentFactory(this.component);
const componentRef = this.container.createComponent(factory);
const componentRef = this.container.createComponent(this.component);
this.instance = componentRef.instance;
} catch (ex) {

View File

@ -20,7 +20,6 @@ import {
ElementRef,
ViewContainerRef,
ViewChild,
ComponentFactoryResolver,
} from '@angular/core';
import { CoreLogger } from '@singletons/logger';
import { CoreDomUtils } from '@services/utils/dom';
@ -78,7 +77,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
protected mergedContextMenu?: CoreContextMenuComponent;
protected createdMainContextMenuElement?: HTMLElement;
constructor(element: ElementRef, protected factoryResolver: ComponentFactoryResolver) {
constructor(element: ElementRef) {
this.element = element.nativeElement;
this.logger = CoreLogger.getInstance('CoreNavBarButtonsComponent');
@ -186,8 +185,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
* @returns Created component.
*/
protected createMainContextMenu(): CoreContextMenuComponent {
const factory = this.factoryResolver.resolveComponentFactory(CoreContextMenuComponent);
const componentRef = this.container.createComponent<CoreContextMenuComponent>(factory);
const componentRef = this.container.createComponent(CoreContextMenuComponent);
this.createdMainContextMenuElement = componentRef.location.nativeElement;

View File

@ -56,7 +56,7 @@ import { CoreUtils } from '@services/utils/utils';
*/
@Component({
selector: 'core-compile-html',
template: '<core-loading [hideUntil]="loaded"><ng-container #dynamicComponent></ng-container></core-loading>',
template: '<core-loading [hideUntil]="loaded"><ng-container #dynamicComponent /></core-loading>',
styles: [':host { display: contents; }'],
})
export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
@ -66,16 +66,16 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
@Input() jsData?: Record<string, unknown>; // Data to pass to the fake component.
@Input() extraImports: unknown[] = []; // Extra import modules.
@Input() extraProviders: Type<unknown>[] = []; // Extra providers.
@Input() forceCompile?: boolean; // 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.
@Output() created = new EventEmitter<unknown>(); // Will emit an event when the component is instantiated.
@Output() compiling = new EventEmitter<boolean>(); // Event that indicates whether the template is being compiled.
loaded = false;
componentInstance?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
// Get the container where to put the content.
@ViewChild('dynamicComponent', { read: ViewContainerRef }) container?: ViewContainerRef;
loaded?: boolean;
componentInstance?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
protected componentRef?: ComponentRef<unknown>;
protected element: HTMLElement;
protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the jsData input.
@ -114,6 +114,10 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
* @inheritdoc
*/
async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> {
if (!this.container) {
return;
}
// 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)))) {
@ -124,16 +128,18 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
try {
const componentClass = await this.getComponentClass();
const factory = await CoreCompile.createAndCompileComponent(this.text, componentClass, this.extraImports);
// Destroy previous components.
this.componentRef?.destroy();
if (factory) {
// Create the component.
this.componentRef = this.container?.createComponent(factory);
this.componentRef && this.created.emit(this.componentRef.instance);
}
// Create the component.
this.componentRef = await CoreCompile.createAndCompileComponent(
this.text,
componentClass,
this.container,
this.extraImports,
);
this.componentRef && this.created.emit(this.componentRef.instance);
this.loaded = true;
} catch (error) {
@ -192,7 +198,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
}
/**
* Component being initialized.
* @inheritdoc
*/
ngOnInit(): void {
// If there is some javascript to run, do it now.
@ -204,7 +210,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
for (const name in compileInstance.pendingCalls) {
const pendingCall = compileInstance.pendingCalls[name];
if (typeof this[name] == 'function') {
if (typeof this[name] === 'function') {
// Call the function.
Promise.resolve(this[name].apply(this, pendingCall.params)).then(pendingCall.defer.resolve)
.catch(pendingCall.defer.reject);
@ -218,21 +224,21 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
}
/**
* Content has been initialized.
* @inheritdoc
*/
ngAfterContentInit(): void {
this.callLifecycleHookOverride('ngAfterContentInit');
}
/**
* View has been initialized.
* @inheritdoc
*/
ngAfterViewInit(): void {
this.callLifecycleHookOverride('ngAfterViewInit');
}
/**
* Component destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.callLifecycleHookOverride('ngOnDestroy');
@ -283,9 +289,9 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck {
* once the component has been created.
* @returns Result of the call. Undefined if no component instance or the function doesn't exist.
*/
callComponentFunction(name: string, params?: unknown[], callWhenCreated: boolean = true): unknown {
callComponentFunction(name: string, params?: unknown[], callWhenCreated = true): unknown {
if (this.componentInstance) {
if (typeof this.componentInstance[name] == 'function') {
if (typeof this.componentInstance[name] === 'function') {
return this.componentInstance[name].apply(this.componentInstance, params);
}
} else if (callWhenCreated) {

View File

@ -0,0 +1,28 @@
// (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, Pipe } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
/**
* Copy of translate pipe to use when compiling a dynamic component.
* For some reason, when compiling a dynamic component the original translate pipe isn't found so we use this copy instead.
*/
@Injectable()
@Pipe({
name: 'translate',
pure: false, // required to update the value when the promise is resolved
standalone: true,
})
export class TranslatePipeForCompile extends TranslatePipe {}

View File

@ -17,14 +17,13 @@ import {
Injector,
Component,
NgModule,
Compiler,
ComponentFactory,
ComponentRef,
NgModuleRef,
NO_ERRORS_SCHEMA,
Type,
Provider,
createNgModule,
ViewContainerRef,
} from '@angular/core';
import { JitCompilerFactory } from '@angular/platform-browser-dynamic';
import {
ActionSheetController,
AlertController,
@ -34,6 +33,7 @@ import {
ToastController,
} from '@ionic/angular';
import { TranslateService } from '@ngx-translate/core';
import { TranslatePipeForCompile } from '../pipes/translate';
import { CoreLogger } from '@singletons/logger';
import { CoreEvents } from '@singletons/events';
@ -160,6 +160,8 @@ import { CorePromisedValue } from '@classes/promised-value';
import { CorePlatform } from '@services/platform';
import { CoreAutoLogoutService } from '@features/autologout/services/autologout';
import '@angular/compiler';
/**
* Service to provide functionalities regarding compiling dynamic HTML and Javascript.
*/
@ -167,7 +169,6 @@ import { CoreAutoLogoutService } from '@features/autologout/services/autologout'
export class CoreCompileProvider {
protected logger: CoreLogger;
protected compiler: Compiler;
// Other Ionic/Angular providers that don't depend on where they are injected.
protected readonly OTHER_SERVICES: unknown[] = [
@ -186,10 +187,8 @@ export class CoreCompileProvider {
getWorkshopComponentModules,
];
constructor(protected injector: Injector, compilerFactory: JitCompilerFactory) {
constructor(protected injector: Injector) {
this.logger = CoreLogger.getInstance('CoreCompileProvider');
this.compiler = compilerFactory.createCompiler();
}
/**
@ -197,14 +196,17 @@ export class CoreCompileProvider {
*
* @param template The template of the component.
* @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.
* @returns Promise resolved with the factory to instantiate 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
): Promise<ComponentFactory<T> | undefined> {
): Promise<ComponentRef<T> | undefined> {
// Create the component using the template and the class.
const component = Component({ template })(componentClass);
@ -213,17 +215,24 @@ export class CoreCompileProvider {
...CoreArray.flatten(lazyImports),
...this.IMPORTS,
...extraImports,
TranslatePipeForCompile,
];
// Now create the module containing the component.
const module = NgModule({ imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA] })(class {});
try {
// Compile the module and the component.
const factories = await this.compiler.compileModuleAndAllComponentsAsync(module);
viewContainerRef.clear();
// Search and return the factory of the component we just created.
return factories.componentFactories.find(factory => factory.componentType == component);
// Now create the module containing the component.
const ngModuleRef = createNgModule(
NgModule({ imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA] })(class {}),
this.injector,
);
return viewContainerRef.createComponent(
component,
{
environmentInjector: ngModuleRef,
},
);
} catch (error) {
this.logger.error('Error compiling template', template);
this.logger.error(error);
@ -331,10 +340,10 @@ export class CoreCompileProvider {
// We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
for (const i in providers) {
const providerDef = providers[i];
if (typeof providerDef == 'function' && providerDef.name) {
if (typeof providerDef === 'function' && providerDef.name) {
try {
// Inject the provider to the instance. We use the class name as the property name.
instance[providerDef.name.replace(/DelegateService$/, 'Delegate')] = this.injector.get(providerDef);
instance[providerDef.name.replace(/DelegateService$/, 'Delegate')] = this.injector.get<Provider>(providerDef);
} catch (ex) {
this.logger.error('Error injecting provider', providerDef.name, ex);
}
@ -407,29 +416,6 @@ export class CoreCompileProvider {
];
}
/**
* Instantiate a dynamic component.
*
* @param template The template of the component.
* @param componentClass The JS class of the component.
* @param injector The injector to use. It's recommended to pass it so NavController and similar can be injected.
* @returns Promise resolved with the component instance.
*/
async instantiateDynamicComponent<T = unknown>(
template: string,
componentClass: Type<T>,
injector?: Injector,
): Promise<ComponentRef<T> | undefined> {
injector = injector || this.injector;
const factory = await this.createAndCompileComponent(template, componentClass);
if (factory) {
// Create and return the component.
return factory.create(injector, undefined, undefined, injector.get(NgModuleRef));
}
}
}
export const CoreCompile = makeSingleton(CoreCompileProvider);

View File

@ -48,18 +48,18 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
// Get the compile element. Don't set the right type to prevent circular dependencies.
@ViewChild('compile') compileComponent?: CoreCompileHtmlComponent;
@HostBinding('class') @Input() component!: string;
@HostBinding('class') @Input() component = '';
@Input() method!: string;
@Input() args?: Record<string, unknown>;
@Input() initResult?: CoreSitePluginsContent | null; // Result of the init WS call of the handler.
@Input() data?: Record<string, unknown>; // Data to pass to the component.
@Input() data: Record<string, unknown> = {}; // Data to pass to the component.
@Input() preSets?: CoreSiteWSPreSets; // The preSets for the WS call.
@Input() pageTitle?: string; // Current page title. It can be used by the "new-content" directives.
@Output() onContentLoaded = new EventEmitter<CoreSitePluginsPluginContentLoadedData>(); // Emits event when content is loaded.
@Output() onLoadingContent = new EventEmitter<boolean>(); // Emits an event when starts to load the content.
content?: string; // Content.
javascript?: string; // Javascript to execute.
content = ''; // Content.
javascript = ''; // Javascript to execute.
otherData?: Record<string, unknown>; // Other data of the content.
dataLoaded = false;
invalidateObservable = new Subject<void>(); // An observable to notify observers when to invalidate data.
@ -120,13 +120,27 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
this.jsData = Object.assign(this.data, CoreSitePlugins.createDataForJS(this.initResult, result));
// Pass some methods as jsData so they can be called from the template too.
this.jsData.fetchContent = refresh => this.fetchContent(refresh);
this.jsData.openContent = (title, args, component, method, jsData, preSets, ptrEnabled) =>
this.openContent(title, args, component, method, jsData, preSets, ptrEnabled);
this.jsData.refreshContent = showSpinner => this.refreshContent(showSpinner);
this.jsData.updateContent = (args, component, method, jsData, preSets) =>
this.updateContent(args, component, method, jsData, preSets);
this.jsData.updateModuleCourseContent = (cmId, alreadyFetched) => this.updateModuleCourseContent(cmId, alreadyFetched);
this.jsData.fetchContent = (refresh?: boolean) => this.fetchContent(refresh);
this.jsData.openContent = (
title: string,
args?: Record<string, unknown>,
component?: string,
method?: string,
jsData?: Record<string, unknown> | boolean,
preSets?: CoreSiteWSPreSets,
ptrEnabled?: boolean,
) => this.openContent(title, args, component, method, jsData, preSets, ptrEnabled);
this.jsData.refreshContent = (showSpinner?: boolean) => this.refreshContent(showSpinner);
this.jsData.updateContent = (
args?: Record<string, unknown>,
component?: string,
method?: string,
jsData?: Record<string, unknown>,
preSets?: CoreSiteWSPreSets,
) => this.updateContent(args, component, method, jsData, preSets);
this.jsData.updateModuleCourseContent = (cmId: number, alreadyFetched?: boolean) =>
this.updateModuleCourseContent(cmId, alreadyFetched);
this.jsData.updateCachedContent = () => this.updateCachedContent();
this.onContentLoaded.emit({ refresh: !!refresh, success: true });
} catch (error) {
@ -154,7 +168,7 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
*/
openContent(
title: string,
args?: Record<string, unknown>,
args: Record<string, unknown> = {},
component?: string,
method?: string,
jsData?: Record<string, unknown> | boolean,
@ -167,7 +181,6 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
component = component || this.component;
method = method || this.method;
args = args || {};
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, {
@ -187,7 +200,7 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
*
* @param showSpinner Whether to show spinner while refreshing.
*/
async refreshContent(showSpinner: boolean = true): Promise<void> {
async refreshContent(showSpinner = true): Promise<void> {
if (showSpinner) {
this.dataLoaded = false;
}

View File

@ -16,10 +16,10 @@ import {
AbstractType,
ApplicationInitStatus,
ApplicationRef,
ComponentFactoryResolver as ComponentFactoryResolverService,
Injector,
NgZone as NgZoneService,
Type,
EnvironmentInjector,
} from '@angular/core';
import { Router as RouterService } from '@angular/router';
import { HttpClient } from '@angular/common/http';
@ -204,7 +204,6 @@ export const Http = makeSingleton(HttpClient);
export const ActionSheetController = makeSingleton(ActionSheetControllerService);
export const AngularDelegate = makeSingleton(AngularDelegateService);
export const AlertController = makeSingleton(AlertControllerService);
export const ComponentFactoryResolver = makeSingleton(ComponentFactoryResolverService);
export const LoadingController = makeSingleton(LoadingControllerService);
export const ModalController = makeSingleton(ModalControllerService);
export const PopoverController = makeSingleton(PopoverControllerService);
@ -226,5 +225,5 @@ export const Translate: Omit<CoreSingletonProxy<TranslateService>, 'instant'> &
export const AngularFrameworkDelegate = asyncInstance(async () => {
const injector = await singletonsInjector;
return AngularDelegate.create(ComponentFactoryResolver.instance, injector);
return AngularDelegate.create(injector.get(EnvironmentInjector), injector);
});

29
src/types/angular.d.ts vendored 100644
View File

@ -0,0 +1,29 @@
// (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 { UrlTree } from '@angular/router';
import { NavigationOptions } from '@ionic/angular/common/providers/nav-controller';
declare module '@ionic/angular' {
export interface NavController {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navigateForward(url: string | UrlTree | any[], options?: NavigationOptions): Promise<boolean | null>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navigateRoot(url: string | UrlTree | any[], options?: NavigationOptions): Promise<boolean | null>;
}
}