fix missing close animation on modals + add confirm modal layout

This commit is contained in:
Mads Rasmussen
2022-08-10 11:28:03 +02:00
parent 20c7c95f71
commit 41a97b16bc
6 changed files with 162 additions and 50 deletions

View File

@@ -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<any>) => {
this._modalService?.modals.subscribe((modals: Array<UmbModalHandler>) => {
this._modals = modals;
console.log('modals', modals);
});
});
}
@@ -43,7 +42,9 @@ export class UmbBackofficeModalContainer extends UmbContextConsumerMixin(LitElem
render() {
return html`
<uui-modal-container> ${repeat(this._modals, (modal) => html`${modal.modal}`)} </uui-modal-container>
<uui-modal-container>
${repeat(this._modals, (modalHandler) => html`${modalHandler.element}`)})})}
</uui-modal-container>
`;
}
}

View File

@@ -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 <strong>${content.name}</strong>?`,
});
modalHandler?.onClose.then(({ confirmed }) => {
if (confirmed) {
this._selectedContent.splice(index, 1);
this.requestUpdate('_selectedContent');
}
});
}
private _renderContent(content: any, index: number) {
return html`
<uui-ref-node name=${content.name} detail=${content.id}>
<uui-action-bar slot="actions">
<uui-button @click=${() => this._removeContent(index)}>Remove</uui-button>
<uui-button @click=${() => this._removeContent(index, content)}>Remove</uui-button>
</uui-action-bar>
</uui-ref-node>
`;

View File

@@ -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`
<uui-dialog-layout class="uui-text" .headline=${this.data?.headline}>
${this.data?.content}
<uui-button slot="actions" id="cancel" label="Cancel" @click="${this._handleCancel}">Cancel</uui-button>
<uui-button
slot="actions"
id="confirm"
color="${this.data?.color || 'positive'}"
look="primary"
label="${this.data?.confirmLabel || 'Confirm'}"
@click=${this._handleConfirm}></uui-button>
</uui-dialog-layout>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-modal-layout-confirm': UmbModelLayoutConfirmElement;
}
}

View File

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

View File

@@ -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<unknown>) {
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<unknown>) {
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<unknown>) {
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<any> {

View File

@@ -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<UmbModalData> {
type?: UmbModelType;
size?: UUIModalSidebarSize;
data: UmbModalData;
}
export class UmbModalService {
private _modals: BehaviorSubject<Array<UmbModalHandler>> = new BehaviorSubject(<Array<UmbModalHandler>>[]);
public readonly modals: Observable<Array<UmbModalHandler>> = 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<unknown>): 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);
}
}