/** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ import { ContextRequestEvent } from '../context-request-event.js'; import { ValueNotifier } from '../value-notifier.js'; export class ContextProviderEvent extends Event { /** * * @param context the context which this provider can provide * @param contextTarget the original context target of the provider */ constructor(context, contextTarget) { super('context-provider', { bubbles: true, composed: true }); this.context = context; this.contextTarget = contextTarget; } } /** * A ReactiveController which adds context provider behavior to a * custom element. * * This controller simply listens to the `context-request` event when * the host is connected to the DOM and registers the received callbacks * against its observable Context implementation. * * The controller may also be attached to any HTML element in which case it's * up to the user to call hostConnected() when attached to the DOM. This is * done automatically for any custom elements implementing * ReactiveControllerHost. */ export class ContextProvider extends ValueNotifier { constructor(host, contextOrOptions, initialValue) { super(contextOrOptions.context !== undefined ? contextOrOptions.initialValue : initialValue); this.onContextRequest = (ev) => { // Only call the callback if the context matches. if (ev.context !== this.context) { return; } // Also, in case an element is a consumer AND a provider // of the same context, we want to avoid the element to self-register. const consumerHost = ev.contextTarget ?? ev.composedPath()[0]; if (consumerHost === this.host) { return; } ev.stopPropagation(); this.addCallback(ev.callback, consumerHost, ev.subscribe); }; /** * When we get a provider request event, that means a child of this element * has just woken up. If it's a provider of our context, then we may need to * re-parent our subscriptions, because is a more specific provider than us * for its subtree. */ this.onProviderRequest = (ev) => { // Ignore events when the context doesn't match. if (ev.context !== this.context) { return; } // Also, in case an element is a consumer AND a provider // of the same context it shouldn't provide to itself. const childProviderHost = ev.contextTarget ?? ev.composedPath()[0]; if (childProviderHost === this.host) { return; } // Re-parent all of our subscriptions in case this new child provider // should take them over. const seen = new Set(); for (const [callback, { consumerHost }] of this.subscriptions) { // Prevent infinite loops in the case where a one host element // is providing the same context multiple times. // // While normally it's a no-op to attempt to re-parent a subscription // that already has its proper parent, in the case where there's more // than one ValueProvider for the same context on the same hostElement, // they will each call the consumer, and since they will each have their // own dispose function, a well behaved consumer will notice the change // in dispose function and call their old one. // // This will cause the subscriptions to thrash, but worse, without this // set check here, we can end up in an infinite loop, as we add and remove // the same subscriptions onto the end of the map over and over. if (seen.has(callback)) { continue; } seen.add(callback); consumerHost.dispatchEvent(new ContextRequestEvent(this.context, consumerHost, callback, true)); } ev.stopPropagation(); }; this.host = host; if (contextOrOptions.context !== undefined) { this.context = contextOrOptions.context; } else { this.context = contextOrOptions; } this.attachListeners(); this.host.addController?.(this); } attachListeners() { this.host.addEventListener('context-request', this.onContextRequest); this.host.addEventListener('context-provider', this.onProviderRequest); } hostConnected() { // emit an event to signal a provider is available for this context this.host.dispatchEvent(new ContextProviderEvent(this.context, this.host)); } } //# sourceMappingURL=context-provider.js.map