This repository has been archived by the owner on Jun 17, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Move backdrop and click handler to modal service since they should not be used in web * Add support for opening modals using ViewContainerRef
- Loading branch information
Showing
11 changed files
with
313 additions
and
113 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { | ||
ApplicationRef, | ||
ComponentFactoryResolver, | ||
ComponentRef, | ||
EmbeddedViewRef, | ||
Injectable, | ||
Injector, | ||
Type, | ||
ViewContainerRef | ||
} from '@angular/core'; | ||
import { first } from 'rxjs/operators'; | ||
|
||
import { DynamicModalComponent } from '../components/modal/dynamic-modal.component'; | ||
import { ModalInjector } from '../components/modal/modal-injector'; | ||
import { ModalRef } from '../components/modal/modal.ref'; | ||
|
||
export class ModalConfig<D = any> { | ||
data?: D; | ||
allowMultipleModals: boolean = false; | ||
} | ||
|
||
@Injectable() | ||
export class ModalService { | ||
protected modalCount = 0; | ||
|
||
constructor(private componentFactoryResolver: ComponentFactoryResolver, private applicationRef: ApplicationRef, | ||
private injector: Injector) {} | ||
|
||
async openViewRef<T>(componentType: Type<T>, viewContainerRef: ViewContainerRef, | ||
setComponentParameters: (component: T) => void = null): Promise<[ModalRef, T]> { | ||
|
||
this.modalCount++; | ||
const [modalRef, modalComponentRef] = this.openInternal(componentType, null, false); | ||
modalComponentRef.instance.setComponentParameters = setComponentParameters; | ||
|
||
viewContainerRef.insert(modalComponentRef.hostView); | ||
|
||
await modalRef.onCreated.pipe(first()).toPromise(); | ||
|
||
return [modalRef, modalComponentRef.instance.componentRef.instance]; | ||
} | ||
|
||
open(componentType: Type<any>, config?: ModalConfig) { | ||
if (!(config?.allowMultipleModals ?? false) && this.modalCount > 0) { | ||
return; | ||
} | ||
this.modalCount++; | ||
|
||
const [modalRef, _] = this.openInternal(componentType, config, true); | ||
|
||
return modalRef; | ||
} | ||
|
||
protected openInternal(componentType: Type<any>, config?: ModalConfig, attachToDom?: boolean): | ||
[ModalRef, ComponentRef<DynamicModalComponent>] { | ||
|
||
const [modalRef, componentRef] = this.createModalComponent(config); | ||
componentRef.instance.childComponentType = componentType; | ||
|
||
if (attachToDom) { | ||
this.applicationRef.attachView(componentRef.hostView); | ||
const domElem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement; | ||
document.body.appendChild(domElem); | ||
} | ||
|
||
modalRef.onClosed.pipe(first()).subscribe(() => { | ||
if (attachToDom) { | ||
this.applicationRef.detachView(componentRef.hostView); | ||
} | ||
componentRef.destroy(); | ||
this.modalCount--; | ||
}); | ||
|
||
this.setupHandlers(modalRef); | ||
|
||
return [modalRef, componentRef]; | ||
} | ||
|
||
protected setupHandlers(modalRef: ModalRef) { | ||
let backdrop: HTMLElement = null; | ||
|
||
// Add backdrop, setup [data-dismiss] handler. | ||
modalRef.onCreated.pipe(first()).subscribe(el => { | ||
document.body.classList.add('modal-open'); | ||
|
||
backdrop = document.createElement('div'); | ||
backdrop.className = 'modal-backdrop fade'; | ||
backdrop.style.zIndex = `${this.modalCount}040`; | ||
document.body.appendChild(backdrop); | ||
|
||
el.querySelector('.modal-dialog').addEventListener('click', (e: Event) => { | ||
e.stopPropagation(); | ||
}); | ||
|
||
const modalEl: HTMLElement = el.querySelector('.modal'); | ||
modalEl.style.zIndex = `${this.modalCount}050`; | ||
|
||
const modals = Array.from(el.querySelectorAll('.modal, .modal *[data-dismiss="modal"]')); | ||
for (const closeElement of modals) { | ||
closeElement.addEventListener('click', event => { | ||
modalRef.close(); | ||
}); | ||
} | ||
}); | ||
|
||
// onClose is used in Web to hook into bootstrap. On other projects we pipe it directly to closed. | ||
modalRef.onClose.pipe(first()).subscribe(() => { | ||
modalRef.closed(); | ||
|
||
if (this.modalCount === 0) { | ||
document.body.classList.remove('modal-open'); | ||
} | ||
|
||
if (backdrop != null) { | ||
document.body.removeChild(backdrop); | ||
} | ||
}); | ||
} | ||
|
||
protected createModalComponent(config: ModalConfig): [ModalRef, ComponentRef<DynamicModalComponent>] { | ||
const modalRef = new ModalRef(); | ||
|
||
const map = new WeakMap(); | ||
map.set(ModalConfig, config); | ||
map.set(ModalRef, modalRef); | ||
|
||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(DynamicModalComponent); | ||
const componentRef = componentFactory.create(new ModalInjector(this.injector, map)); | ||
|
||
return [modalRef, componentRef]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Injectable } from '@angular/core'; | ||
|
||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib-common/abstractions/passwordReprompt.service'; | ||
|
||
import { PasswordRepromptComponent } from '../components/password-reprompt.component'; | ||
import { ModalService } from './modal.service'; | ||
|
||
@Injectable() | ||
export class PasswordRepromptService implements PasswordRepromptServiceAbstraction { | ||
protected component = PasswordRepromptComponent; | ||
|
||
constructor(private modalService: ModalService) { } | ||
|
||
protectedFields() { | ||
return ['TOTP', 'Password', 'H_Field', 'Card Number', 'Security Code']; | ||
} | ||
|
||
async showPasswordPrompt() { | ||
const ref = this.modalService.open(this.component, {allowMultipleModals: true}); | ||
|
||
if (ref == null) { | ||
return false; | ||
} | ||
|
||
const result = await ref.onClosedPromise(); | ||
return result === true; | ||
} | ||
} |
Oops, something went wrong.