1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 07:13:32 +00:00

Dynamic Modals (#417)

* Move backdrop and click handler to modal service since they should not be used in web

* Add support for opening modals using ViewContainerRef
This commit is contained in:
Oscar Hinton
2021-08-26 10:04:29 +02:00
committed by GitHub
parent add4b2f3e9
commit daa4f6f9a6
11 changed files with 313 additions and 113 deletions

View File

@@ -1,78 +0,0 @@
import {
Component,
ComponentFactoryResolver,
EventEmitter,
OnDestroy,
Output,
Type,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
@Component({
selector: 'app-modal',
template: `<ng-template #container></ng-template>`,
})
export class ModalComponent implements OnDestroy {
@Output() onClose = new EventEmitter();
@Output() onClosed = new EventEmitter();
@Output() onShow = new EventEmitter();
@Output() onShown = new EventEmitter();
@ViewChild('container', { read: ViewContainerRef, static: true }) container: ViewContainerRef;
parentContainer: ViewContainerRef = null;
fade: boolean = true;
constructor(protected componentFactoryResolver: ComponentFactoryResolver,
protected messagingService: MessagingService) { }
ngOnDestroy() {
document.body.classList.remove('modal-open');
document.body.removeChild(document.querySelector('.modal-backdrop'));
}
show<T>(type: Type<T>, parentContainer: ViewContainerRef, fade: boolean = true,
setComponentParameters: (component: T) => void = null): T {
this.onShow.emit();
this.messagingService.send('modalShow');
this.parentContainer = parentContainer;
this.fade = fade;
document.body.classList.add('modal-open');
const backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop' + (this.fade ? ' fade' : '');
document.body.appendChild(backdrop);
const factory = this.componentFactoryResolver.resolveComponentFactory<T>(type);
const componentRef = this.container.createComponent<T>(factory);
if (setComponentParameters != null) {
setComponentParameters(componentRef.instance);
}
document.querySelector('.modal-dialog').addEventListener('click', (e: Event) => {
e.stopPropagation();
});
const modals = Array.from(document.querySelectorAll('.modal, .modal *[data-dismiss="modal"]'));
for (const closeElement of modals) {
closeElement.addEventListener('click', event => {
this.close();
});
}
this.onShown.emit();
this.messagingService.send('modalShown');
return componentRef.instance;
}
close() {
this.onClose.emit();
this.messagingService.send('modalClose');
this.onClosed.emit();
this.messagingService.send('modalClosed');
if (this.parentContainer != null) {
this.parentContainer.clear();
}
}
}

View File

@@ -0,0 +1,57 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ComponentFactoryResolver,
ComponentRef,
ElementRef,
OnDestroy,
Type,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ModalRef } from './modal.ref';
@Component({
selector: 'app-modal',
template: '<ng-template #modalContent></ng-template>',
})
export class DynamicModalComponent implements AfterViewInit, OnDestroy {
componentRef: ComponentRef<any>;
@ViewChild('modalContent', { read: ViewContainerRef, static: true }) modalContentRef: ViewContainerRef;
childComponentType: Type<any>;
setComponentParameters: (component: any) => void;
constructor(private componentFactoryResolver: ComponentFactoryResolver, private cd: ChangeDetectorRef,
private el: ElementRef<HTMLElement>, public modalRef: ModalRef) {}
ngAfterViewInit() {
this.loadChildComponent(this.childComponentType);
if (this.setComponentParameters != null) {
this.setComponentParameters(this.componentRef.instance);
}
this.cd.detectChanges();
this.modalRef.created(this.el.nativeElement);
}
loadChildComponent(componentType: Type<any>) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType);
this.modalContentRef.clear();
this.componentRef = this.modalContentRef.createComponent(componentFactory);
}
ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
}
}
close() {
this.modalRef.close();
}
}

View File

@@ -0,0 +1,15 @@
import {
InjectFlags,
InjectionToken,
Injector,
Type
} from '@angular/core';
export class ModalInjector implements Injector {
constructor(private _parentInjector: Injector, private _additionalTokens: WeakMap<any, any>) {}
get<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
get(token: any, notFoundValue?: any, flags?: any) {
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);
}
}

View File

@@ -0,0 +1,51 @@
import { Observable, Subject } from 'rxjs';
import { first } from 'rxjs/operators';
export class ModalRef {
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
onClose: Observable<any>; // Initiated close.
onClosed: Observable<any>; // Modal was closed (Remove element from DOM)
onShow: Observable<any>; // Start showing modal
onShown: Observable<any>; // Modal is fully visible
private readonly _onCreated = new Subject<HTMLElement>();
private readonly _onClose = new Subject<any>();
private readonly _onClosed = new Subject<any>();
private readonly _onShow = new Subject<any>();
private readonly _onShown = new Subject<any>();
private lastResult: any;
constructor() {
this.onCreated = this._onCreated.asObservable();
this.onClose = this._onClose.asObservable();
this.onClosed = this._onClosed.asObservable();
this.onShow = this._onShow.asObservable();
this.onShown = this._onShow.asObservable();
}
show() {
this._onShow.next();
}
shown() {
this._onShown.next();
}
close(result?: any) {
this.lastResult = result;
this._onClose.next(result);
}
closed() {
this._onClosed.next(this.lastResult);
}
created(el: HTMLElement) {
this._onCreated.next(el);
}
onClosedPromise(): Promise<any> {
return this.onClosed.pipe(first()).toPromise();
}
}

View File

@@ -0,0 +1,30 @@
import { Directive } from '@angular/core';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ModalRef } from './modal/modal.ref';
@Directive()
export class PasswordRepromptComponent {
showPassword = false;
masterPassword = '';
constructor(private modalRef: ModalRef, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService) {}
togglePassword() {
this.showPassword = !this.showPassword;
}
async submit() {
if (!await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, null)) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidMasterPassword'));
return;
}
this.modalRef.close(true);
}
}