// (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 = 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(element?: Element | null, directiveClass?: DirectiveConstructor): 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(element?: Element | null, directiveClass?: DirectiveConstructor): 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(element: Element, directiveClass?: DirectiveConstructor): 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( element: Element | null, directiveClass?: DirectiveConstructor, ): Promise { 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( element: Element, selector?: string, directiveClass?: DirectiveConstructor, ): Promise { 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(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[], ): Promise { 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(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; }, []); if (allElements.length !== elementsAfterReady.length) { await this.waitMultipleDirectivesReady(element, directives); } } } /** * Directive constructor. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type DirectiveConstructor = { new(...args: any[]): T }; /** * Data to identify a directive when waiting for ready. */ type DirectiveData = { selector?: string; // If defined, CSS Selector to wait for. class?: DirectiveConstructor; // Directive class. };