diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-boundary.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-boundary.controller.ts new file mode 100644 index 0000000000..6fa5b7d1b1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-boundary.controller.ts @@ -0,0 +1,40 @@ +import type { UmbContextToken } from '../token/index.js'; +import { UmbContextBoundary } from './context-boundary.js'; +import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbContextBoundaryController extends UmbContextBoundary implements UmbController { + #host: UmbControllerHost; + #controllerAlias: string; + + public get controllerAlias(): string { + return this.#controllerAlias; + } + + constructor(host: UmbControllerHost, contextAlias: string | UmbContextToken) { + super(host.getHostElement(), contextAlias); + this.#host = host; + // Makes the controllerAlias unique for this instance, this enables multiple Contexts to be provided under the same name. (This only makes sense cause of Context Token Discriminators) + // This does mean that if someone provides a context with the same name, but with a different instance, it will not override the previous instance. But its good since it enables extensions to provide contexts at the same scope of other contexts. + this.#controllerAlias = 'umbContextBoundary_' + contextAlias.toString(); + + // If this API is already provided with this alias? Then we do not want to register this controller: + const existingControllers = host.getUmbControllers((x) => x.controllerAlias === this.controllerAlias); + if (existingControllers.length > 0) { + // This just an additional awareness feature to make devs Aware, the alternative would be adding it anyway, but that would destroy existing controller of this alias. + // Back out, this instance is already provided, by another controller. + throw new Error( + `Context API: The context boundary of '${this.controllerAlias}' is already provided by another Context Provider Controller.`, + ); + } else { + host.addUmbController(this); + } + } + + public override destroy(): void { + if (this.#host) { + this.#host.removeUmbController(this); + (this.#host as unknown) = undefined; + } + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-boundary.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-boundary.ts new file mode 100644 index 0000000000..a44f61af62 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-boundary.ts @@ -0,0 +1,60 @@ +import type { UmbContextRequestEvent } from '../consume/context-request.event.js'; +import type { UmbContextToken } from '../token/index.js'; +import { UMB_CONTENT_REQUEST_EVENT_TYPE } from '../consume/context-request.event.js'; +import { UmbContextProvideEventImplementation } from './context-provide.event.js'; + +/** + * @class UmbContextBoundary + */ +export class UmbContextBoundary { + #eventTarget: EventTarget; + + #contextAlias: string; + + /** + * Creates an instance of UmbContextBoundary. + * @param {EventTarget} eventTarget - the host element for this context provider + * @param {string | UmbContextToken} contextIdentifier - a string or token to identify the context + * @param {*} instance - the instance to provide + * @memberof UmbContextBoundary + */ + constructor(eventTarget: EventTarget, contextIdentifier: string | UmbContextToken) { + this.#eventTarget = eventTarget; + + const idSplit = contextIdentifier.toString().split('#'); + this.#contextAlias = idSplit[0]; + + this.#eventTarget.addEventListener(UMB_CONTENT_REQUEST_EVENT_TYPE, this.#handleContextRequest); + } + + /** + * @private + * @param {UmbContextRequestEvent} event - the event to handle + * @memberof UmbContextBoundary + */ + #handleContextRequest = ((event: UmbContextRequestEvent): void => { + if (event.contextAlias !== this.#contextAlias) return; + + if (event.stopAtContextMatch) { + // Since the alias matches, we will stop it from bubbling further up. But we still allow it to ask the other Contexts of the element. Hence not calling `event.stopImmediatePropagation();` + event.stopPropagation(); + } + }) as EventListener; + + /** + * @memberof UmbContextBoundary + */ + public hostConnected(): void { + //this.hostElement.addEventListener(UMB_CONTENT_REQUEST_EVENT_TYPE, this.#handleContextRequest); + this.#eventTarget.dispatchEvent(new UmbContextProvideEventImplementation(this.#contextAlias)); + } + + /** + * @memberof UmbContextBoundary + */ + public hostDisconnected(): void {} + + destroy(): void { + (this.#eventTarget as unknown) = undefined; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts index 31f62cdc8e..21a8aa9f86 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts @@ -46,7 +46,7 @@ export class UmbContextProvider { diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/index.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/index.ts index 1b23b3d4ba..3690c440fd 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/index.ts @@ -1,3 +1,5 @@ +export * from './context-boundary.js'; +export * from './context-boundary.controller.js'; +export * from './context-provide.event.js'; export * from './context-provider.controller.js'; export * from './context-provider.js'; -export * from './context-provide.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts index 165adbea9d..5bde58c3f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts @@ -13,10 +13,14 @@ import { type UUIModalDialogElement, type UUIModalSidebarElement, } from '@umbraco-cms/backoffice/external/uui'; -import type { UmbRouterSlotElement } from '@umbraco-cms/backoffice/router'; +import { UMB_ROUTE_CONTEXT, type UmbRouterSlotElement } from '@umbraco-cms/backoffice/router'; import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import type { UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api'; -import { UMB_CONTENT_REQUEST_EVENT_TYPE, UmbContextProvider } from '@umbraco-cms/backoffice/context-api'; +import { + UMB_CONTENT_REQUEST_EVENT_TYPE, + UmbContextBoundary, + UmbContextProvider, +} from '@umbraco-cms/backoffice/context-api'; @customElement('umb-modal') export class UmbModalElement extends UmbLitElement { @@ -41,7 +45,7 @@ export class UmbModalElement extends UmbLitElement { #innerElement = new UmbBasicState(undefined); #modalExtensionObserver?: UmbObserverController; - #modalRouterElement: UmbRouterSlotElement = document.createElement('umb-router-slot'); + #modalRouterElement?: HTMLDivElement | UmbRouterSlotElement; #onClose = () => { this.element?.removeEventListener(UUIModalCloseEvent, this.#onClose); @@ -85,6 +89,7 @@ export class UmbModalElement extends UmbLitElement { * */ if (this.#modalContext.router) { + this.#modalRouterElement = document.createElement('umb-router-slot'); this.#modalRouterElement.routes = [ { path: '', @@ -92,9 +97,13 @@ export class UmbModalElement extends UmbLitElement { }, ]; this.#modalRouterElement.parent = this.#modalContext.router; + } else { + this.#modalRouterElement = document.createElement('div'); + new UmbContextBoundary(this.#modalRouterElement, UMB_ROUTE_CONTEXT).hostConnected(); } this.element.appendChild(this.#modalRouterElement); + this.#observeModal(this.#modalContext.alias.toString()); const provider = new UmbContextProvider(this.element, UMB_MODAL_CONTEXT, this.#modalContext); @@ -151,14 +160,14 @@ export class UmbModalElement extends UmbLitElement { } #appendInnerElement(element: HTMLElement) { - this.#modalRouterElement.appendChild(element); + this.#modalRouterElement!.appendChild(element); this.#innerElement.setValue(element); } #removeInnerElement() { const innerElement = this.#innerElement.getValue(); if (innerElement) { - this.#modalRouterElement.removeChild(innerElement); + this.#modalRouterElement!.removeChild(innerElement); this.#innerElement.setValue(undefined); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/router/modal-registration/modal-route-registration.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/router/modal-registration/modal-route-registration.controller.ts index d487de0fff..1ee307a27a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/router/modal-registration/modal-route-registration.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/router/modal-registration/modal-route-registration.controller.ts @@ -259,9 +259,9 @@ export class UmbModalRouteRegistrationController< } public open(params: { [key: string]: string | number }, prepend?: string) { - if (this.active) return; + if (this.active || !this.#routeBuilder) return; - window.history.pushState({}, '', this.#routeBuilder?.(params) + (prepend ? `${prepend}` : '')); + window.history.pushState({}, '', this.#routeBuilder(params) + (prepend ? `${prepend}` : '')); } /** @@ -277,6 +277,7 @@ export class UmbModalRouteRegistrationController< return this; } public _internal_setRouteBuilder(urlBuilder: UmbModalRouteBuilder) { + if (!this.#routeContext) return; this.#routeBuilder = urlBuilder; this.#urlBuilderCallback?.(urlBuilder); }