diff --git a/src/Umbraco.Web.UI.Client/libs/modal/elements/modal-element.element.ts b/src/Umbraco.Web.UI.Client/libs/modal/elements/modal-element.element.ts index e51b4f959a..6abc2ff93d 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/elements/modal-element.element.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/elements/modal-element.element.ts @@ -3,7 +3,7 @@ import { UmbModalHandler } from '..'; import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-modal-element') -export class UmbModalBaseElement extends UmbLitElement { +export class UmbModalBaseElement extends UmbLitElement { @property({ attribute: false }) modalHandler?: UmbModalHandler; @@ -13,6 +13,6 @@ export class UmbModalBaseElement ext declare global { interface HTMLElementTagNameMap { - 'umb-modal-element': UmbModalBaseElement; + 'umb-modal-element': UmbModalBaseElement; } } diff --git a/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts b/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts index 01f370afcd..75aa43e2c2 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts @@ -13,7 +13,7 @@ import { ManifestModal } from '@umbraco-cms/extensions-registry'; /** * Type which omits the real submit method, and replaces it with a submit method which accepts an optional argument depending on the generic type. */ -export type UmbModalHandler = Omit< +export type UmbModalHandler = Omit< UmbModalHandlerClass, 'submit' > & @@ -35,7 +35,7 @@ type OptionalSubmitArgumentIfUndefined = T extends undefined }; //TODO consider splitting this into two separate handlers -export class UmbModalHandlerClass { +export class UmbModalHandlerClass { private _submitPromise: Promise; private _submitResolver?: (value: ModalResult) => void; private _submitRejecter?: () => void; diff --git a/src/Umbraco.Web.UI.Client/libs/modal/modal-registration-controller.ts b/src/Umbraco.Web.UI.Client/libs/modal/modal-registration-controller.ts new file mode 100644 index 0000000000..6dd45e6e52 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/modal-registration-controller.ts @@ -0,0 +1,15 @@ +import { UmbController, UmbControllerHostInterface } from '@umbraco-cms/controller'; + +/* +export class UmbModalRegistrationController extends UmbController { + constructor(host: UmbControllerHostInterface, modalAlias: string | NewType) { + super(host); + this.key = config?.key || uuidv4(); + } + + updateSetup() {} + + hostConnected(): void {} + hostDisconnected(): void {} +} +*/ diff --git a/src/Umbraco.Web.UI.Client/libs/modal/modal.context.ts b/src/Umbraco.Web.UI.Client/libs/modal/modal.context.ts index 3bf9eda86a..c7dcb4c32e 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/modal.context.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/modal.context.ts @@ -22,7 +22,7 @@ export interface UmbModalConfig { export class UmbModalContext { host: UmbControllerHostInterface; // TODO: Investigate if we can get rid of HTML elements in our store, so we can use one of our states. - #modals = new BehaviorSubject(>>[]); + #modals = new BehaviorSubject(>[]); public readonly modals = this.#modals.asObservable(); constructor(host: UmbControllerHostInterface) { @@ -76,7 +76,7 @@ export class UmbModalContext { * @return {*} {UmbModalHandler} * @memberof UmbModalContext */ - public open( + public open( modalAlias: string | UmbModalToken, data?: ModalData, config?: UmbModalConfig diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts index 6dc7240081..99510c47a8 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts @@ -1,6 +1,6 @@ import { UmbModalConfig } from '../modal.context'; -export class UmbModalToken { +export class UmbModalToken { /** * Get the data type of the token's data. * diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.element.ts index 7ba2791d09..f6e2cabcb6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.element.ts @@ -1,6 +1,6 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; import { UmbModalRouteBuilder, UmbRouteContext, UMB_ROUTE_CONTEXT_TOKEN } from '@umbraco-cms/router'; @@ -27,6 +27,10 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen protected getFormElement() { return undefined; } + + @property() + alias?: string; + /** * This is a minimum amount of selected items in this input. * @type {number} @@ -99,6 +103,7 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen //private _modalContext?: UmbModalContext; private _routeContext?: UmbRouteContext; + @state() private _linkPickerURL?: UmbModalRouteBuilder; constructor() { @@ -114,21 +119,19 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen () => !!this.max && this.urls.length > this.max ); - /* - this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { - this._modalContext = instance; - }); - */ - this.consumeContext(UMB_ROUTE_CONTEXT_TOKEN, (instance) => { this._routeContext = instance; // Registre the routes of this UI: - // TODO: To avoid retriving the property alias, we might make use of the property context? + // TODO: Make a registreModal method on the property context // Or maybe its not the property-alias, but something unique? as this might not be in a property?. - this._linkPickerURL = this._routeContext.registerModal(UMB_LINK_PICKER_MODAL_TOKEN, { - path: `${'to-do-myPropertyAlias'}/:index`, + + this._routeContext.registerModal(UMB_LINK_PICKER_MODAL_TOKEN, { + path: `${'this.alias'}/:index`, onSetup: (routingInfo) => { + console.log('call onSetup'); + // TODO: Make onSetup optional. + // TODO: Maybe use UmbRouteLocation? // Get index from routeInfo: const indexParam = routingInfo.match.params.index; if (!indexParam) return false; @@ -147,8 +150,9 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen console.log('onSetup modal got data:', data); - const modalData = { + return { index: index, + lol: false, link: { name: data?.name, published: data?.published, @@ -161,10 +165,9 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen config: { hideAnchor: this.hideAnchor, ignoreUserStartNodes: this.ignoreUserStartNodes, - overlaySize: this.overlaySize || 'small', + overlaySize: this.overlaySize || 'small', // TODO: this should not be here, but use the ModalToken. }, }; - return modalData; }, onSubmit: (submitData) => { console.log('On submit in property editor input'); @@ -174,6 +177,10 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen onReject: () => { console.log('User cancelled dialog.'); }, + onUrlBuilder: (urlBuilder) => { + console.log('got onUrlBuilder'); + this._linkPickerURL = urlBuilder; + }, }); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/index.ts index c3719e1789..d8b1d5db61 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/index.ts @@ -17,7 +17,7 @@ export interface UmbPropertySettingsModalResult { }; } -export const UMB_PROPERTY_SETTINGS_MODAL_TOKEN = new UmbModalToken( +export const UMB_PROPERTY_SETTINGS_MODAL_TOKEN = new UmbModalToken( 'Umb.Modal.PropertySettings', { type: 'sidebar', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts index 9aa4b3f305..ba166b2deb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts @@ -9,7 +9,7 @@ import { ManifestPropertyEditorUI } from '@umbraco-cms/extensions-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; @customElement('umb-property-settings-modal') -export class UmbPropertySettingsModalElement extends UmbModalBaseElement { +export class UmbPropertySettingsModalElement extends UmbModalBaseElement { static styles = [ UUITextStyles, css` diff --git a/src/Umbraco.Web.UI.Client/src/core/router/route.context.ts b/src/Umbraco.Web.UI.Client/src/core/router/route.context.ts index 3fbc8458cb..58029cf4b8 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/route.context.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/route.context.ts @@ -5,75 +5,132 @@ import { UmbModalToken, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; const EmptyDiv = document.createElement('div'); +// Get the second generic type of UmbModalToken: +type GetDataType = T extends UmbModalToken ? Data : unknown; + +// Get the second generic type of UmbModalToken: +type GetResultType = T extends UmbModalToken ? Result : unknown; + // TODO: Consider accepting the Token as a generic: -export type UmbModalRoute = { +export type UmbModalRouteOptions = { path: string; onSetup: (routingInfo: IRoutingInfo) => UmbModalTokenData | false; onSubmit: (data: UmbModalTokenResult) => void | PromiseLike; onReject: () => void; + onUrlBuilder?: (urlBuilder: UmbModalRouteBuilder) => void; +}; + +type UmbModalRouteRegistration = { + alias: UmbModalToken | string; + options: UmbModalRouteOptions; + routeSetup: (component: HTMLElement, info: IRoutingInfo) => void; }; export type UmbModalRouteBuilder = (params: { [key: string]: string | number }) => string; export class UmbRouteContext { - #host: UmbControllerHostInterface; + //#host: UmbControllerHostInterface; + #modalRegistrations: UmbModalRouteRegistration[] = []; #modalContext?: typeof UMB_MODAL_CONTEXT_TOKEN.TYPE; #contextRoutes: IRoute[] = []; + #routerBasePath?: string; constructor(host: UmbControllerHostInterface, private _onGotModals: (contextRoutes: any) => void) { - this.#host = host; + //this.#host = host; new UmbContextProviderController(host, UMB_ROUTE_CONTEXT_TOKEN, this); /*new UmbContextConsumerController(host, UMB_ROUTE_CONTEXT_TOKEN, (context) => { console.log('got a parent', this === context, this, context); + // Why did i want a parent route? was it to capture and inherit routes? + // This is maybe not so necessary as it stands right now, so lets see how it goes. });*/ new UmbContextConsumerController(host, UMB_MODAL_CONTEXT_TOKEN, (context) => { this.#modalContext = context; + this.#generateContextRoutes(); }); // Consider using this event, to stay up to date with current full-URL. which is necessary for Modal opening. - // window.addEventListener('navigationsuccess', this._onNavigationChanged); + window.addEventListener('navigationsuccess', this.#onNavigationChanged as unknown as EventListener); } - public registerModal( - modalAlias: UmbModalToken | string, - options: UmbModalRoute - ): UmbModalRouteBuilder { - const localPath = `modal/${modalAlias.toString()}/${options.path}`; - this.#contextRoutes.push({ + #removeModalPath(info: IRoutingInfo) { + window.history.pushState({}, '', window.location.href.split(info.match.fragments.consumed)[0]); + } + + #generateRoute(modalRegistration: UmbModalRouteRegistration): IRoute { + const localPath = `modal/${modalRegistration.alias.toString()}/${modalRegistration.options.path}`; + return { path: localPath, pathMatch: 'suffix', component: EmptyDiv, - setup: (component, info) => { + setup: modalRegistration.routeSetup, + }; + } + + #generateContextRoutes() { + this.#contextRoutes = this.#modalRegistrations.map((modalRegistration) => { + return this.#generateRoute(modalRegistration); + }); + + // TODO: Should we await one frame, to ensure we don't call back too much?. + this._onGotModals(this.#contextRoutes); + } + + #onNavigationChanged = (event: Event) => { + console.log('route got navigation changed', event); + this.#generateNewURLs(); + }; + + #generateNewURLs() { + this.#modalRegistrations.forEach(this.#generateNewURL); + } + + #generateNewURL = (modalRegistration: UmbModalRouteRegistration) => { + console.log('#generateNewURL', this); + if (!modalRegistration.options.onUrlBuilder || !this.#routerBasePath) return; + + const routeBasePath = this.#routerBasePath.endsWith('/') ? this.#routerBasePath : this.#routerBasePath + '/'; + const localPath = `modal/${modalRegistration.alias.toString()}/${modalRegistration.options.path}`; + + const urlBuilder = (params: { [key: string]: string | number }) => { + const localRoutePath = stripSlash( + localPath.replace(PARAM_IDENTIFIER, (substring: string, ...args: string[]) => { + return params[args[0]].toString(); + //return `([^\/]+)`; + }) + ); + return routeBasePath + localRoutePath; + }; + + modalRegistration.options.onUrlBuilder(urlBuilder); + }; + public registerModal( + alias: UmbModalToken | string, + options: UmbModalRouteOptions + ) { + const registration = { + alias: alias, + options: options, + routeSetup: (component: HTMLElement, info: IRoutingInfo) => { const modalData = options.onSetup(info); if (modalData && this.#modalContext) { - const modalHandler = this.#modalContext.open(modalAlias, modalData); + const modalHandler = this.#modalContext.open(alias, modalData); modalHandler.onSubmit().then( - () => this._removeModalPath(info), - () => this._removeModalPath(info) + () => this.#removeModalPath(info), + () => this.#removeModalPath(info) ); modalHandler.onSubmit().then(options.onSubmit, options.onReject); } }, - }); - - //TODO: move to a method: - this._onGotModals(this.#contextRoutes); - - return (params: { [key: string]: string | number }) => { - const localRoutePath = stripSlash( - localPath.replace(PARAM_IDENTIFIER, (substring: string, ...args: string[]) => { - return params[args[0]]; - //return `([^\/]+)`; - }) - ); - const baseRoutePath = window.location.href; - return (baseRoutePath.endsWith('/') ? baseRoutePath : baseRoutePath + '/') + localRoutePath; }; + this.#modalRegistrations.push(registration); + this.#generateNewURL(registration); + this.#generateContextRoutes(); } - private _removeModalPath(info: IRoutingInfo) { - window.history.pushState({}, '', window.location.href.split(info.match.fragments.consumed)[0]); - console.log('ask to remove path', info.match.fragments.consumed); + public _internal_routerGotBasePath(routerBasePath: string) { + if (this.#routerBasePath === routerBasePath) return; + this.#routerBasePath = routerBasePath; + this.#generateNewURLs(); } } diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router-slot.element.ts b/src/Umbraco.Web.UI.Client/src/core/router/router-slot.element.ts index fde2927bc8..07ab280e56 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/router-slot.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/router-slot.element.ts @@ -76,6 +76,7 @@ export class UmbRouterSlotElement extends UmbLitElement { protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { super.firstUpdated(_changedProperties); this._routerPath = this.#router.constructAbsolutePath('') || ''; + this.#routeContext._internal_routerGotBasePath(this._routerPath); this.dispatchEvent(new UmbRouterSlotInitEvent()); } @@ -83,6 +84,7 @@ export class UmbRouterSlotElement extends UmbLitElement { const newAbsolutePath = this.#router.constructAbsolutePath('') || ''; if (this._routerPath !== newAbsolutePath) { this._routerPath = newAbsolutePath; + this.#routeContext._internal_routerGotBasePath(this._routerPath); this.dispatchEvent(new UmbRouterSlotInitEvent()); const newActiveLocalPath = this.#router.match?.route.path;