Vmeda.Online/src/core/singletons/directives-registry.ts
Dani Palou 1c967b7813 MOBILE-4479 core: Make wait directives functions recursive
To be able to wait for elements inside the loading elements
2023-12-04 09:14:20 +01:00

213 lines
7.0 KiB
TypeScript

// (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 { Directive } from '@angular/core';
import { AsyncDirective } from '@classes/async-directive';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from './logger';
/**
* Registry to keep track of directive instances.
*/
export class CoreDirectivesRegistry {
private static instances: WeakMap<Element, unknown[]> = new WeakMap();
protected static logger = CoreLogger.getInstance('CoreDirectivesRegistry');
/**
* Register a directive instance.
*
* @param element Root element.
* @param instance Directive instance.
*/
static register(element: Element, instance: unknown): void {
const list = this.instances.get(element) ?? [];
list.push(instance);
this.instances.set(element, list);
}
/**
* Resolve a directive instance.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Directive instance.
*/
static resolve<T>(element?: Element | null, directiveClass?: DirectiveConstructor<T>): T | null {
const list = (element && this.instances.get(element) as T[]) ?? [];
return list.find(instance => !directiveClass || instance instanceof directiveClass) ?? null;
}
/**
* Resolve all directive instances.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Directive instances.
*/
static resolveAll<T>(element?: Element | null, directiveClass?: DirectiveConstructor<T>): T[] {
const list = (element && this.instances.get(element) as T[]) ?? [];
return list.filter(instance => !directiveClass || instance instanceof directiveClass) ?? [];
}
/**
* Get a directive instance and fail if it cannot be resolved.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Directive instance.
*/
static require<T>(element: Element, directiveClass?: DirectiveConstructor<T>): T {
const instance = this.resolve(element, directiveClass);
if (!instance) {
throw new Error('Couldn\'t resolve directive instance');
}
return instance;
}
/**
* Get a directive instance and wait to be ready.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Promise resolved when done.
*/
static async waitDirectiveReady<T extends AsyncDirective>(
element: Element | null,
directiveClass?: DirectiveConstructor<T>,
): Promise<void> {
const instance = this.resolve(element, directiveClass);
if (!instance) {
this.logger.error('No instance registered for element ' + directiveClass, element);
return;
}
await instance.ready();
}
/**
* Get all directive instances and wait to be ready.
*
* @param element Root element.
* @param selector If defined, CSS Selector to wait for.
* @param directiveClass Directive class.
* @returns Promise resolved when done.
*/
static async waitDirectivesReady<T extends AsyncDirective>(
element: Element,
selector?: string,
directiveClass?: DirectiveConstructor<T>,
): Promise<void> {
const findElements = (): Element[] => {
if (!selector || element.matches(selector)) {
// Element to wait is myself.
return [element];
} else {
return Array.from(element.querySelectorAll(selector));
}
};
const elements = findElements();
if (!elements.length) {
return;
}
await Promise.all(elements.map(async element => {
const instances = this.resolveAll<T>(element, directiveClass);
await Promise.all(instances.map(instance => instance.ready()));
}));
// Wait for next tick to ensure directives are completely rendered.
await CoreUtils.nextTick();
// Check if there are new elements now that the found elements are ready (there could be nested elements).
if (elements.length !== findElements().length) {
await this.waitDirectivesReady(element, selector, directiveClass);
}
}
/**
* Get all directive instances (with multiple types) and wait for them to be ready.
*
* @param element Root element.
* @param directives Directives to wait.
* @returns Promise resolved when done.
*/
static async waitMultipleDirectivesReady(
element: Element,
directives: DirectiveData<AsyncDirective>[],
): Promise<void> {
const findElements = (selector?: string): Element[] => {
if (!selector || element.matches(selector)) {
// Element to wait is myself.
return [element];
} else {
return Array.from(element.querySelectorAll(selector));
}
};
let allElements: Element[] = [];
await Promise.all(directives.map(async directive => {
const elements = findElements(directive.selector);
if (!elements.length) {
return;
}
allElements = allElements.concat(elements);
await Promise.all(elements.map(async element => {
const instances = this.resolveAll<AsyncDirective>(element, directive.class);
await Promise.all(instances.map(instance => instance.ready()));
}));
}));
// Wait for next tick to ensure directives are completely rendered.
await CoreUtils.nextTick();
// Check if there are new elements now that the found elements are ready (there could be nested elements).
const elementsAfterReady = directives.reduce((elements, directive) => {
elements = elements.concat(findElements(directive.selector));
return elements;
}, <Element[]> []);
if (allElements.length !== elementsAfterReady.length) {
await this.waitMultipleDirectivesReady(element, directives);
}
}
}
/**
* Directive constructor.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DirectiveConstructor<T = Directive> = { new(...args: any[]): T };
/**
* Data to identify a directive when waiting for ready.
*/
type DirectiveData<T extends AsyncDirective> = {
selector?: string; // If defined, CSS Selector to wait for.
class?: DirectiveConstructor<T>; // Directive class.
};