From c50b478a4d1b2d8e3f83b9dd4c51265a6647edf8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 29 Jan 2018 09:33:43 -0500 Subject: [PATCH] modal component --- src/app/app.module.ts | 8 + src/app/modal.component.ts | 50 ++++ src/app/vault/add-edit.component.ts | 5 + .../vault/password-generator.component.html | 150 ++++++++++ src/app/vault/password-generator.component.ts | 31 ++ src/app/vault/vault.component.html | 4 +- src/app/vault/vault.component.ts | 21 +- src/scss/modal.scss | 282 ++++++++++++++++++ src/scss/styles.scss | 1 + 9 files changed, 550 insertions(+), 2 deletions(-) create mode 100644 src/app/modal.component.ts create mode 100644 src/app/vault/password-generator.component.html create mode 100644 src/app/vault/password-generator.component.ts create mode 100644 src/scss/modal.scss diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0b95d8b41d..a919f638a2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -19,6 +19,8 @@ import { GroupingsComponent } from './vault/groupings.component'; import { I18nPipe } from './pipes/i18n.pipe'; import { IconComponent } from './vault/icon.component'; import { LoginComponent } from './accounts/login.component'; +import { ModalComponent } from './modal.component'; +import { PasswordGeneratorComponent } from './vault/password-generator.component'; import { StopClickDirective } from './directives/stop-click.directive'; import { StopPropDirective } from './directives/stop-prop.directive'; import { VaultComponent } from './vault/vault.component'; @@ -47,11 +49,17 @@ import { ViewComponent } from './vault/view.component'; I18nPipe, IconComponent, LoginComponent, + ModalComponent, + PasswordGeneratorComponent, StopClickDirective, StopPropDirective, VaultComponent, ViewComponent, ], + entryComponents: [ + ModalComponent, + PasswordGeneratorComponent, + ], providers: [], bootstrap: [AppComponent], }) diff --git a/src/app/modal.component.ts b/src/app/modal.component.ts new file mode 100644 index 0000000000..00ad833f71 --- /dev/null +++ b/src/app/modal.component.ts @@ -0,0 +1,50 @@ +import * as template from './modal.component.html'; + +import { + Component, + ComponentFactoryResolver, + EventEmitter, + Input, + OnDestroy, + Output, + Type, + ViewChild, + ViewContainerRef, +} from '@angular/core'; + +@Component({ + selector: 'app-modal', + template: ``, +}) +export class ModalComponent implements OnDestroy { + @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef; + parentContainer: ViewContainerRef = null; + fade: boolean = true; + + constructor(private componentFactoryResolver: ComponentFactoryResolver) { } + + ngOnDestroy() { + document.body.classList.remove('modal-open'); + document.body.removeChild(document.querySelector('.modal-backdrop')); + } + + show(type: Type, parentContainer: ViewContainerRef, fade: boolean = true): T { + 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(type); + const componentRef = this.container.createComponent(factory); + return componentRef.instance; + } + + close() { + if (this.parentContainer != null) { + this.parentContainer.clear(); + } + } +} diff --git a/src/app/vault/add-edit.component.ts b/src/app/vault/add-edit.component.ts index b5ed06b921..452b9aba0b 100644 --- a/src/app/vault/add-edit.component.ts +++ b/src/app/vault/add-edit.component.ts @@ -39,6 +39,7 @@ export class AddEditComponent implements OnChanges { @Output() onDeletedCipher = new EventEmitter(); @Output() onCancelled = new EventEmitter(); @Output() onEditAttachments = new EventEmitter(); + @Output() onGeneratePassword = new EventEmitter(); editMode: boolean = false; cipher: CipherView; @@ -172,4 +173,8 @@ export class AddEditComponent implements OnChanges { this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem')); this.onDeletedCipher.emit(this.cipher); } + + generatePassword() { + this.onGeneratePassword.emit(); + } } diff --git a/src/app/vault/password-generator.component.html b/src/app/vault/password-generator.component.html new file mode 100644 index 0000000000..7fcf23a133 --- /dev/null +++ b/src/app/vault/password-generator.component.html @@ -0,0 +1,150 @@ + diff --git a/src/app/vault/password-generator.component.ts b/src/app/vault/password-generator.component.ts new file mode 100644 index 0000000000..7c6a6c6c69 --- /dev/null +++ b/src/app/vault/password-generator.component.ts @@ -0,0 +1,31 @@ +import * as template from './password-generator.component.html'; + +import { + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { CipherType } from 'jslib/enums/cipherType'; + +import { CollectionService } from 'jslib/abstractions/collection.service'; + +@Component({ + selector: 'password-generator', + template: template, +}) +export class PasswordGeneratorComponent implements OnInit { + @Input() in: string; + @Output() out = new EventEmitter(); + + constructor() { + // ctor + } + + async ngOnInit() { + console.log(this.in); + setTimeout(() => { this.out.emit('world'); }, 2000); + } +} diff --git a/src/app/vault/vault.component.html b/src/app/vault/vault.component.html index bc58bd9066..f709c96eee 100644 --- a/src/app/vault/vault.component.html +++ b/src/app/vault/vault.component.html @@ -22,6 +22,8 @@ (onSavedCipher)="savedCipher($event)" (onDeletedCipher)="deletedCipher($event)" (onEditAttachments)="editCipherAttachments($event)" - (onCancelled)="cancelledAddEdit($event)"> + (onCancelled)="cancelledAddEdit($event)" + (onGeneratePassword)="openPasswordGenerator()"> + diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts index ed8c146f55..d519c4e3c7 100644 --- a/src/app/vault/vault.component.ts +++ b/src/app/vault/vault.component.ts @@ -2,8 +2,10 @@ import * as template from './vault.component.html'; import { Component, + ComponentFactoryResolver, OnInit, ViewChild, + ViewContainerRef, } from '@angular/core'; import { @@ -15,6 +17,8 @@ import { Location } from '@angular/common'; import { CiphersComponent } from './ciphers.component'; import { GroupingsComponent } from './groupings.component'; +import { PasswordGeneratorComponent } from './password-generator.component'; +import { ModalComponent } from '../modal.component'; import { CipherType } from 'jslib/enums/cipherType'; @@ -29,6 +33,7 @@ import { FolderView } from 'jslib/models/view/folderView'; export class VaultComponent implements OnInit { @ViewChild(CiphersComponent) ciphersComponent: CiphersComponent; @ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent; + @ViewChild('passwordGenerator', { read: ViewContainerRef }) passwordGeneratorModal: ViewContainerRef; action: string; cipherId: string = null; @@ -37,7 +42,8 @@ export class VaultComponent implements OnInit { folderId: string = null; collectionId: string = null; - constructor(private route: ActivatedRoute, private router: Router, private location: Location) { + constructor(private route: ActivatedRoute, private router: Router, private location: Location, + private componentFactoryResolver: ComponentFactoryResolver) { } async ngOnInit() { @@ -164,6 +170,19 @@ export class VaultComponent implements OnInit { this.go(); } + async openPasswordGenerator() { + let factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + let componentRef = this.passwordGeneratorModal.createComponent(factory); + let modal = componentRef.instance as ModalComponent; + let childComponent = modal.show(PasswordGeneratorComponent, + this.passwordGeneratorModal); + childComponent.in = 'hello'; + childComponent.out.subscribe((i: string) => { + console.log(i); + //modal.close(); + }); + } + private clearFilters() { this.folderId = null; this.collectionId = null; diff --git a/src/scss/modal.scss b/src/scss/modal.scss new file mode 100644 index 0000000000..d9dff0cf09 --- /dev/null +++ b/src/scss/modal.scss @@ -0,0 +1,282 @@ +@import "variables.scss"; + +$white: white; +$black: black; +$border-width: 1px; +$line-height-base: 14px; +$gray-200: $gray; +$border-radius-lg: $border-radius; + +// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss + +$grid-breakpoints: ( xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px ) !default; + +$zindex-modal-backdrop: 1040 !default; +$zindex-modal: 1050 !default; + +// Padding applied to the modal body +$modal-inner-padding: 1rem !default; + +$modal-dialog-margin: .5rem !default; +$modal-dialog-margin-y-sm-up: 1.75rem !default; + +$modal-title-line-height: $line-height-base !default; + +$modal-content-bg: $white !default; +$modal-content-border-color: rgba($black, .2) !default; +$modal-content-border-width: $border-width !default; +$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default; +$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default; + +$modal-backdrop-bg: $background-color !default; +$modal-backdrop-opacity: .5 !default; +$modal-header-border-color: $gray-200 !default; +$modal-footer-border-color: $modal-header-border-color !default; +$modal-header-border-width: $modal-content-border-width !default; +$modal-footer-border-width: $modal-header-border-width !default; +$modal-header-padding: 1rem !default; + +$modal-lg: 800px !default; +$modal-md: 500px !default; +$modal-sm: 300px !default; + +$modal-transition: transform .3s ease-out !default; + +// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/mixins/_breakpoints.scss + +@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) { + $min: breakpoint-min($name, $breakpoints); + + @if $min { + @media (min-width: $min) { + @content; + } + } + @else { + @content; + } +} + +@function breakpoint-min($name, $breakpoints: $grid-breakpoints) { + $min: map-get($breakpoints, $name); + @return if($min != 0, $min, null); +} + +// Custom Added CSS animations + +@keyframes modalshow { + 0% { + opacity: 0; + transform: translate(0, -25%); + } + + 100% { + opacity: 1; + transform: translate(0, 0); + } +} + +@keyframes backdropshow { + 0% { + opacity: 0; + } + + 100% { + opacity: $modal-backdrop-opacity; + } +} + +.modal::-webkit-scrollbar-thumb { + background-color: darken($modal-backdrop-bg, 20%); + + &:hover { + background-color: darken($modal-backdrop-bg, 50%); + } +} + +// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_modal.scss + +// .modal-open - body class for killing the scroll +// .modal - container to scroll within +// .modal-dialog - positioning shell for the actual modal +// .modal-content - actual modal w/ bg and corners and stuff + + +// Kill the scroll on the body +.modal-open { + overflow: hidden; +} + +// Container that the modal scrolls within +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $zindex-modal; + //display: none; + overflow: hidden; + // Prevent Chrome on Windows from adding a focus outline. For details, see + // https://github.com/twbs/bootstrap/pull/10951. + outline: 0; + // We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a + // gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342 + // See also https://github.com/twbs/bootstrap/issues/17695 + .modal-open & { + overflow-x: hidden; + overflow-y: auto; + } +} + +// Shell div to position the modal with bottom padding +.modal-dialog { + position: relative; + width: auto; + margin: $modal-dialog-margin; + // allow clicks to pass through for custom click handling to close modal + pointer-events: none; + // When fading in the modal, animate it to slide down + .modal.fade & { + //@include transition($modal-transition); + //transform: translate(0, -25%); + animation: modalshow 0.3s ease-in; + } + //.modal.show & { + // transform: translate(0, 0); + //} + transform: translate(0, 0); +} + +.modal-dialog-centered { + display: flex; + align-items: center; + min-height: calc(100% - (#{$modal-dialog-margin} * 2)); +} + +// Actual modal +.modal-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog` + // counteract the pointer-events: none; in the .modal-dialog + pointer-events: auto; + background-color: $modal-content-bg; + background-clip: padding-box; + border: $modal-content-border-width solid $modal-content-border-color; + //@include border-radius($border-radius-lg); + //@include box-shadow($modal-content-box-shadow-xs); + border-radius: $border-radius-lg; + box-shadow: $modal-content-box-shadow-xs; + // Remove focus outline from opened modal + outline: 0; +} + +// Modal background +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $zindex-modal-backdrop; + background-color: $modal-backdrop-bg; + // Fade for backdrop + &.fade { + //opacity: 0; + animation: backdropshow 0.1s ease-in; + } + //&.show { + // opacity: $modal-backdrop-opacity; + //} + opacity: $modal-backdrop-opacity; +} + +// Modal header +// Top section of the modal w/ title and dismiss +.modal-header { + display: flex; + align-items: flex-start; // so the close btn always stays on the upper right corner + justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends + padding: $modal-header-padding; + border-bottom: $modal-header-border-width solid $modal-header-border-color; + //@include border-top-radius($border-radius-lg); + border-radius: $border-radius-lg; + + .close { + padding: $modal-header-padding; + // auto on the left force icon to the right even when there is no .modal-title + margin: (-$modal-header-padding) (-$modal-header-padding) (-$modal-header-padding) auto; + } +} + +// Title text within header +.modal-title { + margin-bottom: 0; + line-height: $modal-title-line-height; +} + +// Modal body +// Where all modal content resides (sibling of .modal-header and .modal-footer) +.modal-body { + position: relative; + // Enable `flex-grow: 1` so that the body take up as much space as possible + // when should there be a fixed height on `.modal-dialog`. + flex: 1 1 auto; + padding: $modal-inner-padding; +} + +// Footer (for actions) +.modal-footer { + display: flex; + align-items: center; // vertically center + justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items + padding: $modal-inner-padding; + border-top: $modal-footer-border-width solid $modal-footer-border-color; + // Easily place margin between footer elements + > :not(:first-child) { + margin-left: .25rem; + } + + > :not(:last-child) { + margin-right: .25rem; + } +} + +// Measure scrollbar width for padding body during modal show/hide +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +// Scale up the modal +@include media-breakpoint-up(sm) { + // Automatically set modal's width for larger viewports + .modal-dialog { + max-width: $modal-md; + margin: $modal-dialog-margin-y-sm-up auto; + } + + .modal-dialog-centered { + min-height: calc(100% - (#{$modal-dialog-margin-y-sm-up} * 2)); + } + + .modal-content { + //@include box-shadow($modal-content-box-shadow-sm-up); + box-shadow: $modal-content-box-shadow-sm-up; + } + + .modal-sm { + max-width: $modal-sm; + } +} + +@include media-breakpoint-up(lg) { + .modal-lg { + max-width: $modal-lg; + } +} diff --git a/src/scss/styles.scss b/src/scss/styles.scss index ec6b0033ff..4ba5ad8186 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -4,4 +4,5 @@ @import "list.scss"; @import "box.scss"; @import "misc.scss"; +@import "modal.scss"; @import "plugins.scss";