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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
angular/src/components/modal/dynamic-modal.component.ts
Normal file
57
angular/src/components/modal/dynamic-modal.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
15
angular/src/components/modal/modal-injector.ts
Normal file
15
angular/src/components/modal/modal-injector.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
51
angular/src/components/modal/modal.ref.ts
Normal file
51
angular/src/components/modal/modal.ref.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
30
angular/src/components/password-reprompt.component.ts
Normal file
30
angular/src/components/password-reprompt.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user