aboutsummaryrefslogtreecommitdiffstats
path: root/sdc-workflow-designer-ui/src/app/paletx/plx-modal
diff options
context:
space:
mode:
Diffstat (limited to 'sdc-workflow-designer-ui/src/app/paletx/plx-modal')
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts16
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts9
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts4
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts109
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts103
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts114
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts82
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less125
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts21
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts597
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts54
11 files changed, 1234 insertions, 0 deletions
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts
new file mode 100644
index 00000000..887b66e4
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts
@@ -0,0 +1,16 @@
+import {TestBed} from '@angular/core/testing';
+import {PlxModalBackdrop} from './modal-backdrop';
+
+describe('plx-modal-backdrop', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({declarations: [PlxModalBackdrop]});
+ });
+
+ it('should render backdrop with required CSS classes', () => {
+ const fixture = TestBed.createComponent(PlxModalBackdrop);
+
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveCssClass('modal-backdrop');
+ });
+});
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts
new file mode 100644
index 00000000..07e2ff84
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts
@@ -0,0 +1,9 @@
+import {Component} from '@angular/core';
+
+@Component({
+ selector: 'plx-modal-backdrop',
+ template: '',
+ host: {'class': 'modal-backdrop fade show'}
+})
+export class PlxModalBackdrop {
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts
new file mode 100644
index 00000000..08395852
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts
@@ -0,0 +1,4 @@
+export enum ModalDismissReasons {
+ BACKDROP_CLICK,
+ ESC
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts
new file mode 100644
index 00000000..061dc70e
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts
@@ -0,0 +1,109 @@
+import {Injectable, ComponentRef} from '@angular/core';
+import {PlxModalBackdrop} from './modal-backdrop';
+import {PlxModalWindow} from './modal-window';
+import {ContentRef} from '../util/popup';
+
+/**
+ * A reference to an active (currently opened) modal. Instances of this class
+ * can be injected into components passed as modal content.
+ */
+@Injectable()
+export class PlxActiveModal {
+ /**
+ * Can be used to close a modal, passing an optional result.
+ */
+ public close(result?: any): void {
+ // TO DO
+ }
+
+ /**
+ * Can be used to dismiss a modal, passing an optional reason.
+ */
+ public dismiss(reason?: any): void {
+ // TO DO
+ }
+}
+
+/**
+ * A reference to a newly opened modal.
+ */
+@Injectable()
+export class PlxModalRef {
+ private _resolve: (result?: any) => void;
+ private _reject: (reason?: any) => void;
+
+ /**
+ * The instance of component used as modal's content.
+ * Undefined when a TemplateRef is used as modal's content.
+ */
+ get componentInstance(): any {
+ if (this._contentRef.componentRef) {
+ return this._contentRef.componentRef.instance;
+ }
+ }
+
+ // only needed to keep TS1.8 compatibility
+ set componentInstance(instance: any) {
+ // TO DO
+ }
+
+ /**
+ * A promise that is resolved when a modal is closed and rejected when a modal is dismissed.
+ */
+ public result: Promise<any>;
+
+ constructor(private _windowCmptRef: ComponentRef<PlxModalWindow>, private _contentRef: ContentRef,
+ private _backdropCmptRef?: ComponentRef<PlxModalBackdrop>) {
+ _windowCmptRef.instance.dismissEvent.subscribe((reason: any) => {
+ this.dismiss(reason);
+ });
+
+ this.result = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ this.result.then(null, () => {
+ // TO DO
+ });
+ }
+
+ /**
+ * Can be used to close a modal, passing an optional result.
+ */
+ public close(result?: any): void {
+ if (this._windowCmptRef) {
+ this._resolve(result);
+ this._removeModalElements();
+ }
+ }
+
+ /**
+ * Can be used to dismiss a modal, passing an optional reason.
+ */
+ public dismiss(reason?: any): void {
+ if (this._windowCmptRef) {
+ this._reject(reason);
+ this._removeModalElements();
+ }
+ }
+
+ private _removeModalElements() {
+ const windowNativeEl = this._windowCmptRef.location.nativeElement;
+ windowNativeEl.parentNode.removeChild(windowNativeEl);
+ this._windowCmptRef.destroy();
+
+ if (this._backdropCmptRef) {
+ const backdropNativeEl = this._backdropCmptRef.location.nativeElement;
+ backdropNativeEl.parentNode.removeChild(backdropNativeEl);
+ this._backdropCmptRef.destroy();
+ }
+
+ if (this._contentRef && this._contentRef.viewRef) {
+ this._contentRef.viewRef.destroy();
+ }
+
+ this._windowCmptRef = null;
+ this._backdropCmptRef = null;
+ this._contentRef = null;
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts
new file mode 100644
index 00000000..37f5b171
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts
@@ -0,0 +1,103 @@
+import {
+ ApplicationRef,
+ Injectable,
+ Injector,
+ ReflectiveInjector,
+ ComponentFactory,
+ ComponentFactoryResolver,
+ ComponentRef,
+ TemplateRef
+} from '@angular/core';
+
+import {ContentRef} from '../util/popup';
+import {isDefined, isString} from '../util/util';
+
+import {PlxModalBackdrop} from './modal-backdrop';
+import {PlxModalWindow} from './modal-window';
+import {PlxActiveModal, PlxModalRef} from './modal-ref';
+
+@Injectable()
+export class PlxModalStack {
+ private _backdropFactory: ComponentFactory<PlxModalBackdrop>;
+ private _windowFactory: ComponentFactory<PlxModalWindow>;
+
+ constructor(private _applicationRef: ApplicationRef, private _injector: Injector,
+ private _componentFactoryResolver: ComponentFactoryResolver) {
+ this._backdropFactory = _componentFactoryResolver.resolveComponentFactory(PlxModalBackdrop);
+ this._windowFactory = _componentFactoryResolver.resolveComponentFactory(PlxModalWindow);
+ }
+
+ public open(moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any, options): PlxModalRef {
+ const containerSelector = options.container || 'body';
+ const containerEl = document.querySelector(containerSelector);// 默认获取到body的DOM
+
+ if (!containerEl) {
+ throw new Error(`The specified modal container "${containerSelector}" was not found in the DOM.`);
+ }
+
+ const activeModal = new PlxActiveModal();
+ const contentRef = this._getContentRef(moduleCFR, contentInjector, content, activeModal);
+
+ let windowCmptRef: ComponentRef<PlxModalWindow>;
+ let backdropCmptRef: ComponentRef<PlxModalBackdrop>;
+ let ngbModalRef: PlxModalRef;
+
+
+ if (options.backdrop !== false) {
+ backdropCmptRef = this._backdropFactory.create(this._injector);
+ this._applicationRef.attachView(backdropCmptRef.hostView);
+ containerEl.appendChild(backdropCmptRef.location.nativeElement);
+ }
+ windowCmptRef = this._windowFactory.create(this._injector, contentRef.nodes);
+
+ /**
+ * Attaches a view so that it will be dirty checked.
+ * The view will be automatically detached when it is destroyed.
+ * This will throw if the view is already attached to a ViewContainer.
+ */
+ this._applicationRef.attachView(windowCmptRef.hostView);
+
+ containerEl.appendChild(windowCmptRef.location.nativeElement);
+
+ ngbModalRef = new PlxModalRef(windowCmptRef, contentRef, backdropCmptRef);
+
+ activeModal.close = (result: any) => {
+ ngbModalRef.close(result);
+ };
+ activeModal.dismiss = (reason: any) => {
+ ngbModalRef.dismiss(reason);
+ };
+
+ this._applyWindowOptions(windowCmptRef.instance, options);
+
+ return ngbModalRef;
+ }
+
+ private _applyWindowOptions(windowInstance: PlxModalWindow, options: Object): void {
+ ['backdrop', 'keyboard', 'size', 'windowClass'].forEach((optionName: string) => {
+ if (isDefined(options[optionName])) {
+ windowInstance[optionName] = options[optionName];
+ }
+ });
+ }
+
+ private _getContentRef(moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any,
+ context: PlxActiveModal): ContentRef {
+ if (!content) {
+ return new ContentRef([]);
+ } else if (content instanceof TemplateRef) {
+ const viewRef = content.createEmbeddedView(context);
+ this._applicationRef.attachView(viewRef);
+ return new ContentRef([viewRef.rootNodes], viewRef);
+ } else if (isString(content)) {
+ return new ContentRef([[document.createTextNode(`${content}`)]]);
+ } else {
+ const contentCmptFactory = moduleCFR.resolveComponentFactory(content);
+ const modalContentInjector =
+ ReflectiveInjector.resolveAndCreate([{provide: PlxActiveModal, useValue: context}], contentInjector);
+ const componentRef = contentCmptFactory.create(modalContentInjector);
+ this._applicationRef.attachView(componentRef.hostView);
+ return new ContentRef([[componentRef.location.nativeElement]], componentRef.hostView, componentRef);
+ }
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts
new file mode 100644
index 00000000..5767bfee
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts
@@ -0,0 +1,114 @@
+import {TestBed, ComponentFixture} from '@angular/core/testing';
+
+import {PlxModalWindow} from './modal-window';
+import {ModalDismissReasons} from './modal-dismiss-reasons';
+
+describe('plx-modal-dialog', () => {
+
+ let fixture: ComponentFixture<PlxModalWindow>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({declarations: [PlxModalWindow]});
+ fixture = TestBed.createComponent(PlxModalWindow);
+ });
+
+ describe('basic rendering functionality', () => {
+
+ it('should render default modal window', () => {
+ fixture.detectChanges();
+
+ const modalEl: Element = fixture.nativeElement;
+ const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog');
+
+ expect(modalEl).toHaveCssClass('modal');
+ expect(dialogEl).toHaveCssClass('modal-dialog');
+ });
+
+ it('should render default modal window with a specified size', () => {
+ fixture.componentInstance.size = 'sm';
+ fixture.detectChanges();
+
+ const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog');
+ expect(dialogEl).toHaveCssClass('modal-dialog');
+ expect(dialogEl).toHaveCssClass('modal-sm');
+ });
+
+ it('should render default modal window with a specified class', () => {
+ fixture.componentInstance.windowClass = 'custom-class';
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveCssClass('custom-class');
+ });
+
+ it('aria attributes', () => {
+ fixture.detectChanges();
+ const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog');
+
+ expect(fixture.nativeElement.getAttribute('role')).toBe('dialog');
+ expect(dialogEl.getAttribute('role')).toBe('document');
+ });
+ });
+
+ describe('dismiss', () => {
+
+ it('should dismiss on backdrop click by default', (done) => {
+ fixture.detectChanges();
+
+ fixture.componentInstance.dismissEvent.subscribe(($event) => {
+ expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK);
+ done();
+ });
+
+ fixture.nativeElement.click();
+ });
+
+ it('should not dismiss on modal content click when there is active backdrop', (done) => {
+ fixture.detectChanges();
+ fixture.componentInstance.dismissEvent.subscribe(
+ () => {
+ done.fail(new Error('Should not trigger dismiss event'));
+ });
+
+ fixture.nativeElement.querySelector('.modal-content').click();
+ setTimeout(done, 200);
+ });
+
+ it('should ignore backdrop clicks when there is no backdrop', (done) => {
+ fixture.componentInstance.backdrop = false;
+ fixture.detectChanges();
+
+ fixture.componentInstance.dismissEvent.subscribe(($event) => {
+ expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK);
+ done.fail(new Error('Should not trigger dismiss event'));
+ });
+
+ fixture.nativeElement.querySelector('.modal-dialog').click();
+ setTimeout(done, 200);
+ });
+
+ it('should ignore backdrop clicks when backdrop is "static"', (done) => {
+ fixture.componentInstance.backdrop = 'static';
+ fixture.detectChanges();
+
+ fixture.componentInstance.dismissEvent.subscribe(($event) => {
+ expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK);
+ done.fail(new Error('Should not trigger dismiss event'));
+ });
+
+ fixture.nativeElement.querySelector('.modal-dialog').click();
+ setTimeout(done, 200);
+ });
+
+ it('should dismiss on esc press by default', (done) => {
+ fixture.detectChanges();
+
+ fixture.componentInstance.dismissEvent.subscribe(($event) => {
+ expect($event).toBe(ModalDismissReasons.ESC);
+ done();
+ });
+
+ fixture.debugElement.triggerEventHandler('keyup.esc', {});
+ });
+ });
+
+});
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts
new file mode 100644
index 00000000..eda5b39f
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts
@@ -0,0 +1,82 @@
+import {
+ Component,
+ Output,
+ EventEmitter,
+ Input,
+ ElementRef,
+ Renderer,
+ OnInit,
+ AfterViewInit,
+ OnDestroy, ViewEncapsulation
+} from '@angular/core';
+
+import {ModalDismissReasons} from './modal-dismiss-reasons';
+
+@Component({
+ selector: 'plx-modal-window',
+ host: {
+ '[class]': '"modal plx-modal fade show" + (windowClass ? " " + windowClass : "")',
+ 'role': 'dialog',
+ 'tabindex': '-1',
+ 'style': 'display: block;',
+ '(keyup.esc)': 'escKey($event)',
+ '(click)': 'backdropClick($event)'
+ },
+ template: `
+ <div [class]="'modal-dialog' + (size ? ' modal-' + size : '')" role="document">
+ <div class="modal-content"><ng-content></ng-content></div>
+ </div>
+ `,
+ styleUrls: ['modal.less'],
+ encapsulation: ViewEncapsulation.None
+})
+export class PlxModalWindow implements OnInit, AfterViewInit, OnDestroy {
+ private _elWithFocus: Element; // element that is focused prior to modal opening
+
+ @Input() public backdrop: boolean | string = true;
+ @Input() public keyboard = true;
+ @Input() public size: string;
+ @Input() public windowClass: string;
+
+ @Output('dismiss') public dismissEvent = new EventEmitter();
+
+ constructor(private _elRef: ElementRef, private _renderer: Renderer) {
+ }
+
+ public backdropClick($event): void {
+ if (this.backdrop === true && this._elRef.nativeElement === $event.target) {
+ this.dismiss(ModalDismissReasons.BACKDROP_CLICK);
+ }
+ }
+
+ public escKey($event): void {
+ if (this.keyboard && !$event.defaultPrevented) {
+ this.dismiss(ModalDismissReasons.ESC);
+ }
+ }
+
+ public dismiss(reason): void {
+ this.dismissEvent.emit(reason);
+ }
+
+ public ngOnInit() {
+ this._elWithFocus = document.activeElement;
+ this._renderer.setElementClass(document.body, 'modal-open', true);
+ }
+
+ public ngAfterViewInit() {
+ if (!this._elRef.nativeElement.contains(document.activeElement)) {
+ this._renderer.invokeElementMethod(this._elRef.nativeElement, 'focus', []);
+ }
+ }
+
+ public ngOnDestroy() {
+ if (this._elWithFocus && document.body.contains(this._elWithFocus)) {
+ this._renderer.invokeElementMethod(this._elWithFocus, 'focus', []);
+ } else {
+ this._renderer.invokeElementMethod(document.body, 'focus', []);
+ }
+ this._elWithFocus = null;
+ this._renderer.setElementClass(document.body, 'modal-open', false);
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less
new file mode 100644
index 00000000..c17a7fd1
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less
@@ -0,0 +1,125 @@
+@import "../../assets/components/themes/default/theme.less";
+
+plx-modal-window {
+ .modal {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ outline: 0;
+ z-index: 10000;
+ }
+ .modal-dialog {
+ position: relative;
+ max-width: 600px;
+ margin: 30px auto;
+ &.modal-sm {
+ max-width: 600px;
+ }
+ &.modal-lg {
+ max-width: 1000px;
+ }
+ }
+ .modal-content {
+ position: relative;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ background-color: @component-bg;
+ background-clip: padding-box;
+ border-radius: @radius;
+ box-shadow: 0 5px 15px @shadow-color;
+ outline: 0;
+ .modal-header {
+ border-bottom: 0;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: justify;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ padding: 15px;
+ }
+ .modal-body {
+ .form-group:last-child, form:last-child {
+ margin-bottom: 0;
+ }
+ }
+ .modal-footer {
+ display: block;
+ border-top: 0;
+ margin-top: 0;
+ padding: 0 15px 15px 15px;
+ }
+ .modal-title {
+ font-size: @font-size-title-level1;
+ margin-bottom: 0;
+ line-height: 1.5;
+ }
+ .modal-btn {
+ text-align: center;
+ font-size: 0;
+ }
+ }
+ .close {
+ color: @fonticon-color;
+ font-size: @font-size-title-level2;
+ text-shadow: none;
+ width: 24px;
+ height: 24px;
+ background: @scene-textcolor;
+ border-radius: 20px;
+ padding-bottom: 2px;
+ outline: none;
+ &:hover {
+ color: @fonticon-color;
+ background: @fonticon-bg-color-hover;
+ }
+ }
+ .alert-modal {
+ &.row {
+ margin-left: 100px;
+ margin-bottom: 30px;
+ text-align: left;
+ .tip-img {
+ display: inline-block;
+ width: 52px;
+ height: 52px;
+ border-radius: 50px;
+ font-size: 45px;
+ text-align: center;
+ line-height: 1;
+ margin-top: -5px;
+ margin-right: 15px;
+ &::before {
+ content: "!";
+ }
+ }
+ .tip-info {
+ width: 300px;
+ .alert-title {
+ font-size: @font-size-title-level2;
+ color: @title-text-color;
+ }
+ .alert-result {
+ margin-top: 5px;
+ font-size: @font-size;
+ color: @unselected-text-color;
+ }
+ }
+ .warning {
+ border: 3px solid @warning-color;
+ color: @warning-color;
+ }
+ .error {
+ border: 3px solid @error-color;
+ color: @error-color;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts
new file mode 100644
index 00000000..67fdb478
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts
@@ -0,0 +1,21 @@
+import {NgModule, ModuleWithProviders} from '@angular/core';
+
+import {PlxModalBackdrop} from './modal-backdrop';
+import {PlxModalWindow} from './modal-window';
+import {PlxModalStack} from './modal-stack';
+import {PlxModal} from './modal';
+
+export {PlxModal, PlxModalOptions} from './modal';
+export {PlxModalRef, PlxActiveModal} from './modal-ref';
+export {ModalDismissReasons} from './modal-dismiss-reasons';
+
+@NgModule({
+ declarations: [PlxModalBackdrop, PlxModalWindow],
+ entryComponents: [PlxModalBackdrop, PlxModalWindow],
+ providers: [PlxModal]
+})
+export class PlxModalModule {
+ public static forRoot(): ModuleWithProviders {
+ return {ngModule: PlxModalModule, providers: [PlxModal, PlxModalStack]};
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts
new file mode 100644
index 00000000..a99c86b1
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts
@@ -0,0 +1,597 @@
+import {Component, Injectable, ViewChild, OnDestroy, NgModule, getDebugNode, DebugElement} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {TestBed, ComponentFixture} from '@angular/core/testing';
+
+import {PlxModalModule, PlxModal, PlxActiveModal, PlxModalRef} from './modal.module';
+
+const NOOP = () => {
+};
+
+@Injectable()
+class SpyService {
+ called = false;
+}
+
+describe('plx-modal', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ beforeEach(() => {
+ jasmine.addMatchers({
+ toHaveModal: function (util, customEqualityTests) {
+ return {
+ compare: function (actual, content?, selector?) {
+ const allModalsContent = document.querySelector(selector || 'body').querySelectorAll('.modal-content');
+ let pass = true;
+ let errMsg;
+
+ if (!content) {
+ pass = allModalsContent.length > 0;
+ errMsg = 'at least one modal open but found none';
+ } else if (Array.isArray(content)) {
+ pass = allModalsContent.length === content.length;
+ errMsg = `${content.length} modals open but found ${allModalsContent.length}`;
+ } else {
+ pass = allModalsContent.length === 1 && allModalsContent[0].textContent.trim() === content;
+ errMsg = `exactly one modal open but found ${allModalsContent.length}`;
+ }
+
+ return {pass: pass, message: `Expected ${actual.outerHTML} to have ${errMsg}`};
+ },
+ negativeCompare: function (actual) {
+ const allOpenModals = actual.querySelectorAll('plx-modal-window');
+
+ return {
+ pass: allOpenModals.length === 0,
+ message: `Expected ${actual.outerHTML} not to have any modals open but found ${allOpenModals.length}`
+ };
+ }
+ };
+ }
+ });
+
+ jasmine.addMatchers({
+ toHaveBackdrop: function (util, customEqualityTests) {
+ return {
+ compare: function (actual) {
+ return {
+ pass: document.querySelectorAll('plx-modal-backdrop').length === 1,
+ message: `Expected ${actual.outerHTML} to have exactly one backdrop element`
+ };
+ },
+ negativeCompare: function (actual) {
+ const allOpenModals = document.querySelectorAll('plx-modal-backdrop');
+
+ return {
+ pass: allOpenModals.length === 0,
+ message: `Expected ${actual.outerHTML} not to have any backdrop elements`
+ };
+ }
+ };
+ }
+ });
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({imports: [OesModalTestModule]});
+ fixture = TestBed.createComponent(TestComponent);
+ });
+
+ afterEach(() => {
+ // detect left-over modals and close them or report errors when can't
+
+ const remainingModalWindows = document.querySelectorAll('plx-modal-window');
+ if (remainingModalWindows.length) {
+ fail(`${remainingModalWindows.length} modal windows were left in the DOM.`);
+ }
+
+ const remainingModalBackdrops = document.querySelectorAll('plx-modal-backdrop');
+ if (remainingModalBackdrops.length) {
+ fail(`${remainingModalBackdrops.length} modal backdrops were left in the DOM.`);
+ }
+ });
+
+ describe('basic functionality', () => {
+
+ it('should open and close modal with default options', () => {
+ const modalInstance = fixture.componentInstance.open('foo');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should open and close modal from a TemplateRef content', () => {
+ const modalInstance = fixture.componentInstance.openTpl();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Hello, World!');
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should properly destroy TemplateRef content', () => {
+ const spyService = fixture.debugElement.injector.get(SpyService);
+ const modalInstance = fixture.componentInstance.openDestroyableTpl();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Some content');
+ expect(spyService.called).toBeFalsy();
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(spyService.called).toBeTruthy();
+ });
+
+ it('should open and close modal from a component type', () => {
+ const spyService = fixture.debugElement.injector.get(SpyService);
+ const modalInstance = fixture.componentInstance.openCmpt(DestroyableCmpt);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Some content');
+ expect(spyService.called).toBeFalsy();
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(spyService.called).toBeTruthy();
+ });
+
+ it('should inject active modal ref when component is used as content', () => {
+ fixture.componentInstance.openCmpt(WithActiveModalCmpt);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Close');
+
+ (<HTMLElement>document.querySelector('button.closeFromInside')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should expose component used as modal content', () => {
+ const modalInstance = fixture.componentInstance.openCmpt(WithActiveModalCmpt);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Close');
+ expect(modalInstance.componentInstance instanceof WithActiveModalCmpt).toBeTruthy();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should open and close modal from inside', () => {
+ fixture.componentInstance.openTplClose();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#close')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should open and dismiss modal from inside', () => {
+ fixture.componentInstance.openTplDismiss().result.catch(NOOP);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#dismiss')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should resolve result promise on close', () => {
+ let resolvedResult;
+ fixture.componentInstance.openTplClose().result.then((result) => resolvedResult = result);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#close')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ fixture.whenStable().then(() => {
+ expect(resolvedResult).toBe('myResult');
+ });
+ });
+
+ it('should reject result promise on dismiss', () => {
+ let rejectReason;
+ fixture.componentInstance.openTplDismiss().result.catch((reason) => rejectReason = reason);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#dismiss')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ fixture.whenStable().then(() => {
+ expect(rejectReason).toBe('myReason');
+ });
+ });
+
+ it('should add / remove "modal-open" class to body when modal is open', () => {
+ const modalRef = fixture.componentInstance.open('bar');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+ expect(document.body).toHaveCssClass('modal-open');
+
+ modalRef.close('bar result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(document.body).not.toHaveCssClass('modal-open');
+ });
+
+ it('should not throw when close called multiple times', () => {
+ const modalInstance = fixture.componentInstance.open('foo');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not throw when dismiss called multiple times', () => {
+ const modalRef = fixture.componentInstance.open('foo');
+ modalRef.result.catch(NOOP);
+
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ modalRef.dismiss('some reason');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ modalRef.dismiss('some reason');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+ });
+
+ describe('backdrop options', () => {
+
+ it('should have backdrop by default', () => {
+ const modalInstance = fixture.componentInstance.open('foo');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(fixture.nativeElement).toHaveBackdrop();
+
+ modalInstance.close('some reason');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+ });
+
+ it('should open and close modal without backdrop', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {backdrop: false});
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+
+ modalInstance.close('some reason');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+ });
+
+ it('should open and close modal without backdrop from template content', () => {
+ const modalInstance = fixture.componentInstance.openTpl({backdrop: false});
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('Hello, World!');
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+
+ modalInstance.close('some reason');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+ });
+
+ it('should dismiss on backdrop click', () => {
+ fixture.componentInstance.open('foo').result.catch(NOOP);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(fixture.nativeElement).toHaveBackdrop();
+
+ (<HTMLElement>document.querySelector('plx-modal-window')).click();
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+ });
+
+ it('should not dismiss on "static" backdrop click', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {backdrop: 'static'});
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(fixture.nativeElement).toHaveBackdrop();
+
+ (<HTMLElement>document.querySelector('plx-modal-window')).click();
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal();
+ expect(fixture.nativeElement).toHaveBackdrop();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not dismiss on clicks outside content where there is no backdrop', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {backdrop: false});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ (<HTMLElement>document.querySelector('plx-modal-window')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not dismiss on clicks that result in detached elements', () => {
+ const modalInstance = fixture.componentInstance.openTplIf({});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#if')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+ });
+
+ describe('container options', () => {
+
+ it('should attach window and backdrop elements to the specified container', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {container: '#testContainer'});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo', '#testContainer');
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should throw when the specified container element doesnt exist', () => {
+ const brokenSelector = '#notInTheDOM';
+ expect(() => {
+ fixture.componentInstance.open('foo', {container: brokenSelector});
+ }).toThrowError(`The specified modal container "${brokenSelector}" was not found in the DOM.`);
+ });
+ });
+
+ describe('keyboard options', () => {
+
+ it('should dismiss modals on ESC by default', () => {
+ fixture.componentInstance.open('foo').result.catch(NOOP);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ (<DebugElement>getDebugNode(document.querySelector('plx-modal-window'))).triggerEventHandler('keyup.esc', {});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not dismiss modals on ESC when keyboard option is false', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {keyboard: false});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ (<DebugElement>getDebugNode(document.querySelector('plx-modal-window'))).triggerEventHandler('keyup.esc', {});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not dismiss modals on ESC when default is prevented', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {keyboard: true});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ (<DebugElement>getDebugNode(document.querySelector('plx-modal-window'))).triggerEventHandler('keyup.esc', {
+ defaultPrevented: true
+ });
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+ });
+
+ describe('size options', () => {
+
+ it('should render modals with specified size', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {size: 'sm'});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(document.querySelector('.modal-dialog')).toHaveCssClass('modal-sm');
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ });
+
+ describe('custom class options', () => {
+
+ it('should render modals with the correct custom classes', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {windowClass: 'bar'});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(document.querySelector('plx-modal-window')).toHaveCssClass('bar');
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ });
+
+ describe('focus management', () => {
+
+ it('should focus modal window and return focus to previously focused element', () => {
+ fixture.detectChanges();
+ const openButtonEl = fixture.nativeElement.querySelector('button#open');
+
+ openButtonEl.focus();
+ openButtonEl.click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('from button');
+ expect(document.activeElement).toBe(document.querySelector('plx-modal-window'));
+
+ fixture.componentInstance.close();
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(document.activeElement).toBe(openButtonEl);
+ });
+
+
+ it('should return focus to body if no element focused prior to modal opening', () => {
+ const modalInstance = fixture.componentInstance.open('foo');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(document.activeElement).toBe(document.querySelector('plx-modal-window'));
+
+ modalInstance.close('ok!');
+ expect(document.activeElement).toBe(document.body);
+ });
+ });
+
+ describe('window element ordering', () => {
+ it('should place newer windows on top of older ones', () => {
+ const modalInstance1 = fixture.componentInstance.open('foo', {windowClass: 'window-1'});
+ fixture.detectChanges();
+
+ const modalInstance2 = fixture.componentInstance.open('bar', {windowClass: 'window-2'});
+ fixture.detectChanges();
+
+ let windows = document.querySelectorAll('plx-modal-window');
+ expect(windows.length).toBe(2);
+ expect(windows[0]).toHaveCssClass('window-1');
+ expect(windows[1]).toHaveCssClass('window-2');
+
+ modalInstance2.close();
+ modalInstance1.close();
+ fixture.detectChanges();
+ });
+ });
+});
+
+@Component({selector: 'destroyable-cmpt', template: 'Some content'})
+export class DestroyableCmpt implements OnDestroy {
+ constructor(private _spyService: SpyService) {
+ }
+
+ ngOnDestroy(): void {
+ this._spyService.called = true;
+ }
+}
+
+@Component(
+ {selector: 'modal-content-cmpt', template: '<button class="closeFromInside" (click)="close()">Close</button>'})
+export class WithActiveModalCmpt {
+ constructor(public activeModal: PlxActiveModal) {
+ }
+
+ close() {
+ this.activeModal.close('from inside');
+ }
+}
+
+@Component({
+ selector: 'test-cmpt',
+ template: `
+ <div id="testContainer"></div>
+ <template #content>Hello, {{name}}!</template>
+ <template #destroyableContent><destroyable-cmpt></destroyable-cmpt></template>
+ <template #contentWithClose let-close="close"><button id="close" (click)="close('myResult')">Close me</button></template>
+ <template #contentWithDismiss let-dismiss="dismiss"><button id="dismiss" (click)="dismiss('myReason')">Dismiss me</button></template>
+ <template #contentWithIf>
+ <template [ngIf]="show">
+ <button id="if" (click)="show = false">Click me</button>
+ </template>
+ </template>
+ <button id="open" (click)="open('from button')">Open</button>
+ `
+})
+class TestComponent {
+ name = 'World';
+ openedModal: PlxModalRef;
+ show = true;
+ @ViewChild('content') tplContent;
+ @ViewChild('destroyableContent') tplDestroyableContent;
+ @ViewChild('contentWithClose') tplContentWithClose;
+ @ViewChild('contentWithDismiss') tplContentWithDismiss;
+ @ViewChild('contentWithIf') tplContentWithIf;
+
+ constructor(private modalService: PlxModal) {
+ }
+
+ open(content: string, options?: Object) {
+ this.openedModal = this.modalService.open(content, options);
+ return this.openedModal;
+ }
+
+ close() {
+ if (this.openedModal) {
+ this.openedModal.close('ok');
+ }
+ }
+
+ openTpl(options?: Object) {
+ return this.modalService.open(this.tplContent, options);
+ }
+
+ openCmpt(cmptType: any, options?: Object) {
+ return this.modalService.open(cmptType, options);
+ }
+
+ openDestroyableTpl(options?: Object) {
+ return this.modalService.open(this.tplDestroyableContent, options);
+ }
+
+ openTplClose(options?: Object) {
+ return this.modalService.open(this.tplContentWithClose, options);
+ }
+
+ openTplDismiss(options?: Object) {
+ return this.modalService.open(this.tplContentWithDismiss, options);
+ }
+
+ openTplIf(options?: Object) {
+ return this.modalService.open(this.tplContentWithIf, options);
+ }
+}
+
+@NgModule({
+ declarations: [TestComponent, DestroyableCmpt, WithActiveModalCmpt],
+ exports: [TestComponent, DestroyableCmpt],
+ imports: [CommonModule, PlxModalModule.forRoot()],
+ entryComponents: [DestroyableCmpt, WithActiveModalCmpt],
+ providers: [SpyService]
+})
+class OesModalTestModule {
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts
new file mode 100644
index 00000000..5935eee6
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts
@@ -0,0 +1,54 @@
+import {Injectable, Injector, ComponentFactoryResolver} from '@angular/core';
+import {PlxModalStack} from './modal-stack';
+import {PlxModalRef} from './modal-ref';
+
+/**
+ * Represent options available when opening new modal windows.
+ */
+export interface PlxModalOptions {
+ /**
+ * Whether a backdrop element should be created for a given modal (true by default).
+ * Alternatively, specify 'static' for a backdrop which doesn't close the modal on click.
+ */
+ backdrop?: boolean | 'static';
+
+ /**
+ * An element to which to attach newly opened modal windows.
+ */
+ container?: string;
+
+ /**
+ * Whether to close the modal when escape key is pressed (true by default).
+ */
+ keyboard?: boolean;
+
+ /**
+ * Size of a new modal window.
+ */
+ size?: 'sm' | 'lg';
+
+ /**
+ * Custom class to append to the modal window
+ */
+ windowClass?: string;
+}
+
+/**
+ * A service to open modal windows. Creating a modal is straightforward: create a template and pass it as an argument to
+ * the "open" method!
+ */
+@Injectable()
+export class PlxModal {
+ constructor(private _moduleCFR: ComponentFactoryResolver, private _injector: Injector, private _modalStack: PlxModalStack) {
+ }
+
+ /**
+ * Opens a new modal window with the specified content and using supplied options. Content can be provided
+ * as a TemplateRef or a component type. If you pass a component type as content than instances of those
+ * components can be injected with an instance of the PlxActiveModal class. You can use methods on the
+ * PlxActiveModal class to close / dismiss modals from "inside" of a component.
+ */
+ public open(content: any, options: PlxModalOptions = {}): PlxModalRef {
+ return this._modalStack.open(this._moduleCFR, this._injector, content, options);
+ }
+}