| /** | |
| * @license | |
| * Copyright 2017 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import {PluginApi} from '../../../api/plugin'; | |
| import {HookApi, PluginElement} from '../../../api/hook'; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| type Callback = (value: any) => void; | |
| export interface ModuleInfo { | |
| moduleName: string; | |
| plugin: PluginApi; | |
| pluginUrl?: URL; | |
| type?: EndpointType; | |
| domHook?: HookApi<PluginElement>; | |
| slot?: string; | |
| } | |
| /** | |
| * Plugin-provided custom components can affect content in extension | |
| * points using one of following methods: | |
| * - DECORATE: custom component is set with `content` attribute and may | |
| * decorate (e.g. style) DOM element. | |
| * - REPLACE: contents of extension point are replaced with the custom | |
| * component. | |
| */ | |
| export enum EndpointType { | |
| DECORATE = 'decorate', | |
| REPLACE = 'replace', | |
| } | |
| interface Options { | |
| endpoint: string; | |
| dynamicEndpoint?: string; | |
| slot?: string; | |
| type?: EndpointType; | |
| moduleName?: string; | |
| domHook?: HookApi<PluginElement>; | |
| } | |
| export class GrPluginEndpoints { | |
| private readonly _endpoints = new Map<string, ModuleInfo[]>(); | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| private readonly _callbacks = new Map<string, ((value: any) => void)[]>(); | |
| private readonly _dynamicPlugins = new Map<string, Set<string>>(); | |
| private pluginLoaded = false; | |
| setPluginsReady() { | |
| this.pluginLoaded = true; | |
| } | |
| onNewEndpoint(endpoint: string, callback: Callback) { | |
| if (!this._callbacks.has(endpoint)) { | |
| this._callbacks.set(endpoint, []); | |
| } | |
| this._callbacks.get(endpoint)!.push(callback); | |
| } | |
| onDetachedEndpoint(endpoint: string, callback: Callback) { | |
| if (this._callbacks.has(endpoint)) { | |
| const filteredCallbacks = this._callbacks | |
| .get(endpoint)! | |
| .filter((cb: Callback) => cb !== callback); | |
| this._callbacks.set(endpoint, filteredCallbacks); | |
| } | |
| } | |
| _getOrCreateModuleInfo(plugin: PluginApi, opts: Options): ModuleInfo { | |
| const {endpoint, slot, type, moduleName, domHook} = opts; | |
| const existingModule = this._endpoints | |
| .get(endpoint)! | |
| .find( | |
| (info: ModuleInfo) => | |
| info.plugin === plugin && | |
| info.moduleName === moduleName && | |
| info.domHook === domHook && | |
| info.slot === slot | |
| ); | |
| if (existingModule) { | |
| return existingModule; | |
| } else { | |
| const newModule: ModuleInfo = { | |
| moduleName: moduleName!, | |
| plugin, | |
| pluginUrl: plugin._url, | |
| type, | |
| domHook, | |
| slot, | |
| }; | |
| this._endpoints.get(endpoint)!.push(newModule); | |
| return newModule; | |
| } | |
| } | |
| /** | |
| * Register a plugin to an endpoint. | |
| * | |
| * Dynamic plugins are registered to a specific prefix, such as | |
| * 'change-list-header'. These plugins are then fetched by prefix to determine | |
| * which endpoints to dynamically add to the page. | |
| */ | |
| registerModule(plugin: PluginApi, opts: Options) { | |
| const endpoint = opts.endpoint; | |
| const dynamicEndpoint = opts.dynamicEndpoint; | |
| if (dynamicEndpoint) { | |
| if (!this._dynamicPlugins.has(dynamicEndpoint)) { | |
| this._dynamicPlugins.set(dynamicEndpoint, new Set()); | |
| } | |
| this._dynamicPlugins.get(dynamicEndpoint)!.add(endpoint); | |
| } | |
| if (!this._endpoints.has(endpoint)) { | |
| this._endpoints.set(endpoint, []); | |
| } | |
| const moduleInfo = this._getOrCreateModuleInfo(plugin, opts); | |
| // TODO: the logic below seems wrong when: | |
| // multiple plugins register to the same endpoint | |
| // one register before plugins ready | |
| // the other done after, then only the later one will have the callbacks | |
| // invoked. | |
| if (this.pluginLoaded && this._callbacks.has(endpoint)) { | |
| this._callbacks.get(endpoint)!.forEach(callback => callback(moduleInfo)); | |
| } | |
| } | |
| getDynamicEndpoints(dynamicEndpoint: string): string[] { | |
| const plugins = this._dynamicPlugins.get(dynamicEndpoint); | |
| if (!plugins) return []; | |
| return Array.from(plugins); | |
| } | |
| /** | |
| * Get detailed information about modules registered with an extension | |
| * endpoint. | |
| */ | |
| getDetails(name: string): ModuleInfo[] { | |
| return (this._endpoints.get(name) ?? []).sort((m1, m2) => | |
| m1.plugin.getPluginName().localeCompare(m2.plugin.getPluginName()) | |
| ); | |
| } | |
| } |