Merge pull request #1507 from enkelmedia/1506-custom-modal

Feature/Proposal: Added support for element factory for modal manager context
This commit is contained in:
Niels Lyngsø
2024-11-07 22:22:43 +01:00
committed by GitHub
11 changed files with 244 additions and 45 deletions

View File

@@ -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`
<uui-box>
<p>Open the custom modal</p>
<uui-button look="primary" @click=${this.#onOpenModal}>Open Modal</uui-button>
</uui-box>
`;
}
static override styles = [css`
:host{
display:block;
padding:20px;
}
`];
}
export default UmbExampleCustomModalDashboardElement
declare global {
interface HTMLElementTagNameMap {
'example-custom-modal-dashboard': UmbExampleCustomModalDashboardElement;
}
}

View File

@@ -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`
<dialog>
<h2>Custom Modal-wrapper</h2>
<slot></slot>
</dialog>
`;
}
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;
}
}

View File

@@ -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'),
}
});

View File

@@ -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<ExampleModalData, ExampleModalResult>;
onClickDone(){
this.modalContext?.submit();
}
override render() {
return html`
<div id="modal">
<p>Example content of custom modal element</p>
<uui-button look="primary" label="Submit modal" @click=${() => this.onClickDone()}></uui-button>
</div>
`;
}
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;
}
}

View File

@@ -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];

View File

@@ -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',

View File

@@ -13,7 +13,7 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
@state()
_modals: Array<UmbModalContext> = [];
@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<UmbModalContext>) {
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');
});
}

View File

@@ -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<HTMLElement | undefined>(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();
}

View File

@@ -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<UUIModalElement>;
/**
* Set the background property of the modal backdrop
*/

View File

@@ -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<UUIModalElement>;
public readonly backdropBackground?: string;
public readonly router: IRouterSlot | null = null;
public readonly alias: string | UmbModalToken<ModalData, ModalValue>;
@@ -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;

View File

@@ -150,6 +150,11 @@ export class UmbRouteContext extends UmbContextBase<UmbRouteContext> {
modalRegistration._internal_setRouteBuilder(urlBuilder);
};
override hostDisconnected(): void {
super.hostDisconnected();
this._internal_modalRouterChanged(undefined);
}
}
export const UMB_ROUTE_CONTEXT = new UmbContextToken<UmbRouteContext>('UmbRouterContext');