diff --git a/examples/custom-modal/example-custom-modal-dashboard.element.ts b/examples/custom-modal/example-custom-modal-dashboard.element.ts new file mode 100644 index 0000000000..1606e9aab2 --- /dev/null +++ b/examples/custom-modal/example-custom-modal-dashboard.element.ts @@ -0,0 +1,46 @@ +import { EXAMPLE_MODAL_TOKEN, type ExampleModalData, type ExampleModalResult } from './example-modal-token.js'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import './example-custom-modal-element.element.js'; + +@customElement('example-custom-modal-dashboard') +export class UmbExampleCustomModalDashboardElement extends UmbLitElement { + + #modalManagerContext? : typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; + + constructor() { + super(); + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT,(instance)=>{ + this.#modalManagerContext = instance; + }) + } + + #onOpenModal(){ + this.#modalManagerContext?.open(this,EXAMPLE_MODAL_TOKEN,{}) + } + + override render() { + return html` + +

Open the custom modal

+ Open Modal +
+ `; + } + + static override styles = [css` + :host{ + display:block; + padding:20px; + } + `]; +} + +export default UmbExampleCustomModalDashboardElement + +declare global { + interface HTMLElementTagNameMap { + 'example-custom-modal-dashboard': UmbExampleCustomModalDashboardElement; + } +} diff --git a/examples/custom-modal/example-custom-modal-element.element.ts b/examples/custom-modal/example-custom-modal-element.element.ts new file mode 100644 index 0000000000..4892d6af18 --- /dev/null +++ b/examples/custom-modal/example-custom-modal-element.element.ts @@ -0,0 +1,50 @@ +import { css, html } from "@umbraco-cms/backoffice/external/lit"; +import { defineElement, UUIModalElement } from "@umbraco-cms/backoffice/external/uui"; + +/** + * This class defines a custom design for the modal it self, in the same was as + * UUIModalSidebarElement and UUIModalDialogElement. + */ +@defineElement('example-modal-element') +export class UmbExampleCustomModalElement extends UUIModalElement { + override render() { + return html` + +

Custom Modal-wrapper

+ +
+ `; + } + + static override styles = [ + ...UUIModalElement.styles, + css` + dialog { + width:100%; + height:100%; + max-width: 100%; + max-height: 100%; + top:0; + left:0; + right:0; + bottom:0; + background:#fff; + } + :host([index='0']) dialog { + box-shadow: var(--uui-shadow-depth-5); + } + :host(:not([index='0'])) dialog { + outline: 1px solid rgba(0, 0, 0, 0.1); + } + + `, + ]; +} + +export default UmbExampleCustomModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'example-modal-element': UmbExampleCustomModalElement; + } +} diff --git a/examples/custom-modal/example-modal-token.ts b/examples/custom-modal/example-modal-token.ts new file mode 100644 index 0000000000..84a643f051 --- /dev/null +++ b/examples/custom-modal/example-modal-token.ts @@ -0,0 +1,19 @@ +import { UmbModalToken } from "@umbraco-cms/backoffice/modal"; + +export interface ExampleModalData { + unique: string | null; +} + +export interface ExampleModalResult { + text : string; +} + +export const EXAMPLE_MODAL_TOKEN = new UmbModalToken< +ExampleModalData, +ExampleModalResult +>('example.modal.custom.element', { + modal : { + type : 'custom', + element: () => import('./example-custom-modal-element.element.js'), + } +}); diff --git a/examples/custom-modal/example-modal-view.element.ts b/examples/custom-modal/example-modal-view.element.ts new file mode 100644 index 0000000000..8bca63cd6f --- /dev/null +++ b/examples/custom-modal/example-modal-view.element.ts @@ -0,0 +1,51 @@ +import type { ExampleModalData, ExampleModalResult } from './example-modal-token.js'; +import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; +import './example-custom-modal-element.element.js'; + +@customElement('example-modal-view') +export class UmbExampleModalViewElement extends UmbLitElement { + + @property({ attribute: false }) + public modalContext?: UmbModalContext; + + onClickDone(){ + this.modalContext?.submit(); + } + + override render() { + return html` + + `; + } + + static override styles = [css` + :host { + background: #eaeaea; + display: block; + box-sizing:border-box; + } + + #modal { + box-sizing:border-box; + } + + p { + margin:0; + padding:0; + } + + `]; +} + +export default UmbExampleModalViewElement + +declare global { + interface HTMLElementTagNameMap { + 'example-modal-view': UmbExampleModalViewElement; + } +} diff --git a/examples/custom-modal/index.ts b/examples/custom-modal/index.ts new file mode 100644 index 0000000000..2ddf119f38 --- /dev/null +++ b/examples/custom-modal/index.ts @@ -0,0 +1,29 @@ +import type { ManifestDashboard } from '@umbraco-cms/backoffice/dashboard'; +import type { ManifestModal } from '@umbraco-cms/backoffice/modal'; + +const demoModal : ManifestModal = { + type: 'modal', + name: 'Example Custom Modal Element', + alias: 'example.modal.custom.element', + js: () => import('./example-modal-view.element.js'), +} + +const demoModalsDashboard : ManifestDashboard = { + type: 'dashboard', + name: 'Example Custom Modal Dashboard', + alias: 'example.dashboard.custom.modal.element', + element: () => import('./example-custom-modal-dashboard.element.js'), + weight: 900, + meta: { + label: 'Custom Modal', + pathname: 'custom-modal', + }, + conditions : [ + { + alias: 'Umb.Condition.SectionAlias', + match: 'Umb.Section.Content' + } + ] +} + +export default [demoModal,demoModalsDashboard]; diff --git a/examples/modal-routed/index.ts b/examples/modal-routed/index.ts index 5aca09fe79..3b2f6ee704 100644 --- a/examples/modal-routed/index.ts +++ b/examples/modal-routed/index.ts @@ -1,14 +1,5 @@ -import type { ManifestDashboard, ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; - -// const section : ManifestSection = { -// type: "section", -// alias: 'demo.section', -// name: "Demo Section", -// meta: { -// label: "Demo", -// pathname: "demo" -// } -// } +import type { ManifestDashboard } from '@umbraco-cms/backoffice/dashboard'; +import type { ManifestModal } from '@umbraco-cms/backoffice/modal'; const dashboard: ManifestDashboard = { type: 'dashboard', diff --git a/src/packages/core/components/backoffice-modal-container/backoffice-modal-container.element.ts b/src/packages/core/components/backoffice-modal-container/backoffice-modal-container.element.ts index 8ce49a89a3..61cd3892a0 100644 --- a/src/packages/core/components/backoffice-modal-container/backoffice-modal-container.element.ts +++ b/src/packages/core/components/backoffice-modal-container/backoffice-modal-container.element.ts @@ -13,7 +13,7 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement { @state() _modals: Array = []; - @property({ reflect: true, attribute: 'fill-background' }) + @property({ type: Boolean, reflect: true, attribute: 'fill-background' }) fillBackground = false; private _modalManager?: UmbModalManagerContext; @@ -41,7 +41,7 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement { * @param modals */ #createModalElements(modals: Array) { - this.removeAttribute('fill-background'); + this.fillBackground = false; const oldValue = this._modals; this._modals = modals; @@ -58,26 +58,26 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement { return; } - this._modals.forEach((modal) => { - if (this._modalElementMap.has(modal.key)) return; + this._modals.forEach(async (modalContext) => { + if (this._modalElementMap.has(modalContext.key)) return; const modalElement = new UmbModalElement(); - modalElement.modalContext = modal; + await modalElement.init(modalContext); - modalElement.element?.addEventListener('close-end', this.#onCloseEnd.bind(this, modal.key)); - modal.addEventListener('umb:destroy', this.#onCloseEnd.bind(this, modal.key)); + modalElement.element?.addEventListener('close-end', this.#onCloseEnd.bind(this, modalContext.key)); + modalContext.addEventListener('umb:destroy', this.#onCloseEnd.bind(this, modalContext.key)); - this._modalElementMap.set(modal.key, modalElement); + this._modalElementMap.set(modalContext.key, modalElement); // If any of the modals are fillBackground, set the fillBackground property to true - if (modal.backdropBackground) { + if (modalContext.backdropBackground) { this.fillBackground = true; this.shadowRoot ?.getElementById('container') - ?.style.setProperty('--backdrop-background', modal.backdropBackground); + ?.style.setProperty('--backdrop-background', modalContext.backdropBackground); } - this.requestUpdate(); + this.requestUpdate('_modalElementMap'); }); } diff --git a/src/packages/core/modal/component/modal.element.ts b/src/packages/core/modal/component/modal.element.ts index 665a7c6d21..8df92878c7 100644 --- a/src/packages/core/modal/component/modal.element.ts +++ b/src/packages/core/modal/component/modal.element.ts @@ -9,12 +9,13 @@ import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbBasicState, type UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { UUIModalCloseEvent, + type UUIModalElement, type UUIDialogElement, type UUIModalDialogElement, type UUIModalSidebarElement, } from '@umbraco-cms/backoffice/external/uui'; import { UMB_ROUTE_CONTEXT, type UmbRouterSlotElement } from '@umbraco-cms/backoffice/router'; -import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; +import { createExtensionElement, loadManifestElement } from '@umbraco-cms/backoffice/extension-api'; import type { UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api'; import { UMB_CONTEXT_REQUEST_EVENT_TYPE, @@ -25,22 +26,8 @@ import { @customElement('umb-modal') export class UmbModalElement extends UmbLitElement { #modalContext?: UmbModalContext; - public get modalContext(): UmbModalContext | undefined { - return this.#modalContext; - } - public set modalContext(value: UmbModalContext | undefined) { - if (this.#modalContext === value) return; - this.#modalContext = value; - - if (!value) { - this.destroy(); - return; - } - - this.#createModalElement(); - } - public element?: UUIModalDialogElement | UUIModalSidebarElement; + public element?: UUIModalDialogElement | UUIModalSidebarElement | UUIModalElement; #innerElement = new UmbBasicState(undefined); @@ -52,11 +39,17 @@ export class UmbModalElement extends UmbLitElement { this.#modalContext?.reject({ type: 'close' }); }; - #createModalElement() { - if (!this.#modalContext) return; + async init(modalContext: UmbModalContext | undefined) { + if (this.#modalContext === modalContext) return; + this.#modalContext = modalContext; + + if (!this.#modalContext) { + this.destroy(); + return; + } this.#modalContext.addEventListener('umb:destroy', this.#onContextDestroy); - this.element = this.#createContainerElement(); + this.element = await this.#createContainerElement(); // Makes sure that the modal triggers the reject of the context promise when it is closed by pressing escape. this.element.addEventListener(UUIModalCloseEvent, this.#onClose); @@ -113,7 +106,12 @@ export class UmbModalElement extends UmbLitElement { provider.hostConnected(); } - #createContainerElement() { + async #createContainerElement() { + if (this.#modalContext!.type == 'custom' && this.#modalContext?.element) { + const customWrapperElementCtor = await loadManifestElement(this.#modalContext.element); + return new customWrapperElementCtor!(); + } + return this.#modalContext!.type === 'sidebar' ? this.#createSidebarElement() : this.#createDialogElement(); } diff --git a/src/packages/core/modal/context/modal-manager.context.ts b/src/packages/core/modal/context/modal-manager.context.ts index 162ba3e476..41c35e2f3f 100644 --- a/src/packages/core/modal/context/modal-manager.context.ts +++ b/src/packages/core/modal/context/modal-manager.context.ts @@ -1,18 +1,24 @@ import type { UmbModalToken } from '../token/modal-token.js'; import { UmbModalContext, type UmbModalContextClassArgs } from './modal.context.js'; -import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIModalElement, UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; import { UmbBasicState, appendToFrozenArray } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { ElementLoaderProperty } from '@umbraco-cms/backoffice/extension-api'; -export type UmbModalType = 'dialog' | 'sidebar'; +export type UmbModalType = 'dialog' | 'sidebar' | 'custom'; export interface UmbModalConfig { key?: string; type?: UmbModalType; size?: UUIModalSidebarSize; + /** + * Used to provide a custom modal element to replace the default uui-modal-dialog or uui-modal-sidebar + */ + element?: ElementLoaderProperty; + /** * Set the background property of the modal backdrop */ diff --git a/src/packages/core/modal/context/modal.context.ts b/src/packages/core/modal/context/modal.context.ts index baded34c3d..c4ff23d8d9 100644 --- a/src/packages/core/modal/context/modal.context.ts +++ b/src/packages/core/modal/context/modal.context.ts @@ -2,11 +2,12 @@ import { UmbModalToken } from '../token/modal-token.js'; import type { UmbModalConfig, UmbModalType } from './modal-manager.context.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { IRouterSlot } from '@umbraco-cms/backoffice/external/router-slot'; -import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIModalElement, UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { type UmbDeepPartialObject, umbDeepMerge } from '@umbraco-cms/backoffice/utils'; +import { type ElementLoaderProperty } from '@umbraco-cms/backoffice/extension-api'; export interface UmbModalRejectReason { type: string; @@ -38,6 +39,7 @@ export class UmbModalContext< public readonly data: ModalData; public readonly type: UmbModalType = 'dialog'; public readonly size: UUIModalSidebarSize = 'small'; + public element?: ElementLoaderProperty; public readonly backdropBackground?: string; public readonly router: IRouterSlot | null = null; public readonly alias: string | UmbModalToken; @@ -58,11 +60,13 @@ export class UmbModalContext< if (this.alias instanceof UmbModalToken) { this.type = this.alias.getDefaultModal()?.type || this.type; this.size = this.alias.getDefaultModal()?.size || this.size; + this.element = this.alias.getDefaultModal()?.element || this.element; this.backdropBackground = this.alias.getDefaultModal()?.backdropBackground || this.backdropBackground; } this.type = args.modal?.type || this.type; this.size = args.modal?.size || this.size; + this.element = args.modal?.element || this.element; this.backdropBackground = args.modal?.backdropBackground || this.backdropBackground; const defaultData = this.alias instanceof UmbModalToken ? this.alias.getDefaultData() : undefined; diff --git a/src/packages/core/router/route.context.ts b/src/packages/core/router/route.context.ts index f8f15291d4..5e176c8dcf 100644 --- a/src/packages/core/router/route.context.ts +++ b/src/packages/core/router/route.context.ts @@ -150,6 +150,11 @@ export class UmbRouteContext extends UmbContextBase { modalRegistration._internal_setRouteBuilder(urlBuilder); }; + + override hostDisconnected(): void { + super.hostDisconnected(); + this._internal_modalRouterChanged(undefined); + } } export const UMB_ROUTE_CONTEXT = new UmbContextToken('UmbRouterContext');