diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-modal-container.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-modal-container.element.ts index 6b90621a08..c738054155 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-modal-container.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-modal-container.element.ts @@ -4,7 +4,7 @@ import { customElement, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { Subscription } from 'rxjs'; import { UmbContextConsumerMixin } from '../../core/context'; -import { UmbModalService } from '../../core/services/modal'; +import { UmbModalHandler, UmbModalService } from '../../core/services/modal'; @customElement('umb-backoffice-modal-container') export class UmbBackofficeModalContainer extends UmbContextConsumerMixin(LitElement) { @@ -18,7 +18,7 @@ export class UmbBackofficeModalContainer extends UmbContextConsumerMixin(LitElem ]; @state() - private _modals: any[] = []; + private _modals: UmbModalHandler[] = []; private _modalService?: UmbModalService; private _modalSubscription?: Subscription; @@ -29,9 +29,8 @@ export class UmbBackofficeModalContainer extends UmbContextConsumerMixin(LitElem this.consumeContext('umbModalService', (modalService: UmbModalService) => { this._modalService = modalService; this._modalSubscription?.unsubscribe(); - this._modalService?.modals.subscribe((modals: Array) => { + this._modalService?.modals.subscribe((modals: Array) => { this._modals = modals; - console.log('modals', modals); }); }); } @@ -43,7 +42,9 @@ export class UmbBackofficeModalContainer extends UmbContextConsumerMixin(LitElem render() { return html` - ${repeat(this._modals, (modal) => html`${modal.modal}`)} + + ${repeat(this._modals, (modalHandler) => html`${modalHandler.element}`)})})} + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/property-editor-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/property-editor-content-picker.element.ts index eb959a2389..bb863d7443 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/property-editor-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/property-editor-content-picker.element.ts @@ -2,14 +2,14 @@ import { css, html, LitElement } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; +import { UmbContextConsumerMixin } from '../../core/context'; +import { UmbModalService } from '../../core/services/modal'; + +// TODO: remove these imports when they are part of UUI import '@umbraco-ui/uui-modal'; import '@umbraco-ui/uui-modal-sidebar'; import '@umbraco-ui/uui-modal-container'; import '@umbraco-ui/uui-modal-dialog'; -import { UmbContextConsumerMixin } from '../../core/context'; -import { UmbModalService } from '../../core/services/modal'; - -import './modal-content-picker.element'; @customElement('umb-property-editor-content-picker') export class UmbPropertyEditorContentPicker extends UmbContextConsumerMixin(LitElement) { @@ -50,23 +50,34 @@ export class UmbPropertyEditorContentPicker extends UmbContextConsumerMixin(LitE } private _open() { - const modalHandler = this._modalService?.openSidebar('umb-modal-content-picker', { size: 'small' }); - modalHandler?.onClose.then((result) => { - this._selectedContent = [...this._selectedContent, ...result]; + const modalHandler = this._modalService?.contentPicker({ multiple: true }); + modalHandler?.onClose.then(({ selection }: any) => { + this._selectedContent = [...this._selectedContent, ...selection]; this.requestUpdate('_selectedContent'); }); } - private _removeContent(index: number) { - this._selectedContent.splice(index, 1); - this.requestUpdate('_selectedContent'); + private _removeContent(index: number, content: any) { + const modalHandler = this._modalService?.confirm({ + color: 'danger', + headline: 'Remove', + confirmLabel: 'Remove', + content: html`Remove ${content.name}?`, + }); + + modalHandler?.onClose.then(({ confirmed }) => { + if (confirmed) { + this._selectedContent.splice(index, 1); + this.requestUpdate('_selectedContent'); + } + }); } private _renderContent(content: any, index: number) { return html` - this._removeContent(index)}>Remove + this._removeContent(index, content)}>Remove `; diff --git a/src/Umbraco.Web.UI.Client/src/core/services/modal/layouts/confirm/modal-layout-confirm.element.ts b/src/Umbraco.Web.UI.Client/src/core/services/modal/layouts/confirm/modal-layout-confirm.element.ts new file mode 100644 index 0000000000..b7bb8cd29a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/services/modal/layouts/confirm/modal-layout-confirm.element.ts @@ -0,0 +1,53 @@ +import { html, LitElement, TemplateResult } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbModalHandler } from '../../../modal'; + +export interface UmbModalConfirmData { + headline: string; + content: TemplateResult | string; + color?: 'positive' | 'danger'; + confirmLabel?: string; +} + +@customElement('umb-modal-layout-confirm') +export class UmbModelLayoutConfirmElement extends LitElement { + static styles = [UUITextStyles]; + + @property({ attribute: false }) + modalHandler!: UmbModalHandler; + + @property({ type: Object }) + data!: UmbModalConfirmData; + + private _handleConfirm() { + this.modalHandler.close({ confirmed: true }); + } + + private _handleCancel() { + this.modalHandler.close({ confirmed: false }); + } + + render() { + return html` + + ${this.data?.content} + + Cancel + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-modal-layout-confirm': UmbModelLayoutConfirmElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/modal-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/core/services/modal/layouts/content-picker/modal-layout-content-picker.element.ts similarity index 86% rename from src/Umbraco.Web.UI.Client/src/backoffice/property-editors/modal-content-picker.element.ts rename to src/Umbraco.Web.UI.Client/src/core/services/modal/layouts/content-picker/modal-layout-content-picker.element.ts index c318fb07bf..0a767b34f1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/modal-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/services/modal/layouts/content-picker/modal-layout-content-picker.element.ts @@ -1,10 +1,14 @@ import { css, html, LitElement } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; -import { UmbModalHandler } from '../../core/services/modal'; +import { UmbModalHandler } from '../../../modal'; -@customElement('umb-modal-content-picker') -class UmbModalContentPicker extends LitElement { +export interface UmbModalContentPickerData { + multiple: boolean; +} + +@customElement('umb-modal-layout-content-picker') +export class UmbModalContentPickerElement extends LitElement { static styles = [ UUITextStyles, css` @@ -78,7 +82,7 @@ class UmbModalContentPicker extends LitElement { } private _submit() { - this.modalHandler?.close(this._selectedContent); + this.modalHandler?.close({ selection: this._selectedContent }); } private _close() { @@ -115,6 +119,6 @@ class UmbModalContentPicker extends LitElement { declare global { interface HTMLElementTagNameMap { - 'umb-modal-content-picker': UmbModalContentPicker; + 'umb-modal-layout-content-picker': UmbModalContentPickerElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/services/modal/modal-handler.ts b/src/Umbraco.Web.UI.Client/src/core/services/modal/modal-handler.ts index 132af0aae5..7f5877a41d 100644 --- a/src/Umbraco.Web.UI.Client/src/core/services/modal/modal-handler.ts +++ b/src/Umbraco.Web.UI.Client/src/core/services/modal/modal-handler.ts @@ -1,42 +1,63 @@ -import { html, render } from 'lit'; +import { UUIDialogElement } from '@umbraco-ui/uui'; +import { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog'; +import { UUIModalSidebarElement, UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; +import { v4 as uuidv4 } from 'uuid'; +import { UmbModalOptions } from './modal.service'; //TODO consider splitting this into two separate handlers export class UmbModalHandler { private _closeResolver: any; private _closePromise: any; - public element?: any; + public element: UUIModalDialogElement | UUIModalSidebarElement; public key: string; - public modal: any; + public type: string; + public size: UUIModalSidebarSize; + + constructor(elementName: string, options: UmbModalOptions) { + this.key = uuidv4(); + + this.type = options.type || 'dialog'; + this.size = options.size || 'small'; + this.element = this._createElement(elementName, options); - constructor(elementName: string, modalElementName: string, modalOptions?: any) { - this.key = Date.now().toString(); //TODO better key - this._createLayoutElement(elementName, modalElementName, modalOptions); this._closePromise = new Promise((resolve) => { this._closeResolver = resolve; }); } - private _createLayoutElement(elementName: string, modalElementName: string, modalOptions?: any) { - this.modal = document.createElement(modalElementName); - this.modal.addEventListener('close-end', () => { - this._closeResolver(); - }); + private _createElement(elementName: string, options: UmbModalOptions) { + const layoutElement = this._createLayoutElement(elementName, options); + return options.type === 'sidebar' + ? this._createSidebarElement(layoutElement) + : this._createDialogElement(layoutElement); + } - if (modalOptions) { - // Apply modal options as attributes on the modal - Object.keys(modalOptions).forEach((option) => { - this.modal.setAttribute(option, modalOptions[option]); - }); - } + private _createSidebarElement(layoutElement: HTMLElement) { + const sidebarElement = document.createElement('uui-modal-sidebar'); + sidebarElement.appendChild(layoutElement); + sidebarElement.size = this.size; + return sidebarElement; + } - this.element = document.createElement(elementName); - this.modal.appendChild(this.element); - this.element.modalHandler = this; + private _createDialogElement(layoutElement: HTMLElement) { + const modalDialogElement = document.createElement('uui-modal-dialog'); + const dialogElement: UUIDialogElement = document.createElement('uui-dialog'); + modalDialogElement.appendChild(dialogElement); + dialogElement.appendChild(layoutElement); + return modalDialogElement; + } + + private _createLayoutElement(elementName: string, options: UmbModalOptions) { + const layoutElement: any = document.createElement(elementName); + layoutElement.data = options.data; + layoutElement.modalHandler = this; + return layoutElement; } public close(...args: any) { this._closeResolver(...args); + this.element.close(); } public get onClose(): Promise { diff --git a/src/Umbraco.Web.UI.Client/src/core/services/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/services/modal/modal.service.ts index da80f024af..edc56327fa 100644 --- a/src/Umbraco.Web.UI.Client/src/core/services/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/services/modal/modal.service.ts @@ -1,26 +1,48 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { UmbModalHandler } from './'; +import { UmbModalConfirmData } from './layouts/confirm/modal-layout-confirm.element'; +import { UmbModalContentPickerData } from './layouts/content-picker/modal-layout-content-picker.element'; +import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; + +// TODO: lazy load +import './layouts/confirm/modal-layout-confirm.element'; +import './layouts/content-picker/modal-layout-content-picker.element'; + +export type UmbModelType = 'dialog' | 'sidebar'; + +export interface UmbModalOptions { + type?: UmbModelType; + size?: UUIModalSidebarSize; + data: UmbModalData; +} export class UmbModalService { private _modals: BehaviorSubject> = new BehaviorSubject(>[]); public readonly modals: Observable> = this._modals.asObservable(); - public openSidebar(elementName: string, modalOptions?: any): UmbModalHandler { - return this._open(elementName, 'uui-modal-sidebar', modalOptions); + public confirm(data: UmbModalConfirmData): UmbModalHandler { + return this.open('umb-modal-layout-confirm', { data, type: 'dialog' }); } - public openDialog(elementName: string, modalOptions?: any): UmbModalHandler { - return this._open(elementName, 'uui-modal-dialog', modalOptions); + public contentPicker(data: UmbModalContentPickerData): UmbModalHandler { + return this.open('umb-modal-layout-content-picker', { data, type: 'sidebar', size: 'small' }); } - private _open(elementName: string, modalElementName: string, modalOptions?: any): UmbModalHandler { - const modalHandler = new UmbModalHandler(elementName, modalElementName, modalOptions); - modalHandler.onClose.then(() => this._close(modalHandler)); + public open(elementName: string, options: UmbModalOptions): UmbModalHandler { + const modalHandler = new UmbModalHandler(elementName, options); + + modalHandler.element.addEventListener('close-end', () => this._handleCloseEnd(modalHandler)); + this._modals.next([...this._modals.getValue(), modalHandler]); return modalHandler; } - private _close(modalHandler: UmbModalHandler) { - this._modals.next(this._modals.getValue().filter((modal) => modal.key !== modalHandler.key)); + private _close(key: string) { + this._modals.next(this._modals.getValue().filter((modal) => modal.key !== key)); + } + + private _handleCloseEnd(modalHandler: UmbModalHandler) { + modalHandler.element.removeEventListener('close-end', () => this._handleCloseEnd(modalHandler)); + this._close(modalHandler.key); } }