From 1994c98063c27a41797dec01f2ca9fcbe33ceab0 Mon Sep 17 00:00:00 2001 From: Israel Lavi Date: Mon, 21 May 2018 17:42:00 +0300 Subject: init commit onap ui Change-Id: I1dace78817dbba752c550c182dfea118b4a38646 Issue-ID: SDC-1350 Signed-off-by: Israel Lavi --- src/angular/accordion/accordion.component.html.ts | 21 + src/angular/accordion/accordion.component.ts | 27 ++ src/angular/accordion/accordion.module.ts | 23 ++ .../animations/animation-directives.module.ts | 19 + .../animations/ripple-click.animation.directive.ts | 47 +++ .../autocomplete/autocomplete.component.html.ts | 14 + src/angular/autocomplete/autocomplete.component.ts | 114 +++++ src/angular/autocomplete/autocomplete.module.ts | 23 ++ src/angular/autocomplete/autocomplete.pipe.ts | 16 + src/angular/buttons/button.component.html.ts | 15 + src/angular/buttons/button.component.ts | 62 +++ src/angular/buttons/buttons.module.ts | 21 + src/angular/checklist/checklist.component.html.ts | 15 + src/angular/checklist/checklist.component.ts | 50 +++ src/angular/checklist/checklist.module.ts | 11 + src/angular/checklist/models/Checklist.ts | 18 + src/angular/checklist/models/ChecklistItem.ts | 17 + src/angular/common/enums.ts | 34 ++ src/angular/common/index.ts | 3 + src/angular/components.ts | 50 +++ src/angular/filterbar/filter-bar.component.html.ts | 30 ++ src/angular/filterbar/filter-bar.component.ts | 30 ++ src/angular/filterbar/filter-bar.module.ts | 17 + .../checkbox/checkbox.component.html.ts | 8 + .../checkbox/checkbox.component.spec.ts | 37 ++ .../form-elements/checkbox/checkbox.component.ts | 21 + .../form-elements/dropdown/dropdown-models.ts | 18 + .../dropdown/dropdown-trigger.directive.ts | 17 + .../dropdown/dropdown.component.html.ts | 59 +++ .../dropdown/dropdown.component.spec.ts | 71 ++++ .../form-elements/dropdown/dropdown.component.ts | 149 +++++++ src/angular/form-elements/form-elements.module.ts | 38 ++ .../form-elements/input/input.component.html.ts | 19 + src/angular/form-elements/input/input.component.ts | 54 +++ .../form-elements/radios/radio-button.model.ts | 15 + .../radios/radio-buttons-group.component.html.ts | 20 + .../radios/radio-buttons-group.component.spec.ts | 52 +++ .../radios/radio-buttons-group.component.ts | 52 +++ .../validation/validatable.component.ts | 25 ++ .../validation/validatable.interface.ts | 5 + .../validation/validation-group.component.html.ts | 3 + .../validation/validation-group.component.ts | 47 +++ .../validation/validation.component.html.ts | 3 + .../validation/validation.component.ts | 79 ++++ .../form-elements/validation/validation.module.ts | 35 ++ .../validators/base.validator.component.html.ts | 10 + .../validators/base.validator.component.ts | 25 ++ .../validators/custom.validator.component.ts | 23 ++ .../validators/regex.validator.component.ts | 24 ++ .../validators/required.validator.component.ts | 25 ++ .../validation/validators/validator.interface.ts | 3 + src/angular/index.ts | 68 +++ .../infinite-scroll/infinite-scroll.directive.ts | 35 ++ .../infinite-scroll/infinite-scroll.module.ts | 13 + src/angular/modals/modal-button.component.ts | 29 ++ src/angular/modals/modal-close-button.component.ts | 34 ++ src/angular/modals/modal.component.html.ts | 38 ++ src/angular/modals/modal.component.spec.ts | 105 +++++ src/angular/modals/modal.component.ts | 96 +++++ src/angular/modals/modal.module.ts | 33 ++ src/angular/modals/modal.service.ts | 100 +++++ src/angular/modals/models/modal-config.ts | 44 ++ src/angular/ng1.module.ts | 135 ++++++ .../container/notifcontainer.component.html.ts | 6 + .../container/notifcontainer.component.ts | 31 ++ ...notification-inner-content-example.component.ts | 21 + src/angular/notifications/notification.module.ts | 29 ++ .../notification/notification.component.html.ts | 19 + .../notification/notification.component.ts | 42 ++ .../services/notifications.service.ts | 41 ++ .../notifications/utilities/notification.config.ts | 30 ++ .../popup-menu/popup-menu-item.component.spec.ts | 25 ++ .../popup-menu/popup-menu-item.component.ts | 34 ++ .../popup-menu/popup-menu-list.component.ts | 65 +++ src/angular/popup-menu/popup-menu.module.ts | 21 + src/angular/searchbar/search-bar.component.html.ts | 19 + src/angular/searchbar/search-bar.component.ts | 19 + src/angular/searchbar/search-bar.module.ts | 17 + .../svg-icon/svg-icon-label.component.html.ts | 6 + src/angular/svg-icon/svg-icon-label.component.ts | 26 ++ src/angular/svg-icon/svg-icon.component.html.ts | 3 + src/angular/svg-icon/svg-icon.component.ts | 77 ++++ src/angular/svg-icon/svg-icon.module.ts | 21 + src/angular/tabs/children/tab.component.html.ts | 5 + src/angular/tabs/children/tab.component.ts | 16 + src/angular/tabs/tabs.component.html.ts | 14 + src/angular/tabs/tabs.component.ts | 41 ++ src/angular/tabs/tabs.module.ts | 23 ++ src/angular/tag-cloud/tag-cloud.component.html.ts | 30 ++ src/angular/tag-cloud/tag-cloud.component.ts | 46 +++ src/angular/tag-cloud/tag-cloud.module.ts | 21 + .../tag-cloud/tag-item/tag-item.component.html.ts | 16 + .../tag-cloud/tag-item/tag-item.component.ts | 15 + .../tiles/children/tile-content.component.ts | 10 + .../tiles/children/tile-footer.component.ts | 10 + .../tiles/children/tile-header.component.ts | 10 + src/angular/tiles/tile.component.html.ts | 5 + src/angular/tiles/tile.component.ts | 11 + src/angular/tiles/tile.module.ts | 27 ++ src/angular/tooltip/tooltip-template.component.ts | 20 + src/angular/tooltip/tooltip.directive.ts | 459 +++++++++++++++++++++ src/angular/tooltip/tooltip.module.ts | 17 + .../utils/create-dynamic-component.service.ts | 101 +++++ 103 files changed, 3793 insertions(+) create mode 100644 src/angular/accordion/accordion.component.html.ts create mode 100644 src/angular/accordion/accordion.component.ts create mode 100644 src/angular/accordion/accordion.module.ts create mode 100644 src/angular/animations/animation-directives.module.ts create mode 100644 src/angular/animations/ripple-click.animation.directive.ts create mode 100644 src/angular/autocomplete/autocomplete.component.html.ts create mode 100644 src/angular/autocomplete/autocomplete.component.ts create mode 100644 src/angular/autocomplete/autocomplete.module.ts create mode 100644 src/angular/autocomplete/autocomplete.pipe.ts create mode 100644 src/angular/buttons/button.component.html.ts create mode 100644 src/angular/buttons/button.component.ts create mode 100644 src/angular/buttons/buttons.module.ts create mode 100644 src/angular/checklist/checklist.component.html.ts create mode 100644 src/angular/checklist/checklist.component.ts create mode 100644 src/angular/checklist/checklist.module.ts create mode 100644 src/angular/checklist/models/Checklist.ts create mode 100644 src/angular/checklist/models/ChecklistItem.ts create mode 100644 src/angular/common/enums.ts create mode 100644 src/angular/common/index.ts create mode 100644 src/angular/components.ts create mode 100644 src/angular/filterbar/filter-bar.component.html.ts create mode 100644 src/angular/filterbar/filter-bar.component.ts create mode 100644 src/angular/filterbar/filter-bar.module.ts create mode 100644 src/angular/form-elements/checkbox/checkbox.component.html.ts create mode 100644 src/angular/form-elements/checkbox/checkbox.component.spec.ts create mode 100644 src/angular/form-elements/checkbox/checkbox.component.ts create mode 100644 src/angular/form-elements/dropdown/dropdown-models.ts create mode 100644 src/angular/form-elements/dropdown/dropdown-trigger.directive.ts create mode 100644 src/angular/form-elements/dropdown/dropdown.component.html.ts create mode 100644 src/angular/form-elements/dropdown/dropdown.component.spec.ts create mode 100644 src/angular/form-elements/dropdown/dropdown.component.ts create mode 100644 src/angular/form-elements/form-elements.module.ts create mode 100644 src/angular/form-elements/input/input.component.html.ts create mode 100644 src/angular/form-elements/input/input.component.ts create mode 100644 src/angular/form-elements/radios/radio-button.model.ts create mode 100644 src/angular/form-elements/radios/radio-buttons-group.component.html.ts create mode 100644 src/angular/form-elements/radios/radio-buttons-group.component.spec.ts create mode 100644 src/angular/form-elements/radios/radio-buttons-group.component.ts create mode 100644 src/angular/form-elements/validation/validatable.component.ts create mode 100644 src/angular/form-elements/validation/validatable.interface.ts create mode 100644 src/angular/form-elements/validation/validation-group.component.html.ts create mode 100644 src/angular/form-elements/validation/validation-group.component.ts create mode 100644 src/angular/form-elements/validation/validation.component.html.ts create mode 100644 src/angular/form-elements/validation/validation.component.ts create mode 100644 src/angular/form-elements/validation/validation.module.ts create mode 100644 src/angular/form-elements/validation/validators/base.validator.component.html.ts create mode 100644 src/angular/form-elements/validation/validators/base.validator.component.ts create mode 100644 src/angular/form-elements/validation/validators/custom.validator.component.ts create mode 100644 src/angular/form-elements/validation/validators/regex.validator.component.ts create mode 100644 src/angular/form-elements/validation/validators/required.validator.component.ts create mode 100644 src/angular/form-elements/validation/validators/validator.interface.ts create mode 100644 src/angular/index.ts create mode 100644 src/angular/infinite-scroll/infinite-scroll.directive.ts create mode 100644 src/angular/infinite-scroll/infinite-scroll.module.ts create mode 100644 src/angular/modals/modal-button.component.ts create mode 100644 src/angular/modals/modal-close-button.component.ts create mode 100644 src/angular/modals/modal.component.html.ts create mode 100644 src/angular/modals/modal.component.spec.ts create mode 100644 src/angular/modals/modal.component.ts create mode 100644 src/angular/modals/modal.module.ts create mode 100644 src/angular/modals/modal.service.ts create mode 100644 src/angular/modals/models/modal-config.ts create mode 100644 src/angular/ng1.module.ts create mode 100644 src/angular/notifications/container/notifcontainer.component.html.ts create mode 100644 src/angular/notifications/container/notifcontainer.component.ts create mode 100644 src/angular/notifications/notification-inner-content-example.component.ts create mode 100644 src/angular/notifications/notification.module.ts create mode 100644 src/angular/notifications/notification/notification.component.html.ts create mode 100644 src/angular/notifications/notification/notification.component.ts create mode 100644 src/angular/notifications/services/notifications.service.ts create mode 100644 src/angular/notifications/utilities/notification.config.ts create mode 100644 src/angular/popup-menu/popup-menu-item.component.spec.ts create mode 100644 src/angular/popup-menu/popup-menu-item.component.ts create mode 100644 src/angular/popup-menu/popup-menu-list.component.ts create mode 100644 src/angular/popup-menu/popup-menu.module.ts create mode 100644 src/angular/searchbar/search-bar.component.html.ts create mode 100644 src/angular/searchbar/search-bar.component.ts create mode 100644 src/angular/searchbar/search-bar.module.ts create mode 100644 src/angular/svg-icon/svg-icon-label.component.html.ts create mode 100644 src/angular/svg-icon/svg-icon-label.component.ts create mode 100644 src/angular/svg-icon/svg-icon.component.html.ts create mode 100644 src/angular/svg-icon/svg-icon.component.ts create mode 100644 src/angular/svg-icon/svg-icon.module.ts create mode 100644 src/angular/tabs/children/tab.component.html.ts create mode 100644 src/angular/tabs/children/tab.component.ts create mode 100644 src/angular/tabs/tabs.component.html.ts create mode 100644 src/angular/tabs/tabs.component.ts create mode 100644 src/angular/tabs/tabs.module.ts create mode 100644 src/angular/tag-cloud/tag-cloud.component.html.ts create mode 100644 src/angular/tag-cloud/tag-cloud.component.ts create mode 100644 src/angular/tag-cloud/tag-cloud.module.ts create mode 100644 src/angular/tag-cloud/tag-item/tag-item.component.html.ts create mode 100644 src/angular/tag-cloud/tag-item/tag-item.component.ts create mode 100644 src/angular/tiles/children/tile-content.component.ts create mode 100644 src/angular/tiles/children/tile-footer.component.ts create mode 100644 src/angular/tiles/children/tile-header.component.ts create mode 100644 src/angular/tiles/tile.component.html.ts create mode 100644 src/angular/tiles/tile.component.ts create mode 100644 src/angular/tiles/tile.module.ts create mode 100644 src/angular/tooltip/tooltip-template.component.ts create mode 100644 src/angular/tooltip/tooltip.directive.ts create mode 100644 src/angular/tooltip/tooltip.module.ts create mode 100644 src/angular/utils/create-dynamic-component.service.ts (limited to 'src/angular') diff --git a/src/angular/accordion/accordion.component.html.ts b/src/angular/accordion/accordion.component.html.ts new file mode 100644 index 0000000..ac5f81f --- /dev/null +++ b/src/angular/accordion/accordion.component.html.ts @@ -0,0 +1,21 @@ +export default ` +
+
+
+ + + + + + +
+
+ {{title}} +
+
+
+ +
+
`; diff --git a/src/angular/accordion/accordion.component.ts b/src/angular/accordion/accordion.component.ts new file mode 100644 index 0000000..b16df89 --- /dev/null +++ b/src/angular/accordion/accordion.component.ts @@ -0,0 +1,27 @@ +/** + * Created by M.S.BIT on 26/04/2018. + */ + +import {Component, Input, Output, EventEmitter} from "@angular/core"; +import {Placement} from "../common/enums"; +import template from './accordion.component.html'; + +@Component({ + selector: 'sdc-accordion', + template: template, +}) +export class AccordionComponent { + + @Input('arrow-direction') arrowDirection: Placement; + @Input('css-class') customCSSClass: string; + @Input('title') title: string; + @Input('open') open: boolean; + @Output('accordionChanged') changed = new EventEmitter(); + + public accordionArrowDirection = Placement; + + public toggleAccordion(){ + this.open = !this.open; + this.changed.emit(this.open); + } +} diff --git a/src/angular/accordion/accordion.module.ts b/src/angular/accordion/accordion.module.ts new file mode 100644 index 0000000..6cda646 --- /dev/null +++ b/src/angular/accordion/accordion.module.ts @@ -0,0 +1,23 @@ +/** + * Created by M.S.BIT on 26/04/2018. + */ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import {AccordionComponent} from "./accordion.component"; +import {SvgIconModule} from "../svg-icon/svg-icon.module"; + +@NgModule({ + declarations: [ + AccordionComponent + ], + imports: [ + CommonModule, + SvgIconModule + ], + exports: [ + AccordionComponent + ], +}) +export class AccordionModule { + +} diff --git a/src/angular/animations/animation-directives.module.ts b/src/angular/animations/animation-directives.module.ts new file mode 100644 index 0000000..c6a8203 --- /dev/null +++ b/src/angular/animations/animation-directives.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from "@angular/core"; +import { RippleClickAnimationDirective } from "./ripple-click.animation.directive"; +import { CommonModule } from "@angular/common"; + +@NgModule({ + declarations: [ + RippleClickAnimationDirective + ], + imports: [ + CommonModule + ], + exports: [ + RippleClickAnimationDirective + + ], +}) +export class AnimationDirectivesModule { + +} diff --git a/src/angular/animations/ripple-click.animation.directive.ts b/src/angular/animations/ripple-click.animation.directive.ts new file mode 100644 index 0000000..7b9ed55 --- /dev/null +++ b/src/angular/animations/ripple-click.animation.directive.ts @@ -0,0 +1,47 @@ +import { Directive, Input, HostBinding, HostListener } from "@angular/core"; + +export enum RippleAnimationAction { + CLICK = 0, + MOUSE_ENTER = 1 +}; + +@Directive({ + selector: `[SdcRippleClickAnimation]` +}) +export class RippleClickAnimationDirective { + private animated: boolean; + + @Input() rippleClickDisabled: boolean; + @Input() rippleOnAction:RippleAnimationAction = RippleAnimationAction.CLICK; + + @HostBinding('class.sdc-ripple-click__animated') animationClass: string; + + @HostListener('click') onClick() { + if(this.rippleOnAction === RippleAnimationAction.CLICK){ + this.animateStart(); + } + } + + @HostListener('mouseenter') onMouseEnter() { + //console.log("Mouseenter!", this.rippleOnAction); + if(this.rippleOnAction === RippleAnimationAction.MOUSE_ENTER){ + this.animateStart(); + } + } + + private animateStart():void{ + if (!this.rippleClickDisabled) { + this.animated = true; + this.animationClass = 'sdc-ripple-click__animated'; + } + } + @HostListener('animationend') onAnimationComplete() { + this.animated = false; + this.animationClass = ''; + } + + constructor() { + this.rippleClickDisabled = false; + this.animated = false; + } +} diff --git a/src/angular/autocomplete/autocomplete.component.html.ts b/src/angular/autocomplete/autocomplete.component.html.ts new file mode 100644 index 0000000..5df7352 --- /dev/null +++ b/src/angular/autocomplete/autocomplete.component.html.ts @@ -0,0 +1,14 @@ +export default ` +
+ + +
    +
  • {{item.value}}
  • +
+
+`; diff --git a/src/angular/autocomplete/autocomplete.component.ts b/src/angular/autocomplete/autocomplete.component.ts new file mode 100644 index 0000000..5570eff --- /dev/null +++ b/src/angular/autocomplete/autocomplete.component.ts @@ -0,0 +1,114 @@ +import { OnInit, animate, Component, EventEmitter, Input, Output, state, style, transition, trigger } from '@angular/core'; +import { FilterBarComponent } from "../filterbar/filter-bar.component"; +import { URLSearchParams, Http } from "@angular/http"; +import { AutocompletePipe } from "./autocomplete.pipe"; +import template from "./autocomplete.component.html"; +import 'rxjs/add/operator/map'; + +export interface IDataSchema { + key: string; + value: string; +} + +@Component({ + selector: 'sdc-autocomplete', + template: template, + animations: [ + trigger('displayResultsAnimation', [ + state('true', style({ + height: '*', + opacity: 1 + })), + state('false', style({ + height: 0, + opacity: 0 + })), + transition('* => *', animate('200ms')) + ]), + ], + providers: [AutocompletePipe] +}) +export class SearchWithAutoCompleteComponent implements OnInit { + @Input() public data: any[] = []; + @Input() public dataSchema: IDataSchema; + @Input() public dataUrl: string; + @Input() public label: string; + @Input() public placeholder: string; + @Output() public itemSelected: EventEmitter = new EventEmitter(); + + private searchQuery: string; + private complexData: any[] = []; + private autoCompleteResults: any[] = []; + private isItemSelected: boolean = false; + + public constructor(private http: Http, private autocompletePipe: AutocompletePipe) { + } + + public ngOnInit(): void { + if (this.data) { + this.handleLocalData(); + } + this.searchQuery = ""; + } + + private handleLocalData = (): void => { + // Convert the data (simple | complex) to unified complex data with key value. + // In case user supplied dataSchema, this is complex data + if (!this.dataSchema) { + this.convertSimpleData(); + } else { + this.convertComplexData(); + } + } + + private convertSimpleData = (): void => { + this.complexData = []; + this.data.forEach((item: any) => { + this.complexData.push({key: item, value: item}); + }); + } + + private convertComplexData = (): void => { + this.complexData = []; + this.data.forEach((item: any) => { + this.complexData.push({key: item[this.dataSchema.key], value: item[this.dataSchema.value]}); + }); + } + + private onItemSelected = (selectedItem: IDataSchema): void => { + this.searchQuery = selectedItem.value; + this.isItemSelected = true; + this.autoCompleteResults = []; + this.itemSelected.emit(selectedItem.key); + } + + private onSearchQueryChanged = (searchText: string): void => { + if (searchText !== this.searchQuery) { + this.searchQuery = searchText; + if (!this.searchQuery) { + this.onClearSearch(); + } else { + if (this.dataUrl) { + const params: URLSearchParams = new URLSearchParams(); + params.set('searchQuery', this.searchQuery); + this.http.get(this.dataUrl, {search: params}) + .map((response) => { + this.data = response.json(); + this.handleLocalData(); + this.autoCompleteResults = this.complexData; + }).subscribe(); + } else { + this.autoCompleteResults = this.autocompletePipe.transform(this.complexData, this.searchQuery); + } + } + this.isItemSelected = false; + } + } + + private onClearSearch = (): void => { + this.autoCompleteResults = []; + if (this.isItemSelected) { + this.itemSelected.emit(); + } + } +} diff --git a/src/angular/autocomplete/autocomplete.module.ts b/src/angular/autocomplete/autocomplete.module.ts new file mode 100644 index 0000000..1bead47 --- /dev/null +++ b/src/angular/autocomplete/autocomplete.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from "@angular/core"; +import { SearchWithAutoCompleteComponent } from "./autocomplete.component"; +import { CommonModule } from "@angular/common"; +import { FilterBarModule } from "../filterbar/filter-bar.module"; +import { AutocompletePipe } from "./autocomplete.pipe"; +import { HttpModule } from '@angular/http'; + +@NgModule({ + declarations: [ + SearchWithAutoCompleteComponent, + AutocompletePipe + ], + imports: [ + FilterBarModule, + CommonModule, + HttpModule + ], + exports: [ + SearchWithAutoCompleteComponent + ], +}) +export class AutoCompleteModule { +} diff --git a/src/angular/autocomplete/autocomplete.pipe.ts b/src/angular/autocomplete/autocomplete.pipe.ts new file mode 100644 index 0000000..bee24ab --- /dev/null +++ b/src/angular/autocomplete/autocomplete.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { IDataSchema } from './autocomplete.component'; + +@Pipe ({ + name: 'AutocompletePipe', +}) +export class AutocompletePipe implements PipeTransform { + public transform(data: IDataSchema[], text: string) { + if (!text || !text.length) { + return data; + } + return data.filter((item: IDataSchema) => { + return item.value.toLowerCase().indexOf(text.toLowerCase()) > -1; + }); + } +} diff --git a/src/angular/buttons/button.component.html.ts b/src/angular/buttons/button.component.html.ts new file mode 100644 index 0000000..f903fd1 --- /dev/null +++ b/src/angular/buttons/button.component.html.ts @@ -0,0 +1,15 @@ +export default ` + + +`; diff --git a/src/angular/buttons/button.component.ts b/src/angular/buttons/button.component.ts new file mode 100644 index 0000000..1f049dc --- /dev/null +++ b/src/angular/buttons/button.component.ts @@ -0,0 +1,62 @@ +import { Component, HostBinding, Input, OnInit } from "@angular/core"; +import { Placement } from "../common/enums"; +import template from "./button.component.html"; + +@Component({ + selector: "sdc-button", + template: template +}) + +export class ButtonComponent implements OnInit { + @Input() public text: string; + @Input() public disabled: boolean; + @Input() public type: string; + @Input() public size: string; + @Input() public preventDoubleClick: boolean; + @Input() public icon_name: string; + @Input() public icon_position: string; + @Input() public show_spinner: boolean; + @Input() public spinner_position: Placement; + @Input() public testId: string; + + public placement = Placement; + private lastClick: Date; + private iconPositionClass: string; + private iconMode: string; + + @HostBinding('class.sdc-button__wrapper') true; + + constructor() { + this.type = "primary"; + this.size = "default"; + this.disabled = false; + this.iconMode = 'primary'; + } + + public ngOnInit(): void { + this.iconPositionClass = this.icon_position ? 'sdc-icon-' + this.icon_position : ''; + this.iconMode = (this.type === "primary") ? 'info' : 'primary'; + } + + public onClick = (e): void => { + const now: Date = new Date(); + if ( this.preventDoubleClick && this.lastClick && (now.getTime() - this.lastClick.getTime()) <= 500 ) { + e.preventDefault(); + e.stopPropagation(); + } + this.lastClick = now; + } + + public disableButton = () => { + if (!this.disabled) { + this.disabled = true; + } + } + + public enableButton = () => { + if (this.disabled) { + this.disabled = false; + } + } + +} diff --git a/src/angular/buttons/buttons.module.ts b/src/angular/buttons/buttons.module.ts new file mode 100644 index 0000000..c804758 --- /dev/null +++ b/src/angular/buttons/buttons.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from "@angular/core"; +import { ButtonComponent } from "./button.component"; +import { CommonModule } from "@angular/common"; +import { SvgIconModule } from './../svg-icon/svg-icon.module'; + +@NgModule({ + declarations: [ + ButtonComponent + ], + imports: [ + CommonModule, + SvgIconModule + ], + exports: [ + ButtonComponent + + ], +}) +export class ButtonsModule { + +} diff --git a/src/angular/checklist/checklist.component.html.ts b/src/angular/checklist/checklist.component.html.ts new file mode 100644 index 0000000..cb6f540 --- /dev/null +++ b/src/angular/checklist/checklist.component.html.ts @@ -0,0 +1,15 @@ +export default ` +
+
+ +
+
+ +
+
+`; diff --git a/src/angular/checklist/checklist.component.ts b/src/angular/checklist/checklist.component.ts new file mode 100644 index 0000000..386cd3e --- /dev/null +++ b/src/angular/checklist/checklist.component.ts @@ -0,0 +1,50 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { ChecklistModel } from "./models/Checklist"; +import { ChecklistItemModel } from "./models/ChecklistItem"; +import template from "./checklist.component.html"; + +@Component({ + selector: 'sdc-checklist', + template: template +}) +export class ChecklistComponent { + @Input() public checklistModel: ChecklistModel; + @Output() public checkedChange: EventEmitter = new EventEmitter(); + + private checkboxCheckedChange(checkbox: ChecklistItemModel, currentChecklistModel: ChecklistModel, stopPropagation?: boolean) { + // push/pop the checkbox value + if (checkbox.isChecked) { + currentChecklistModel.selectedValues.push(checkbox.value); + }else { + const index: number = currentChecklistModel.selectedValues.indexOf(checkbox.value); + currentChecklistModel.selectedValues.splice(index, 1); + } + if (!stopPropagation) { + if (checkbox.subLevelChecklist && + ((checkbox.isChecked && checkbox.subLevelChecklist.selectedValues.length < checkbox.subLevelChecklist.checkboxes.length) || + (!checkbox.isChecked && checkbox.subLevelChecklist.selectedValues.length))) { + checkbox.subLevelChecklist.checkboxes.forEach((childCheckbox: ChecklistItemModel) => { + if (childCheckbox.isChecked !== checkbox.isChecked) { + childCheckbox.isChecked = checkbox.isChecked; + this.checkboxCheckedChange(childCheckbox, checkbox.subLevelChecklist); + } + }); + } + } + // raise event + this.checkedChange.emit(checkbox); + } + + private childCheckboxChange(updatedCheckbox: ChecklistItemModel, parentCheckbox: ChecklistItemModel) { + let updatedValues: any[] = parentCheckbox.subLevelChecklist.selectedValues; + if (parentCheckbox.isChecked !== (updatedValues.length === parentCheckbox.subLevelChecklist.checkboxes.length)) { + parentCheckbox.isChecked = updatedValues.length === parentCheckbox.subLevelChecklist.checkboxes.length; + this.checkboxCheckedChange(parentCheckbox, this.checklistModel, true); + } + this.checkedChange.emit(updatedCheckbox); + } + + private hasCheckedChild(currentCheckbox: Element): boolean { + return !!currentCheckbox.querySelector(".sdc-checkbox__input:checked"); + } +} diff --git a/src/angular/checklist/checklist.module.ts b/src/angular/checklist/checklist.module.ts new file mode 100644 index 0000000..013bf9b --- /dev/null +++ b/src/angular/checklist/checklist.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from "@angular/core"; +import { ChecklistComponent } from "./checklist.component"; +import { CommonModule } from "@angular/common"; +import { FormElementsModule } from "../form-elements/form-elements.module"; + +@NgModule({ + declarations: [ChecklistComponent], + exports: [ChecklistComponent], + imports: [CommonModule, FormElementsModule] +}) +export class ChecklistModule {} diff --git a/src/angular/checklist/models/Checklist.ts b/src/angular/checklist/models/Checklist.ts new file mode 100644 index 0000000..7b50dd3 --- /dev/null +++ b/src/angular/checklist/models/Checklist.ts @@ -0,0 +1,18 @@ +import { ChecklistItemModel } from "./ChecklistItem"; + +export class ChecklistModel { + public selectedValues: any[]; + public checkboxes: ChecklistItemModel[]; + constructor(selectedValues: any[], checkboxes: ChecklistItemModel[]) { + this.selectedValues = selectedValues || []; + this.checkboxes = checkboxes; + // align the selected values list and checkboxes isChecked param + this.checkboxes.forEach((checkbox: ChecklistItemModel) => { + if (this.selectedValues.indexOf(checkbox.value) > -1) { + checkbox.isChecked = true; + }else if (checkbox.isChecked) { + this.selectedValues.push(checkbox.value); + } + }); + } +} diff --git a/src/angular/checklist/models/ChecklistItem.ts b/src/angular/checklist/models/ChecklistItem.ts new file mode 100644 index 0000000..e2d812a --- /dev/null +++ b/src/angular/checklist/models/ChecklistItem.ts @@ -0,0 +1,17 @@ +import { ChecklistModel } from "./Checklist"; +import { isUndefined } from "util"; + +export class ChecklistItemModel { + public label: string; + public value: any; + public disabled: boolean; + public isChecked: boolean; + public subLevelChecklist: ChecklistModel; + constructor(label: string, disabled?: boolean, isChecked?: boolean, subLevelChecklist?: ChecklistModel, value?: any) { + this.label = label; + this.disabled = disabled; + this.isChecked = isChecked; + this.value = isUndefined(value) ? label : value; + this.subLevelChecklist = subLevelChecklist; + } +} diff --git a/src/angular/common/enums.ts b/src/angular/common/enums.ts new file mode 100644 index 0000000..0825d2f --- /dev/null +++ b/src/angular/common/enums.ts @@ -0,0 +1,34 @@ +/* +This file includes all common enum types. + +NOTE: The string values might be used as css class names. +*/ + +export enum Size { + x_large = 'x_large', + large = 'large', + medium = 'medium', + small = 'small', + x_small = 'x_small' +} + +export enum Mode { + primary = 'primary', + secondary = 'secondary', + success = 'success', + error = 'error', + warning = 'warning', + info = 'info' +} + +export enum Placement { + left = 'left', + right = 'right', + top = 'top', + bottom = 'bottom' +} + +export enum RegexPatterns { + email = '^(([^<>()\\[\\]\\\\.,;:\\s@"]+(\.[^<>()\\[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$', + numbers = '^\\d+$' +} diff --git a/src/angular/common/index.ts b/src/angular/common/index.ts new file mode 100644 index 0000000..839eba9 --- /dev/null +++ b/src/angular/common/index.ts @@ -0,0 +1,3 @@ +import * as SdcUiCommon from './enums'; + +export { SdcUiCommon }; diff --git a/src/angular/components.ts b/src/angular/components.ts new file mode 100644 index 0000000..8e06197 --- /dev/null +++ b/src/angular/components.ts @@ -0,0 +1,50 @@ +/* + Exports all the components and services of sdc-ui. + */ + +// Form Elements +export { InputComponent } from "./form-elements/input/input.component"; +export { DropDownComponent } from "./form-elements/dropdown/dropdown.component"; +export { CheckboxComponent } from "./form-elements/checkbox/checkbox.component"; +export { RadioGroupComponent } from "./form-elements/radios/radio-buttons-group.component"; + +// Buttons +export { ButtonComponent } from "./buttons/button.component"; + +// Modals +export { ModalComponent } from "./modals/modal.component"; +export { ModalService } from "./modals/modal.service"; +export { ModalButtonComponent } from "./modals/modal-button.component"; + +// Notifications +export { NotificationComponent } from "./notifications/notification/notification.component"; +export { NotificationContainerComponent } from "./notifications/container/notifcontainer.component"; +export { NotificationsService } from "./notifications/services/notifications.service"; + +// Popup Menu +export { PopupMenuListComponent } from "./popup-menu/popup-menu-list.component"; +export { PopupMenuItemComponent } from "./popup-menu/popup-menu-item.component"; + +// Tiles +export { TileComponent } from "./tiles/tile.component"; +export { TileContentComponent } from "./tiles/children/tile-content.component"; +export { TileFooterComponent } from "./tiles/children/tile-footer.component"; +export { TileHeaderComponent } from "./tiles/children/tile-header.component"; + +// Check List +export { ChecklistComponent } from "./checklist/checklist.component"; + +// Tag Cloud +export { TagItemComponent } from "./tag-cloud/tag-item/tag-item.component"; +export { TagCloudComponent } from "./tag-cloud/tag-cloud.component"; + +// Tabs +export { TabsComponent } from "./tabs/tabs.component"; +export { TabComponent } from './tabs/children/tab.component'; + +// Svg Icons +export { SvgIconComponent } from "./svg-icon/svg-icon.component"; +export { SvgIconLabelComponent } from "./svg-icon/svg-icon-label.component"; + +// Accordion +export { AccordionComponent } from './accordion/accordion.component'; diff --git a/src/angular/filterbar/filter-bar.component.html.ts b/src/angular/filterbar/filter-bar.component.html.ts new file mode 100644 index 0000000..a7d55e2 --- /dev/null +++ b/src/angular/filterbar/filter-bar.component.html.ts @@ -0,0 +1,30 @@ +export default ` +
+ + + + + + + + + + + + + + + + + + + + + +
+`; diff --git a/src/angular/filterbar/filter-bar.component.ts b/src/angular/filterbar/filter-bar.component.ts new file mode 100644 index 0000000..49cc154 --- /dev/null +++ b/src/angular/filterbar/filter-bar.component.ts @@ -0,0 +1,30 @@ +import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core'; +import template from "./filter-bar.component.html"; + +@Component({ + selector: 'sdc-filter-bar', + template: template +}) +export class FilterBarComponent { + + @HostBinding('class') classes = 'sdc-filter-bar'; + + @Input() public placeholder: string; + @Input() public label: string; + @Input() public debounceTime: number; + + @Input() public searchQuery: string; + @Output() public searchQueryChange: EventEmitter = new EventEmitter(); + + constructor() { + this.debounceTime = 200; + } + + private searchTextChange = ($event): void => { + this.searchQueryChange.emit($event); + } + + private clearSearchQuery = (): void => { + this.searchQuery = ""; + } +} diff --git a/src/angular/filterbar/filter-bar.module.ts b/src/angular/filterbar/filter-bar.module.ts new file mode 100644 index 0000000..c3604ed --- /dev/null +++ b/src/angular/filterbar/filter-bar.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { FilterBarComponent } from "./filter-bar.component"; +import { CommonModule } from "@angular/common"; +import { FormElementsModule } from "../form-elements/form-elements.module"; + +@NgModule({ + declarations: [ + FilterBarComponent + ], + imports: [CommonModule, + FormElementsModule], + exports: [ + FilterBarComponent + ], +}) +export class FilterBarModule { +} diff --git a/src/angular/form-elements/checkbox/checkbox.component.html.ts b/src/angular/form-elements/checkbox/checkbox.component.html.ts new file mode 100644 index 0000000..f4031db --- /dev/null +++ b/src/angular/form-elements/checkbox/checkbox.component.html.ts @@ -0,0 +1,8 @@ +export default ` +
+ +
+`; diff --git a/src/angular/form-elements/checkbox/checkbox.component.spec.ts b/src/angular/form-elements/checkbox/checkbox.component.spec.ts new file mode 100644 index 0000000..36f478e --- /dev/null +++ b/src/angular/form-elements/checkbox/checkbox.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, async } from '@angular/core/testing'; +import { CheckboxComponent } from "./checkbox.component"; +import { AnimationDirectivesModule } from "../../animations/animation-directives.module"; +import { FormsModule } from "@angular/forms"; + + +describe("Checbox Tests", ()=>{ + let component: CheckboxComponent; + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + CheckboxComponent + ], + imports:[ + FormsModule, + AnimationDirectivesModule + ] + }).compileComponents(); + const fixture = TestBed.createComponent(CheckboxComponent); + component = fixture.componentInstance; + })); + + it("Component Created", async(()=> { + expect(component).toBeDefined(); + })); + + it( "Test Value suppose to be toggled", async( ()=> { + component.toggleState(true) + expect(component.checked).toEqual(true); + })); + + it( "If disabled not toggled"), async(()=>{ + component.disabled = true; + component.toggleState(true); + expect(component.checked).toEqual(false); + }); +}); diff --git a/src/angular/form-elements/checkbox/checkbox.component.ts b/src/angular/form-elements/checkbox/checkbox.component.ts new file mode 100644 index 0000000..ec05eac --- /dev/null +++ b/src/angular/form-elements/checkbox/checkbox.component.ts @@ -0,0 +1,21 @@ +import { Component, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core'; +import template from "./checkbox.component.html"; + +@Component({ + selector: 'sdc-checkbox', + template: template, + encapsulation: ViewEncapsulation.None +}) +export class CheckboxComponent { + @Input() label:string; + @Input() checked:boolean; + @Input() disabled:boolean; + @Output() checkedChange:EventEmitter = new EventEmitter(); + + public toggleState(newState:boolean) { + if (!this.disabled) { + this.checked = newState; + this.checkedChange.emit(newState); + } + } +} diff --git a/src/angular/form-elements/dropdown/dropdown-models.ts b/src/angular/form-elements/dropdown/dropdown-models.ts new file mode 100644 index 0000000..fa8dc23 --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown-models.ts @@ -0,0 +1,18 @@ +export enum DropDownTypes { + Regular, + Headless, + Auto +} + +export enum DropDownOptionType { + Simple, // default + Header, + Disable, + HorizontalLine +} + +export interface IDropDownOption { + value: any; + label: string; + type?: DropDownOptionType; +} diff --git a/src/angular/form-elements/dropdown/dropdown-trigger.directive.ts b/src/angular/form-elements/dropdown/dropdown-trigger.directive.ts new file mode 100644 index 0000000..94ab3bc --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown-trigger.directive.ts @@ -0,0 +1,17 @@ +import { Directive, Input, HostBinding, HostListener } from "@angular/core"; +import { DropDownComponent } from "./dropdown.component"; + +@Directive({ + selector: '[SdcDropdownTrigger]' +}) + +export class DropDownTriggerDirective { + + @HostBinding('class.js-sdc-dropdown--toggle-hook') true; + @Input() dropDown: DropDownComponent; + + @HostListener('click', ['$event']) onClick = (event) => { + this.dropDown.toggleDropdown(event); + } + +} diff --git a/src/angular/form-elements/dropdown/dropdown.component.html.ts b/src/angular/form-elements/dropdown/dropdown.component.html.ts new file mode 100644 index 0000000..a4247a4 --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown.component.html.ts @@ -0,0 +1,59 @@ +export default ` +
+ +
+ + +
+ + +
+ + + + + + + +
+
    + + +
  • {{option.label || String(option.value)}}
  • + +
    +
+
+ + +
+
+`; diff --git a/src/angular/form-elements/dropdown/dropdown.component.spec.ts b/src/angular/form-elements/dropdown/dropdown.component.spec.ts new file mode 100644 index 0000000..1c0cb4d --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown.component.spec.ts @@ -0,0 +1,71 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { DropDownComponent } from './dropdown.component'; +import { IDropDownOption, DropDownTypes } from "./dropdown-models"; +import { FormsModule } from "@angular/forms"; +import {SvgIconModule} from "../../svg-icon/svg-icon.module"; + + +const label:string = "DropDown example"; +const placeHolder:string = "Please choose option"; +const options:IDropDownOption[] = [ + { + label:'First Option', + value: 'First Option' + }, + { + label:'Second Option', + value: 'Second Option' + }, + { + label:'Third Option', + value: 'Third Option' + } +]; + +describe('DropDown component', () => { + let fixture: ComponentFixture; + let component: DropDownComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DropDownComponent ], + imports:[ + FormsModule, + SvgIconModule + ] + }).compileComponents(); + fixture = TestBed.createComponent(DropDownComponent); + component = fixture.componentInstance; + + })); + + beforeEach(()=>{ + component.label = label; + component.placeHolder = placeHolder; + component.options = options; + component.type = DropDownTypes.Regular; + console.log('herer we got component', component) + fixture.detectChanges(); + }); + + it('component should be created', () => { + expect(component).toBeTruthy(); + }); + + it('component should export the selected value', () => { + const option = options[1]; + component.selectOption(option); + fixture.detectChanges(); + expect(component.selectedOption).toEqual(option); + }); + + it('component should have autocomplite', () => { + expect(component.options.length).toEqual(3); + component.type = DropDownTypes.Auto; + component.filterValue = 'testERrorotesttresadfadfasdfasf'; + fixture.detectChanges(); + component.filterOptions(component.filterValue); + expect(component.options.length).toEqual(0); + }); + +}); diff --git a/src/angular/form-elements/dropdown/dropdown.component.ts b/src/angular/form-elements/dropdown/dropdown.component.ts new file mode 100644 index 0000000..a23072f --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown.component.ts @@ -0,0 +1,149 @@ +import { Component, EventEmitter, Input, Output, forwardRef, OnChanges, SimpleChanges, OnInit, ElementRef, ViewChild, AfterViewInit, HostListener, Renderer } from '@angular/core'; +import { IDropDownOption, DropDownOptionType, DropDownTypes } from "./dropdown-models"; +import { ValidatableComponent } from './../validation/validatable.component'; +import template from './dropdown.component.html'; + +@Component({ + selector: 'sdc-dropdown', + template: template +}) +export class DropDownComponent extends ValidatableComponent implements OnChanges, OnInit { + + @Output('changed') changeEmitter:EventEmitter = new EventEmitter(); + @Input() label: string; + @Input() options: IDropDownOption[]; + @Input() disabled: boolean; + @Input() placeHolder: string; + @Input() required: boolean; + @Input() maxHeight: number; + @Input() selectedOption: IDropDownOption; + @Input() type: DropDownTypes = DropDownTypes.Regular; + @ViewChild('dropDownWrapper') dropDownWrapper: ElementRef; + @ViewChild('optionsContainerElement') optionsContainerElement: ElementRef; + @HostListener('document:click', ['$event']) onClick(e) { + this.onClickDocument(e); + } + + private bottomVisible = true; + private myRenderer: Renderer; + + // Drop-down show/hide flag. default is false (closed) + public show = false; + + // Export DropDownOptionType enum so we can use it on the template + public cIDropDownOptionType = DropDownOptionType; + public cIDropDownTypes = DropDownTypes; + + // Configure unselectable option types + private unselectableOptions = [ + DropDownOptionType.Disable, + DropDownOptionType.Header, + DropDownOptionType.HorizontalLine + ]; + + // Set or unset Group style on drop-down + public isGroupDesign = false; + public animation_init = false; + public allOptions: IDropDownOption[]; + public filterValue: string; + + constructor(public renderer: Renderer) { + super(); + this.myRenderer = renderer; + this.maxHeight = 244; + this.filterValue = ''; + } + + ngOnInit(): void { + if (this.options) { + this.allOptions = this.options; + if (this.options.find(option => option.type === DropDownOptionType.Header)) { + this.isGroupDesign = true; + } + } + } + + ngOnChanges(changes: SimpleChanges): void { + console.log("ngOnChanges"); + if (changes.selectedOption && changes.selectedOption.currentValue !== changes.selectedOption.previousValue) { + if (typeof changes.selectedOption.currentValue === 'string' && this.isSelectable(changes.selectedOption.currentValue)) { + this.setSelected(changes.selectedOption.currentValue); + } else if (this.isSelectable(changes.selectedOption.currentValue.value)) { + this.setSelected(changes.selectedOption.currentValue.value); + } else { + this.setSelected(undefined); + } + } + } + + public getValue(): any { + return this.selectedOption && this.selectedOption.value; + } + + public selectOption = (option: IDropDownOption | string, event?): void => { + if (event) { event.stopPropagation(); } + if (this.type === DropDownTypes.Headless) { + // Hide the options when in headless mode and user select option. + this.myRenderer.setElementStyle(this.dropDownWrapper.nativeElement, 'display', 'none'); + } + if (typeof option === 'string' && this.isSelectable(option)) { + this.setSelected(option); + } else if (this.isSelectable((option as IDropDownOption).value)) { + this.setSelected((option as IDropDownOption).value); + } + } + + public toggleDropdown = (event?): void => { + if (event) { event.stopPropagation(); } + if (this.type === DropDownTypes.Headless) { + // Show the options when in headless mode. + this.myRenderer.setElementStyle(this.dropDownWrapper.nativeElement, 'display', 'block'); + } + if (this.disabled) { return; } + this.animation_init = true; + this.bottomVisible = this.isBottomVisible(); + this.show = !this.show; + } + + public filterOptions = (filterValue): void => { + if (filterValue.length >= 1 && !this.show) { this.toggleDropdown(); } + if (this.selectedOption) { this.selectedOption = null; } + this.options = this.allOptions.filter((option) => { + return option.value.toLowerCase().indexOf(filterValue.toLowerCase()) > -1; + }); + } + + private isSelectable = (value: string): boolean => { + const option: IDropDownOption = this.options.find(o => o.value === value); + if (!option) { return false; } + if (!option.type) { return true; } + return !this.unselectableOptions.find(optionType => optionType === option.type); + } + + private setSelected = (value: string): void => { + this.selectedOption = this.options.find(o => o.value === value); + if (this.type === DropDownTypes.Auto) { this.filterValue = value; } + this.show = false; + this.changeEmitter.next(this.selectedOption); + } + + private isBottomVisible = (): boolean => { + const windowPos = window.innerHeight + window.pageYOffset; + const boundingRect = this.dropDownWrapper.nativeElement.getBoundingClientRect(); + const dropDownPos = boundingRect.top + boundingRect.height + this.maxHeight; + return windowPos > dropDownPos; + } + + private onClickDocument = (event): void => { + if (this.type === DropDownTypes.Headless) { + if (!this.optionsContainerElement.nativeElement.contains(event.target)) { + this.show = false; + } + } else { + if (!this.dropDownWrapper.nativeElement.contains(event.target)) { + this.show = false; + } + } + } + +} diff --git a/src/angular/form-elements/form-elements.module.ts b/src/angular/form-elements/form-elements.module.ts new file mode 100644 index 0000000..744f8b8 --- /dev/null +++ b/src/angular/form-elements/form-elements.module.ts @@ -0,0 +1,38 @@ +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { InputComponent } from "./input/input.component"; +import { DropDownComponent } from "./dropdown/dropdown.component"; +import { CommonModule } from "@angular/common"; +import { CheckboxComponent } from "./checkbox/checkbox.component"; +import { RadioGroupComponent } from "./radios/radio-buttons-group.component"; +import { AnimationDirectivesModule } from '../animations/animation-directives.module'; +import { DropDownTriggerDirective } from "./dropdown/dropdown-trigger.directive"; +import {SvgIconModule} from "../svg-icon/svg-icon.module"; +import { ValidationModule } from './validation/validation.module'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + AnimationDirectivesModule, + SvgIconModule + ], + declarations: [ + DropDownComponent, + InputComponent, + CheckboxComponent, + RadioGroupComponent, + DropDownTriggerDirective, + ], + exports: [ + DropDownComponent, + DropDownTriggerDirective, + InputComponent, + CheckboxComponent, + RadioGroupComponent, + ValidationModule + ] +}) +export class FormElementsModule { +} diff --git a/src/angular/form-elements/input/input.component.html.ts b/src/angular/form-elements/input/input.component.html.ts new file mode 100644 index 0000000..f8a4609 --- /dev/null +++ b/src/angular/form-elements/input/input.component.html.ts @@ -0,0 +1,19 @@ +export default ` +
+ + +
+`; diff --git a/src/angular/form-elements/input/input.component.ts b/src/angular/form-elements/input/input.component.ts new file mode 100644 index 0000000..af0e9f4 --- /dev/null +++ b/src/angular/form-elements/input/input.component.ts @@ -0,0 +1,54 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormControl } from "@angular/forms"; +import { ValidationComponent } from '../validation/validation.component'; +import { ValidatableComponent } from './../validation/validatable.component'; +import 'rxjs/add/operator/debounceTime'; +import template from "./input.component.html"; + +@Component({ + selector: 'sdc-input', + template: template, +}) +export class InputComponent extends ValidatableComponent implements OnInit { + + @Output('valueChange') public baseEmitter: EventEmitter = new EventEmitter(); + @Input() public label: string; + @Input() public value: any; + @Input() public name: string; + @Input() public classNames: string; + @Input() public disabled: boolean; + @Input() public type: string; + @Input() public placeHolder: string; + @Input() public required: boolean; + @Input() public minLength: number; + @Input() public maxLength: number; + @Input() public debounceTime: number; + @Input() public testId: string; + + protected control: FormControl; + + constructor() { + super(); + this.control = new FormControl('', []); + this.debounceTime = 0; + this.placeHolder = ''; + this.type = 'text'; + } + + ngOnInit() { + this.control.valueChanges. + debounceTime(this.debounceTime) + .subscribe((newValue: any) => { + this.baseEmitter.emit(this.value); + }); + } + + public getValue(): any { + return this.value; + } + + onKeyPress(value: string) { + this.valueChanged(this.value); + } + +} diff --git a/src/angular/form-elements/radios/radio-button.model.ts b/src/angular/form-elements/radios/radio-button.model.ts new file mode 100644 index 0000000..1ad4b3f --- /dev/null +++ b/src/angular/form-elements/radios/radio-button.model.ts @@ -0,0 +1,15 @@ +export interface IRadioButtonModel { + label: string; + disabled: boolean; + name: string; + value: string; +}; + +export interface IOptionGroup { + items: IRadioButtonModel[]; +}; + +export enum Direction { + vertical, + horizontal +} diff --git a/src/angular/form-elements/radios/radio-buttons-group.component.html.ts b/src/angular/form-elements/radios/radio-buttons-group.component.html.ts new file mode 100644 index 0000000..28a27af --- /dev/null +++ b/src/angular/form-elements/radios/radio-buttons-group.component.html.ts @@ -0,0 +1,20 @@ +export default ` + +
+ +
+`; diff --git a/src/angular/form-elements/radios/radio-buttons-group.component.spec.ts b/src/angular/form-elements/radios/radio-buttons-group.component.spec.ts new file mode 100644 index 0000000..273a701 --- /dev/null +++ b/src/angular/form-elements/radios/radio-buttons-group.component.spec.ts @@ -0,0 +1,52 @@ +import { TestBed, async } from '@angular/core/testing'; +import { RadioGroupComponent } from "./radio-buttons-group.component"; +import { FormsModule } from "@angular/forms"; +import { IRadioButtonModel } from "./radio-button.model"; +import { AnimationDirectivesModule } from "../../animations/animation-directives.module"; + + +describe("Radio Buttons unit-tests", ()=>{ + let component: RadioGroupComponent; + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + RadioGroupComponent + ], + imports:[ + FormsModule, + AnimationDirectivesModule + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(RadioGroupComponent); + component = fixture.componentInstance; + component.disabled = false;//TODO constructor + component.options = { + items: [] + }; + })); + + it('Component Created', async(()=> { + expect(component).toBeDefined(); + })); + + it('Not possible to choose value which not exists', async(() =>{ + component.value = 'test'; + expect(component.value).not.toEqual('test'); + })); + + it('Normal flow', async(() =>{ + component.options.items = [ { + value: 'val1', + name: 'exp6', + label: 'Label of Radio1' + }, { + value: 'val2', + name: 'exp6', + label: 'Label of Radio2' + }]; + component.value = component.options.items[0].value; + expect(component.value).toEqual(component.options.items[0].value); + })); + +}); diff --git a/src/angular/form-elements/radios/radio-buttons-group.component.ts b/src/angular/form-elements/radios/radio-buttons-group.component.ts new file mode 100644 index 0000000..800d8b0 --- /dev/null +++ b/src/angular/form-elements/radios/radio-buttons-group.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, Output, ViewEncapsulation, EventEmitter, HostBinding } from "@angular/core"; +import { Direction, IOptionGroup, IRadioButtonModel } from "./radio-button.model"; +import template from './radio-buttons-group.component.html'; + +@Component({ + selector: 'sdc-radio-group', + template: template, + encapsulation: ViewEncapsulation.None +}) +export class RadioGroupComponent { + + private _direction: Direction = Direction.vertical; + private _selectedValue: string; + + @HostBinding('class') classes = 'sdc-radio-group'; + + @Input() public legend: string; + @Input() public options: IOptionGroup; + @Input() public disabled: boolean; + + @Input() + get value(): string { + return this._selectedValue; + } + set value(value: string) { + if (this.isOptionExists(value)) { + this._selectedValue = value; + } + } + + @Output() public valueChange: EventEmitter = new EventEmitter(); + + @Input() + get direction(): string { + return Direction[this._direction]; + } + set direction(direction: string) { + this._direction = (direction === 'horizontal' ? Direction.horizontal : Direction.vertical); + } + + public onValueChanged(value): void { + this.valueChange.emit(value); + } + + private isOptionExists(value) { + const exist = this.options.items.find((item: IRadioButtonModel) => { + return item.value === value; + }); + return exist !== undefined; + } + +} diff --git a/src/angular/form-elements/validation/validatable.component.ts b/src/angular/form-elements/validation/validatable.component.ts new file mode 100644 index 0000000..4817dea --- /dev/null +++ b/src/angular/form-elements/validation/validatable.component.ts @@ -0,0 +1,25 @@ +import { Input, Component } from "@angular/core"; +import { ValidationComponent } from './validation.component'; +import { Subject } from 'rxjs/Subject'; +import { IValidatableComponent } from './validatable.interface'; + +export abstract class ValidatableComponent implements IValidatableComponent { + + // Each ValidatableComponent should handle the style in case of error, according to this boolean + public valid = true; + + // Each ValidatableComponent will notify when the value is changed. + public notifier: Subject; + + constructor() { + this.notifier = new Subject(); + } + + public abstract getValue(): any; + + // Each ValidatableComponent should call the valueChanged on value changed function. + protected valueChanged = (value: any): void => { + this.notifier.next(value); + } + +} diff --git a/src/angular/form-elements/validation/validatable.interface.ts b/src/angular/form-elements/validation/validatable.interface.ts new file mode 100644 index 0000000..6aceafe --- /dev/null +++ b/src/angular/form-elements/validation/validatable.interface.ts @@ -0,0 +1,5 @@ +export interface IValidatableComponent { + + getValue(): any; + +} diff --git a/src/angular/form-elements/validation/validation-group.component.html.ts b/src/angular/form-elements/validation/validation-group.component.html.ts new file mode 100644 index 0000000..dff591e --- /dev/null +++ b/src/angular/form-elements/validation/validation-group.component.html.ts @@ -0,0 +1,3 @@ +export default ` + +`; diff --git a/src/angular/form-elements/validation/validation-group.component.ts b/src/angular/form-elements/validation/validation-group.component.ts new file mode 100644 index 0000000..59ecf4c --- /dev/null +++ b/src/angular/form-elements/validation/validation-group.component.ts @@ -0,0 +1,47 @@ +import { Input, Component, ContentChildren, EventEmitter, Output, QueryList, SimpleChanges, HostBinding, AfterContentInit } from "@angular/core"; +import { AbstractControl, FormControl } from "@angular/forms"; +import { Subscribable } from "rxjs/Observable"; +import { AnonymousSubscription } from "rxjs/Subscription"; +import { IValidator } from './validators/validator.interface'; +import { ValidatorComponent } from './validators/base.validator.component'; +import { RegexValidatorComponent } from './validators/regex.validator.component'; +import { RequiredValidatorComponent } from './validators/required.validator.component'; +import { ValidatableComponent } from './validatable.component'; +import { ValidationComponent } from './validation.component'; +import { CustomValidatorComponent } from './validators/custom.validator.component'; +import template from "./validation.component.html"; + +@Component({ + selector: 'sdc-validation-group', + template +}) +export class ValidationGroupComponent implements AfterContentInit { + + @Input() public disabled: boolean; + @HostBinding('class') classes; + + @ContentChildren(ValidationComponent) public validationsComponents: QueryList; + + private supportedValidator: Array>; + + constructor() { + this.disabled = false; + this.classes = 'sdc-validation-group'; + } + + ngAfterContentInit(): void { + + } + + public validate(): boolean { + let validationResult = true; + // Iterate over all validationComponent inside the group and return boolean result true in case all validations passed. + this.validationsComponents.forEach((validationComponent) => { + if (validationComponent.validate()) { + validationResult = false; + } + }); + return validationResult; + } + +} diff --git a/src/angular/form-elements/validation/validation.component.html.ts b/src/angular/form-elements/validation/validation.component.html.ts new file mode 100644 index 0000000..0f11a23 --- /dev/null +++ b/src/angular/form-elements/validation/validation.component.html.ts @@ -0,0 +1,3 @@ +export default ` + +`; diff --git a/src/angular/form-elements/validation/validation.component.ts b/src/angular/form-elements/validation/validation.component.ts new file mode 100644 index 0000000..4abdd12 --- /dev/null +++ b/src/angular/form-elements/validation/validation.component.ts @@ -0,0 +1,79 @@ +import { Input, Component, ContentChildren, EventEmitter, Output, QueryList, SimpleChanges, HostBinding, AfterContentInit } from "@angular/core"; +import { AbstractControl, FormControl } from "@angular/forms"; +import { Subscribable } from "rxjs/Observable"; +import { AnonymousSubscription } from "rxjs/Subscription"; +import { IValidator } from './validators/validator.interface'; +import { ValidatorComponent } from './validators/base.validator.component'; +import { RegexValidatorComponent } from './validators/regex.validator.component'; +import { RequiredValidatorComponent } from './validators/required.validator.component'; +import { ValidatableComponent } from './validatable.component'; +import { CustomValidatorComponent } from './validators/custom.validator.component'; +import template from "./validation.component.html"; + +@Component({ + selector: 'sdc-validation', + template +}) +export class ValidationComponent implements AfterContentInit { + + @Input() public validateElement: ValidatableComponent; + @Input() public disabled: boolean; + @Output() public validityChanged: EventEmitter = new EventEmitter(); + @HostBinding('class') classes; + + // @ContentChildren does not recieve type any or IValidator or ValidatorComponent, so need to create @ContentChildren for each validator type. + @ContentChildren(RegexValidatorComponent) public regexValidator: QueryList; + @ContentChildren(RequiredValidatorComponent) public requireValidator: QueryList; + @ContentChildren(CustomValidatorComponent) public customValidator: QueryList; + + private supportedValidator: Array>; + + constructor() { + this.disabled = false; + this.classes = 'sdc-validation'; + } + + ngAfterContentInit(): void { + this.supportedValidator = [ + this.regexValidator, + this.requireValidator, + this.customValidator + ]; + + this.validateElement.notifier.subscribe( + (value) => { + const validationResult = this.validateOnChange(value); + this.validateElement.valid = validationResult; + }, + (error) => console.log('Validation subscribe error') + ); + } + + public validate = (): boolean => { + const value = this.validateElement.getValue(); + return this.validateOnChange(value); + } + + private validateOnChange(value: any): boolean { + if (this.disabled) { return true; } + + /** + * Iterate over all validators types (required, regex, etc...), and inside each iterate over + * all validators with same type, and return boolean result true in case all validations passed. + */ + const validationResult: boolean = this.supportedValidator.reduce((sum, validatorName) => { + const response: boolean = validatorName.reduce((_sum, validator) => { + return _sum && validator.validate(value); + }, true); + return sum && response; + }, true); + + if (this.validateElement.valid !== validationResult) { + this.validityChanged.emit(validationResult); + } + + return validationResult; + + } + +} diff --git a/src/angular/form-elements/validation/validation.module.ts b/src/angular/form-elements/validation/validation.module.ts new file mode 100644 index 0000000..4213f76 --- /dev/null +++ b/src/angular/form-elements/validation/validation.module.ts @@ -0,0 +1,35 @@ +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { SvgIconModule } from './../../svg-icon/svg-icon.module'; +import { ValidationComponent } from './validation.component'; +import { ValidatorComponent } from './validators/base.validator.component'; +import { RequiredValidatorComponent } from './validators/required.validator.component'; +import { RegexValidatorComponent } from './validators/regex.validator.component'; +import { CustomValidatorComponent } from './validators/custom.validator.component'; +import { ValidationGroupComponent } from './validation-group.component'; + +@NgModule({ + imports: [ + FormsModule, + CommonModule, + ReactiveFormsModule, + SvgIconModule + ], + declarations: [ + ValidationComponent, + RegexValidatorComponent, + RequiredValidatorComponent, + CustomValidatorComponent, + ValidationGroupComponent + ], + exports: [ + ValidationComponent, + RegexValidatorComponent, + RequiredValidatorComponent, + CustomValidatorComponent, + ValidationGroupComponent + ] +}) +export class ValidationModule { +} diff --git a/src/angular/form-elements/validation/validators/base.validator.component.html.ts b/src/angular/form-elements/validation/validators/base.validator.component.html.ts new file mode 100644 index 0000000..aba8eed --- /dev/null +++ b/src/angular/form-elements/validation/validators/base.validator.component.html.ts @@ -0,0 +1,10 @@ +export default ` + + +`; diff --git a/src/angular/form-elements/validation/validators/base.validator.component.ts b/src/angular/form-elements/validation/validators/base.validator.component.ts new file mode 100644 index 0000000..3d751af --- /dev/null +++ b/src/angular/form-elements/validation/validators/base.validator.component.ts @@ -0,0 +1,25 @@ +import { Input, Component, ContentChildren, EventEmitter, Output, QueryList, SimpleChanges, HostBinding } from "@angular/core"; +import { IValidator } from './validator.interface'; +import template from "./base.validator.component.html"; + +@Component({ + selector: 'sdc-validator', + template: template +}) +export abstract class ValidatorComponent { + + @Input() public message: any; + @Input() public disabled: boolean; + @HostBinding('class') classes; + + protected isValid: boolean; + + constructor() { + this.disabled = false; + this.isValid = true; + this.classes = 'sdc-validator sdc-label__error'; + } + + public abstract validate(value: any): boolean; + +} diff --git a/src/angular/form-elements/validation/validators/custom.validator.component.ts b/src/angular/form-elements/validation/validators/custom.validator.component.ts new file mode 100644 index 0000000..eb09636 --- /dev/null +++ b/src/angular/form-elements/validation/validators/custom.validator.component.ts @@ -0,0 +1,23 @@ +import { Input, Component } from "@angular/core"; +import { ValidatorComponent } from "./base.validator.component"; +import { IValidator } from './validator.interface'; +import template from "./base.validator.component.html"; + +@Component({ + selector: 'sdc-custom-validator', + template: template +}) +export class CustomValidatorComponent extends ValidatorComponent implements IValidator { + + @Input() public callback: (...args) => boolean; + + constructor() { + super(); + } + + public validate(value: any): boolean { + this.isValid = this.callback(value); + return this.isValid; + } + +} diff --git a/src/angular/form-elements/validation/validators/regex.validator.component.ts b/src/angular/form-elements/validation/validators/regex.validator.component.ts new file mode 100644 index 0000000..5929016 --- /dev/null +++ b/src/angular/form-elements/validation/validators/regex.validator.component.ts @@ -0,0 +1,24 @@ +import { Input, Component } from "@angular/core"; +import { ValidatorComponent } from "./base.validator.component"; +import { IValidator } from './validator.interface'; +import template from "./base.validator.component.html"; + +@Component({ + selector: 'sdc-regex-validator', + template: template +}) +export class RegexValidatorComponent extends ValidatorComponent implements IValidator { + + @Input() public pattern: RegExp; + + constructor() { + super(); + } + + public validate(value: any): boolean { + const regexp = new RegExp(this.pattern); + this.isValid = regexp.test(value); + return this.isValid; + } + +} diff --git a/src/angular/form-elements/validation/validators/required.validator.component.ts b/src/angular/form-elements/validation/validators/required.validator.component.ts new file mode 100644 index 0000000..7eee932 --- /dev/null +++ b/src/angular/form-elements/validation/validators/required.validator.component.ts @@ -0,0 +1,25 @@ +import { Input, Component } from "@angular/core"; +import { ValidatorComponent } from "./base.validator.component"; +import { IValidator } from './validator.interface'; +import template from "./base.validator.component.html"; + +@Component({ + selector: 'sdc-required-validator', + template: template +}) +export class RequiredValidatorComponent extends ValidatorComponent implements IValidator { + + constructor() { + super(); + } + + public validate(value: any): boolean { + if (value) { + this.isValid = true; + } else { + this.isValid = false; + } + return this.isValid; + } + +} diff --git a/src/angular/form-elements/validation/validators/validator.interface.ts b/src/angular/form-elements/validation/validators/validator.interface.ts new file mode 100644 index 0000000..c0adc24 --- /dev/null +++ b/src/angular/form-elements/validation/validators/validator.interface.ts @@ -0,0 +1,3 @@ +export interface IValidator { + validate(value: any): void; +} diff --git a/src/angular/index.ts b/src/angular/index.ts new file mode 100644 index 0000000..e8a54bd --- /dev/null +++ b/src/angular/index.ts @@ -0,0 +1,68 @@ +import { NgModule } from "@angular/core"; +import { FormElementsModule } from "./form-elements/form-elements.module"; +import { ButtonsModule } from "./buttons/buttons.module"; +import { ModalModule } from "./modals/modal.module"; +import { NotificationModule } from "./notifications/notification.module"; +import { PopupMenuModule } from "./popup-menu/popup-menu.module"; +import { AnimationDirectivesModule } from "./animations/animation-directives.module"; +import { InfiniteScrollModule } from "./infinite-scroll/infinite-scroll.module"; +import { TileModule } from "./tiles/tile.module"; +import { ChecklistModule } from "./checklist/checklist.module"; +import { SvgIconModule } from "./svg-icon/svg-icon.module"; +import { AutoCompleteModule } from "./autocomplete/autocomplete.module"; +import { FilterBarModule } from "./filterbar/filter-bar.module"; +import { SearchBarModule } from "./searchbar/search-bar.module"; +import { TooltipModule } from "./tooltip/tooltip.module"; +import { TagCloudModule } from './tag-cloud/tag-cloud.module'; +import { TabsModule } from "./tabs/tabs.module"; +import { AccordionModule } from "./accordion/accordion.module"; + +@NgModule({ + imports: [ + AnimationDirectivesModule, + ModalModule, + NotificationModule, + FormElementsModule, + ButtonsModule, + PopupMenuModule, + InfiniteScrollModule, + TileModule, + ChecklistModule, + AutoCompleteModule, + FilterBarModule, + SearchBarModule, + TooltipModule, + SvgIconModule, + TagCloudModule, + TabsModule, + AccordionModule + ], + exports: [ + AnimationDirectivesModule, + ModalModule, + NotificationModule, + FormElementsModule, + ButtonsModule, + PopupMenuModule, + InfiniteScrollModule, + TileModule, + ChecklistModule, + AutoCompleteModule, + FilterBarModule, + SearchBarModule, + TooltipModule, + SvgIconModule, + TagCloudModule, + TabsModule, + AccordionModule + ] +}) +export class SdcUiComponentsModule { +} + +import * as SdcUiComponents from './components'; +import * as SdcUiCommon from './common/index'; + +export { SdcUiComponentsNg1Module } from './ng1.module'; +export { SdcUiComponents }; +export { SdcUiCommon }; diff --git a/src/angular/infinite-scroll/infinite-scroll.directive.ts b/src/angular/infinite-scroll/infinite-scroll.directive.ts new file mode 100644 index 0000000..a8ea9f4 --- /dev/null +++ b/src/angular/infinite-scroll/infinite-scroll.directive.ts @@ -0,0 +1,35 @@ +import { Directive, ElementRef, Output, EventEmitter, HostListener, Input } from "@angular/core"; + +@Directive({ + selector: '[infiniteScroll]' +}) +export class InfiniteScrollDirective { + @Input() public infiniteScrollDistance: number = 0; + @Output() public infiniteScroll: EventEmitter; + + private scrollWasHit: boolean = false; + + constructor(private elemRef: ElementRef) { + this.infiniteScroll = new EventEmitter(); + } + + @HostListener('scroll', ['$event']) + public onScroll(evt) { + const scrollContainerElem: HTMLElement = evt.target; + if (scrollContainerElem !== this.elemRef.nativeElement) { + return; + } + + if (scrollContainerElem.scrollTop + scrollContainerElem.clientHeight + this.infiniteScrollDistance >= + scrollContainerElem.scrollHeight) { + // hit only once when entering the distance area from bottom + // (avoid emitting the handler while scrolling in the bottom area) + if (!this.scrollWasHit) { + this.infiniteScroll.emit(); + this.scrollWasHit = true; + } + } else if (this.scrollWasHit) { + this.scrollWasHit = false; + } + } +} diff --git a/src/angular/infinite-scroll/infinite-scroll.module.ts b/src/angular/infinite-scroll/infinite-scroll.module.ts new file mode 100644 index 0000000..8b559ca --- /dev/null +++ b/src/angular/infinite-scroll/infinite-scroll.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; +import { InfiniteScrollDirective } from "./infinite-scroll.directive"; + +@NgModule({ + declarations: [ + InfiniteScrollDirective + ], + exports: [ + InfiniteScrollDirective + ], +}) +export class InfiniteScrollModule { +} diff --git a/src/angular/modals/modal-button.component.ts b/src/angular/modals/modal-button.component.ts new file mode 100644 index 0000000..4fa5b7c --- /dev/null +++ b/src/angular/modals/modal-button.component.ts @@ -0,0 +1,29 @@ +import { Component, Input, HostListener } from "@angular/core"; +import { ButtonComponent } from "../buttons/button.component"; +import { ModalService } from "./modal.service"; +import template from "./../buttons/button.component.html"; + +@Component({ + selector: "sdc-modal-button", + template: template +}) +export class ModalButtonComponent extends ButtonComponent { + + @Input() public id?: string; + @Input() public callback: Function; + @Input() public closeModal: boolean; + @HostListener('click') invokeCallback = (): void => { + if (this.callback) { + this.callback(); + } + if (this.closeModal) { + this.modalService.closeModal(); + } + } + + constructor(private modalService: ModalService) { + super(); + this.closeModal = false; + } + +} diff --git a/src/angular/modals/modal-close-button.component.ts b/src/angular/modals/modal-close-button.component.ts new file mode 100644 index 0000000..e761019 --- /dev/null +++ b/src/angular/modals/modal-close-button.component.ts @@ -0,0 +1,34 @@ +import { Component, Input } from "@angular/core"; +import { ButtonComponent } from "../buttons/button.component"; +import { ModalService } from "./modal.service"; +import { RippleAnimationAction } from "../animations/ripple-click.animation.directive"; + +@Component({ + selector: "sdc-modal-close-button", + template: ` +
+ +
+ ` +}) +export class ModalCloseButtonComponent { + + @Input() testId: string; + @Input() disabled: boolean; + + public rippleAnimationAction: RippleAnimationAction = RippleAnimationAction.MOUSE_ENTER; + + constructor(private modalService: ModalService) { + } + + public closeModal = (): void => { + this.modalService.closeModal(); + } + +} diff --git a/src/angular/modals/modal.component.html.ts b/src/angular/modals/modal.component.html.ts new file mode 100644 index 0000000..90119ac --- /dev/null +++ b/src/angular/modals/modal.component.html.ts @@ -0,0 +1,38 @@ +export default ` +
+
+ +
+
+
+
+
+
+
+
+
+
{{ title }}
+ +
+
+
{{message}}
+
+
+ +
+
+ +`; diff --git a/src/angular/modals/modal.component.spec.ts b/src/angular/modals/modal.component.spec.ts new file mode 100644 index 0000000..372d59d --- /dev/null +++ b/src/angular/modals/modal.component.spec.ts @@ -0,0 +1,105 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, Input, NgModule, ViewContainerRef, Inject, Injectable, Type, ApplicationRef, ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Injector } from '@angular/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core/src/metadata/ng_module'; +import { ModalService } from './modal.service'; +import { CreateDynamicComponentService } from "../utils/create-dynamic-component.service"; +import { IModalConfig, ModalType, ModalSize } from "../../../src/angular/modals/models/modal-config"; +import { ModalInnerContent } from "../../../stories/ng2-component-lab/components/modal-inner-content-example.component"; + + +describe("Modal unit-tests", () => { + let testService: ModalService; + const testInputModal = { + size: 'xl', //'xl|l|md|sm|xsm' + title: 'Test_Title', + message: 'Test_Message', + modalVisible: true + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + providers:[ + ModalService, + { provide : CreateDynamicComponentService, useClass: CreateDynamicComponentServiceTest} + ], + declarations: [], + schemas:[NO_ERRORS_SCHEMA] + }) + testService = TestBed.get(ModalService); + })); + + it('Modal should be open test', () => { + let modalInstance = testService.openModal(testInputModal); + expect(modalInstance).toBeTruthy(); + }) + + it('Modal alert window test', () => { + let modalInstance = testService.openAlertModal('testAlert', 'testMessage'); + expect(modalInstance).toBeTruthy(); + }) + + it('Modal info window test', () => { + let modalInstance = testService.openErrorModal('testMessage', 'sampleTestId'); + expect(modalInstance).toBeTruthy(); + }) + + + it('Custom Modal should be open', () => { + let modalConfig:IModalConfig = { + size: ModalSize.medium, + title: 'Title', + type: ModalType.custom, + buttons: [{text:"Save & Close", closeModal:true}, + {text:"Save", callback:this.customModalOnSave, closeModal:false}, + {text:"Cancel", type: 'secondary', closeModal:true}] + }; + let modalInstance = testService.openCustomModal(modalConfig, ModalInnerContent, {name: "Sample Content"}); + expect(modalInstance).toBeTruthy(); + }) + + it('Shoul close window', () => { + let modalInstance = testService.openModal(testInputModal); + testService.closeModal(); + expect(modalInstance.instance.modalVisible).toBeFalsy(); + }) +}) + + +const testModalInstance = { + instance:{ + closeAnimationComplete:{ + subscribe:() => { + return true; + }, + }, + _createDynamicComponentService:{ + insertComponentDynamically:() => { + return true; + } + }, + modalVisible:true + }, + +}; + +@Component({ + selector: 'modal-test', + template: `
` +}) + + + +export class CreateDynamicComponentServiceTest { + modalVisble: true; + public createComponentDynamically = (modalInstance, customData) => { + return testModalInstance; + } + public insertComponentDynamically = () =>{ + return testModalInstance; + } + +} + + + + diff --git a/src/angular/modals/modal.component.ts b/src/angular/modals/modal.component.ts new file mode 100644 index 0000000..4f4d81f --- /dev/null +++ b/src/angular/modals/modal.component.ts @@ -0,0 +1,96 @@ +import { Component, Input, Output, ViewContainerRef, ViewChild, ComponentRef, trigger, state, animate, transition, style, EventEmitter, Renderer, ElementRef } from '@angular/core'; +import { ModalButtonComponent } from './modal-button.component'; +import { LowerCasePipe } from '@angular/common'; +import { ModalCloseButtonComponent } from './modal-close-button.component'; +import template from './modal.component.html'; + +@Component({ + selector: 'sdc-modal', + template: template, + animations: [ + trigger('toggleBackground', [ + transition('* => 1', [style({opacity: 0}), animate('.45s cubic-bezier(0.23, 1, 0.32, 1)')]), + transition('1 => *', [animate('.35s cubic-bezier(0.23, 1, 0.32, 1)', style({opacity: 0}))]) + ]), + trigger('toggleModal', [ + transition('* => 1', [style({opacity: 0, transform: 'translateY(-80px)'}), animate('.45s cubic-bezier(0.23, 1, 0.32, 1)')]), + transition('1 => *', [style({opacity: 1, transform: 'translateY(0px)'}), animate('.35s ease-in-out', style({opacity:0, transform: 'translateY(-80px)'}))]) + ]) + ] +}) + +export class ModalComponent { + + @Input() size: string; 'xl|l|md|sm|xsm'; + @Input() title: string; + @Input() message: string; + @Input() buttons: ModalButtonComponent[]; + @Input() type: string; 'info|error|alert|custom'; + @Input() testId: string; + @Output() closeAnimationComplete: EventEmitter = new EventEmitter(); + + @ViewChild('modalCloseButton') + set refCloseButton(_modalCloseButton: ModalCloseButtonComponent) { + this.modalCloseButton = _modalCloseButton; + } + + modalVisible: boolean; + // Allows for custom component as body instead of simple message. + // See ModalService.createActionModal for implementation details, and HttpService's catchError() for example. + @ViewChild('dynamicContentContainer', {read: ViewContainerRef}) dynamicContentContainer: ViewContainerRef; + innerModalContent: ComponentRef; + + public calculatedTestId: string; + public modalCloseButton: ModalCloseButtonComponent; + + constructor(private renderer: Renderer, + private lowerCasePipe: LowerCasePipe + ) { + this.modalVisible = true; + } + + getCalculatedTestId = (buttonText: string): string => { + // TODO: Replace this + if (this.testId) { + return this.testId + '-' + this.lowerCasePipe.transform(buttonText); + } + return null; + } + + public modalToggled = (toggleEvent: any) => { + if (!toggleEvent.toState) { + this.closeAnimationComplete.emit(); + } + } + + public getCloseButton = (): ModalCloseButtonComponent => { + return this.modalCloseButton; + } + + public getButtonById = (id: string): ModalButtonComponent => { + return this.buttons.find((button) => { + return button.id && button.id === id; + }); + } + + public getButtons = (): ModalButtonComponent[] => { + return this.buttons; + } + + public setButtons = (_buttons: ModalButtonComponent[]): void => { + this.buttons = _buttons; + } + + public getTitle = (): string => { + return this.title; + } + + public setTitle = (_title: string): void => { + this.title = _title; + } + + public hoverAnimation(evn: MouseEvent) { + this.renderer.setElementClass(evn.target as HTMLElement, 'sdc-ripple-click__animated', true); + // evn.taregt.classList.add('sdc-ripple-click__animated'); + } +} diff --git a/src/angular/modals/modal.module.ts b/src/angular/modals/modal.module.ts new file mode 100644 index 0000000..5697437 --- /dev/null +++ b/src/angular/modals/modal.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from "@angular/core"; +import { ModalComponent } from "./modal.component"; +import { ModalService } from "./modal.service"; +import { CommonModule, LowerCasePipe } from "@angular/common"; +import { ButtonsModule } from "../buttons/buttons.module"; +import { AnimationDirectivesModule } from "../animations/animation-directives.module"; +import { CreateDynamicComponentService } from "../utils/create-dynamic-component.service"; +import { ModalButtonComponent } from "./modal-button.component"; +import { ModalCloseButtonComponent } from "./modal-close-button.component"; +import { SvgIconModule } from "../svg-icon/svg-icon.module"; + +@NgModule({ + declarations: [ + ModalComponent, + ModalButtonComponent, + ModalCloseButtonComponent + ], + imports: [ + CommonModule, + ButtonsModule, + AnimationDirectivesModule, + SvgIconModule + ], + entryComponents: [ + ModalComponent, + ModalCloseButtonComponent + ], + exports: [ModalButtonComponent], + providers: [CreateDynamicComponentService, ModalService, LowerCasePipe] +}) +export class ModalModule { + +} diff --git a/src/angular/modals/modal.service.ts b/src/angular/modals/modal.service.ts new file mode 100644 index 0000000..d80ad1f --- /dev/null +++ b/src/angular/modals/modal.service.ts @@ -0,0 +1,100 @@ +import { Injectable, Type, ComponentRef } from '@angular/core'; +import { ModalComponent } from "./modal.component"; +import { CreateDynamicComponentService } from "../utils/create-dynamic-component.service"; +import { IModalConfig, ModalType, ModalSize, IModalButtonComponent } from "./models/modal-config"; + +@Injectable() +export class ModalService { + + private currentModal: ComponentRef; + + constructor(private createDynamicComponentService: CreateDynamicComponentService) { + } + + /* Shortcut method to open an alert modal with title, message, and close button that simply closes the modal. */ + public openAlertModal(title: string, message: string, actionButtonText?: string, actionButtonCallback?: Function, testId?: string) { + const modalConfig = { + size: ModalSize.small, + title: title, + message: message, + testId: testId, + buttons: this.createButtons('secondary', actionButtonText, actionButtonCallback), + type: ModalType.alert + } as IModalConfig; + const modalInstance: ComponentRef = this.openModal(modalConfig); + this.currentModal = modalInstance; + return modalInstance; + } + + public openActionModal = (title: string, message: string, actionButtonText?: string, actionButtonCallback?: Function, testId?: string): ComponentRef => { + const modalConfig = { + size: ModalSize.small, + title: title, + message: message, + testId: testId, + type: ModalType.standard, + buttons: this.createButtons('primary', actionButtonText, actionButtonCallback) + } as IModalConfig; + const modalInstance: ComponentRef = this.openModal(modalConfig); + this.currentModal = modalInstance; + return modalInstance; + } + + public openErrorModal = (errorMessage?: string, testId?: string): ComponentRef => { + const modalConfig = { + size: ModalSize.small, + title: 'Error', + message: errorMessage, + testId: testId, + buttons: [{text: "OK", type: "alert", closeModal: true}], + type: ModalType.error + } as IModalConfig; + const modalInstance: ComponentRef = this.openModal(modalConfig); + this.currentModal = modalInstance; + return modalInstance; + } + + public openCustomModal = (modalConfig: IModalConfig, dynamicComponentType: Type, dynamicComponentInput?: any) => { + const modalInstance: ComponentRef = this.openModal(modalConfig); + this.createInnnerComponent(dynamicComponentType, dynamicComponentInput); + return modalInstance; + } + + public createInnnerComponent = (dynamicComponentType: Type, dynamicComponentInput?: any): void => { + this.currentModal.instance.innerModalContent = this.createDynamicComponentService.insertComponentDynamically(dynamicComponentType, dynamicComponentInput, this.currentModal.instance.dynamicContentContainer); + } + + public openModal = (customModalData: IModalConfig): ComponentRef => { + const modalInstance: ComponentRef = this.createDynamicComponentService.createComponentDynamically(ModalComponent, customModalData); + modalInstance.instance.closeAnimationComplete.subscribe(() => { + this.destroyModal(); + }); + this.currentModal = modalInstance; + return modalInstance; + } + + public getCurrentInstance = () => { + return this.currentModal.instance; + } + + public closeModal = (): void => { // triggers closeModal animation, which then triggers toggleModal.done and the subscription to destroyModal + this.currentModal.instance.modalVisible = false; + } + + private createButtons = (type: string, actionButtonText?: string, actionButtonCallback?: Function): Array => { + const buttons: Array = []; + if (actionButtonText && actionButtonCallback) { + buttons.push({text: actionButtonText, type: type, callback: actionButtonCallback, closeModal: true}); + buttons.push({text: 'Cancel', type: 'secondary', closeModal: true}); + } else { + buttons.push({text: 'Cancel', type: type, closeModal: true}); + } + + return buttons; + } + + private destroyModal = (): void => { + this.currentModal.destroy(); + } + +} diff --git a/src/angular/modals/models/modal-config.ts b/src/angular/modals/models/modal-config.ts new file mode 100644 index 0000000..635942b --- /dev/null +++ b/src/angular/modals/models/modal-config.ts @@ -0,0 +1,44 @@ +import { Placement } from "../../common/enums"; + +export interface IModalConfig { + size?: string; // xl|l|md|sm|xsm + title?: string; + message?: string; + buttons?: IModalButtonComponent[]; + testId?: string; + type?: string; // 'info|error|alert'; +} + +export interface IButtonComponent { + text: string; + disabled?: boolean; + type?: string; + testId?: string; + preventDoubleClick?: boolean; + icon_name?: string; + icon_position?: string; + show_spinner?: boolean; + spinner_position?: Placement; + size?: string; +} + +export interface IModalButtonComponent extends IButtonComponent{ + id?: string; + callback?: Function; + closeModal?: boolean; +} + +export enum ModalType { + alert = "alert", + error = "error", + standard = "info", + custom = "custom" +} + +export enum ModalSize { + xlarge = "xl", + large = "l", + medium = "md", + small = "sm", + xsmall = "xsm" +} diff --git a/src/angular/ng1.module.ts b/src/angular/ng1.module.ts new file mode 100644 index 0000000..6f636f4 --- /dev/null +++ b/src/angular/ng1.module.ts @@ -0,0 +1,135 @@ +import { SdcUiComponentsModule } from './index'; +import { downgradeComponent, downgradeInjectable } from "@angular/upgrade/static"; +import * as Components from './components'; +declare const angular: any; + +let SdcUiComponentsNg1Module = null; + +if (typeof angular !== "undefined") { + + SdcUiComponentsNg1Module = angular.module('SdcUiComponentsNg1', []); + + // // Form Elements + SdcUiComponentsNg1Module.directive('sdcInput', downgradeComponent({ + component: Components.InputComponent, + inputs: ['label', 'value', 'pattern', 'disabled', 'placeHolder', 'required', 'minLength', 'maxLength', 'debounceTime'], + outputs: ['valueChange'] + })); + SdcUiComponentsNg1Module.directive('sdcDropdown', downgradeComponent({ + component: Components.DropDownComponent, + inputs: ['label', 'options', 'disabled', 'placeHolder', 'required', 'validate', 'headless', 'maxHeight', 'selectedOption'], + outputs: ['changeEmitter'] + })); + SdcUiComponentsNg1Module.directive('sdcCheckbox', downgradeComponent({ + component: Components.CheckboxComponent, + inputs: ['label', 'checked', 'disabled'], + outputs: ['checkedChange'] + })); + SdcUiComponentsNg1Module.directive('sdcRadioGroup', downgradeComponent({ + component: Components.RadioGroupComponent, + inputs: ['legend', 'options', 'disabled', 'value', 'direction'], + outputs: ['valueChange'] + })); + + // Buttons + SdcUiComponentsNg1Module.directive('sdcButton', downgradeComponent({ + component: Components.ButtonComponent, + inputs: ['text', 'disabled', 'type', 'size', 'preventDoubleClick', 'icon_name', 'icon_positon'] + })); + + // Modals + SdcUiComponentsNg1Module.service('SdcModalService', downgradeInjectable(Components.ModalService)); + SdcUiComponentsNg1Module.directive('sdcModal', downgradeComponent({ + component: Components.ModalComponent, + inputs: ['size', 'title', 'message', 'buttons', 'type'], + outputs: ['closeAnimationComplete'] + })); + SdcUiComponentsNg1Module.directive('sdcModalButton', downgradeComponent({ + component: Components.ModalButtonComponent, + inputs: ['callback', 'closeModal'] + })); + + // Notifications + SdcUiComponentsNg1Module.service('SdcNotificationService', downgradeInjectable(Components.NotificationsService)); + SdcUiComponentsNg1Module.directive('sdcNotificationContainer', downgradeComponent({ + component: Components.NotificationContainerComponent + })); + SdcUiComponentsNg1Module.directive('sdcNotification', downgradeComponent({ + component: Components.NotificationComponent, + inputs: ['notificationSetting'], + outputs: ['destroyComponent'] + })); + + // Popup Menu + SdcUiComponentsNg1Module.directive('popupMenuList', downgradeComponent({ + component: Components.PopupMenuListComponent, + inputs: ['open', 'position', 'className', 'relative'], + outputs: ['openChange', 'positionChange'] + })); + SdcUiComponentsNg1Module.directive('popupMenuItem', downgradeComponent({ + component: Components.PopupMenuItemComponent, + inputs: ['className', 'type'], + outputs: ['action'] + })); + + // Tiles + SdcUiComponentsNg1Module.directive('sdcTile', downgradeComponent({ + component: Components.TileComponent + })); + SdcUiComponentsNg1Module.directive('sdcTileHeader', downgradeComponent({ + component: Components.TileHeaderComponent + })); + SdcUiComponentsNg1Module.directive('sdcTileContent', downgradeComponent({ + component: Components.TileContentComponent + })); + SdcUiComponentsNg1Module.directive('sdcTileFooter', downgradeComponent({ + component: Components.TileFooterComponent + })); + + // Check List + SdcUiComponentsNg1Module.directive('sdcChecklist', downgradeComponent({ + component: Components.ChecklistComponent, + inputs: ['checklistModel'], + outputs: ['checkedChange'] + })); + + // Tag Cloud + SdcUiComponentsNg1Module.directive('sdcTagCloud', downgradeComponent({ + component: Components.TagCloudComponent, + inputs: ['list', 'isViewOnly', 'isUniqueList', 'uniqueErrorMessage', 'label', 'placeholder'], + outputs: ['listChanged'] + })); + SdcUiComponentsNg1Module.directive('sdcTagItem', downgradeComponent({ + component: Components.TagItemComponent, + inputs: ['text', 'isViewOnly', 'index'], + outputs: ['clickOnDelete'] + })); + + // Tabs + SdcUiComponentsNg1Module.directive('sdcTabs', downgradeComponent({ + component: Components.TabsComponent + })); + SdcUiComponentsNg1Module.directive('sdcTab', downgradeComponent({ + component: Components.TabComponent, + inputs: ['title', 'active'] + })); + + // Svg Icons + SdcUiComponentsNg1Module.directive('svgIcon', downgradeComponent({ + component: Components.SvgIconComponent, + inputs: ['name', 'mode', 'size', 'disabled', 'clickable', 'className'] + })); + SdcUiComponentsNg1Module.directive('svgIconLabel', downgradeComponent({ + component: Components.SvgIconLabelComponent, + inputs: ['name', 'mode', 'size', 'disabled', 'clickable', 'className', 'label', 'labelPlacement', 'labelClassName'] + })); + + // Accordion + SdcUiComponentsNg1Module.directive('sdcAccordion', downgradeComponent({ + component: Components.AccordionComponent, + inputs: ['arrow-direction', 'css-class', 'title', 'open'], + outputs: ['accordionChanged'] + })); +} + +export {SdcUiComponentsNg1Module}; diff --git a/src/angular/notifications/container/notifcontainer.component.html.ts b/src/angular/notifications/container/notifcontainer.component.html.ts new file mode 100644 index 0000000..df08bb4 --- /dev/null +++ b/src/angular/notifications/container/notifcontainer.component.html.ts @@ -0,0 +1,6 @@ +export default ` +
+ + +
+`; diff --git a/src/angular/notifications/container/notifcontainer.component.ts b/src/angular/notifications/container/notifcontainer.component.ts new file mode 100644 index 0000000..a922dc1 --- /dev/null +++ b/src/angular/notifications/container/notifcontainer.component.ts @@ -0,0 +1,31 @@ +import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { NotificationSettings } from "../utilities/notification.config"; +import { NotificationsService } from "../services/notifications.service"; +import template from "./notifcontainer.component.html"; + +@Component({ + selector: "sdc-notification-container", + template: template +}) +export class NotificationContainerComponent implements OnInit { + notifications: NotificationSettings[] = []; + + constructor(private notify: NotificationsService) { + } + + public ngOnInit(){ + this.notify.subscribe( (notif : NotificationSettings) => { + this.notifications.push(notif); + }); + } + + + private onDestroyed = (event : any):void =>{ + let index: number = this.notifications.indexOf(event); + if (index !== -1) { + this.notifications.splice(index, 1); + } + } + +} diff --git a/src/angular/notifications/notification-inner-content-example.component.ts b/src/angular/notifications/notification-inner-content-example.component.ts new file mode 100644 index 0000000..552f7b0 --- /dev/null +++ b/src/angular/notifications/notification-inner-content-example.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "innernotif-content", + template: ` +
+

Custom Notification

+
+ {{notifyTitle}} +
+
+ {{notifyText}} +
+
+` +}) +export class InnerNotifContent { + + @Input() notifyTitle:string; + @Input() notifyText:string; +} diff --git a/src/angular/notifications/notification.module.ts b/src/angular/notifications/notification.module.ts new file mode 100644 index 0000000..5891391 --- /dev/null +++ b/src/angular/notifications/notification.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { NotificationComponent } from "./notification/notification.component"; +import { NotificationContainerComponent } from "./container/notifcontainer.component"; +import { NotificationsService } from "./services/notifications.service"; +import { CreateDynamicComponentService } from "../utils/create-dynamic-component.service"; + + +@NgModule({ + declarations: [ + NotificationComponent, + NotificationContainerComponent, + ], + exports: [ + NotificationComponent, + NotificationContainerComponent, + ], + entryComponents: [ + NotificationComponent, + NotificationContainerComponent, + ], + imports: [ + CommonModule + ], + providers: [NotificationsService, CreateDynamicComponentService] +}) +export class NotificationModule { + +} diff --git a/src/angular/notifications/notification/notification.component.html.ts b/src/angular/notifications/notification/notification.component.html.ts new file mode 100644 index 0000000..450972e --- /dev/null +++ b/src/angular/notifications/notification/notification.component.html.ts @@ -0,0 +1,19 @@ +export default ` +
+
+
+
+
+
+
+ {{notificationSetting.notifyTitle}} +
+
+ {{notificationSetting.notifyText}} +
+
+
+
+
+
+`; diff --git a/src/angular/notifications/notification/notification.component.ts b/src/angular/notifications/notification/notification.component.ts new file mode 100644 index 0000000..476853c --- /dev/null +++ b/src/angular/notifications/notification/notification.component.ts @@ -0,0 +1,42 @@ +import { Component, Input, Output, EventEmitter, OnInit, ViewContainerRef, ViewChild } from "@angular/core"; +import { NotificationSettings } from "../utilities/notification.config"; +import { CreateDynamicComponentService } from "../../utils/create-dynamic-component.service"; +import template from "./notification.component.html"; + +@Component({ + selector: 'sdc-notification', + template: template +}) + +export class NotificationComponent implements OnInit { + + @Input() notificationSetting:NotificationSettings; + @Output() destroyComponent = new EventEmitter(); + @ViewChild("dynamicContentContainer", {read: ViewContainerRef}) contentContainer:ViewContainerRef; + private fade: boolean = false; + + constructor(private createDynamicComponentService: CreateDynamicComponentService) { + } + + public ngOnInit() { + if(this.notificationSetting.hasCustomContent){ + this.createDynamicComponentService.insertComponentDynamically(this.notificationSetting.innerComponentType, this.notificationSetting.innerComponentOptions, this.contentContainer); + } + + if(!this.notificationSetting.sticky){ + setTimeout(() => this.fadeOut(), this.notificationSetting.duration); + } + } + + private fadeOut = ():void => { + this.fade = true; + } + + private destroyMe() { + /*Only destroy on fade out, not on entry animation */ + if(this.fade){ + this.destroyComponent.emit(this.notificationSetting); + } + } + +} diff --git a/src/angular/notifications/services/notifications.service.ts b/src/angular/notifications/services/notifications.service.ts new file mode 100644 index 0000000..28a645c --- /dev/null +++ b/src/angular/notifications/services/notifications.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { NotificationSettings } from '../utilities/notification.config' +import { Subject } from 'rxjs/Subject'; +import { Subscription } from 'rxjs/Subscription'; + + +@Injectable() +export class NotificationsService { + + notifs : NotificationSettings[] = []; + + notifQueue : Subject = new Subject(); + + constructor() {} + + public push(notif : NotificationSettings):void{ + + if( this.notifQueue.observers.length > 0 ) { + this.notifQueue.next(notif); + } else { + this.notifs.push(notif); + } + } + + + + public getNotifications() : NotificationSettings[] { + return this.notifs; + } + + + + public subscribe(observer): Subscription { + let s:Subscription = this.notifQueue.subscribe(observer); + this.notifs.forEach(notif => this.notifQueue.next(notif)); + this.notifs = []; + return s; + } + + +} diff --git a/src/angular/notifications/utilities/notification.config.ts b/src/angular/notifications/utilities/notification.config.ts new file mode 100644 index 0000000..f469b7d --- /dev/null +++ b/src/angular/notifications/utilities/notification.config.ts @@ -0,0 +1,30 @@ +import { Type, ComponentRef } from '@angular/core'; + +export type NotificationType = + "info" | "warn" | "error" | "success"; + +export class NotificationSettings { + + public type: NotificationType; + public notifyText: string; + public notifyTitle: string; + public sticky: boolean; + public hasCustomContent :boolean; + public duration:number; + public innerComponentType: Type; + public innerComponentOptions : any; + + constructor(type: NotificationType, notifyText: string, notifyTitle: string, duration: number = 10000, sticky: boolean = false, hasCustomContent:boolean = false, innerComponentType?:Type, innerComponentOptions? :any) { + + this.type = type; + this.notifyText = notifyText; + this.notifyTitle = notifyTitle; + this.duration = duration; + this.sticky = sticky; + this.hasCustomContent = hasCustomContent; + this.innerComponentType = innerComponentType; + this.innerComponentOptions = innerComponentOptions; + } + + +} diff --git a/src/angular/popup-menu/popup-menu-item.component.spec.ts b/src/angular/popup-menu/popup-menu-item.component.spec.ts new file mode 100644 index 0000000..25b2694 --- /dev/null +++ b/src/angular/popup-menu/popup-menu-item.component.spec.ts @@ -0,0 +1,25 @@ +import { Component, Input, Output, ContentChildren, SimpleChanges, QueryList, EventEmitter, OnChanges, AfterContentInit } from '@angular/core'; +import { FormsModule } from "@angular/forms"; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { PopupMenuItemComponent } from './popup-menu-item.component'; +import { PopupMenuListComponent } from './popup-menu-list.component'; + +describe('Popup Menu', () => { + let component: PopupMenuListComponent; + let fixture: ComponentFixture; + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PopupMenuListComponent ], + }).compileComponents(); + fixture = TestBed.createComponent(PopupMenuListComponent); + component = fixture.componentInstance; + })); + + it('Popup menu component should be created', () => { + expect(component).toBeTruthy(); + }); + + it('Set Position to Popup Menu', () => { + expect(component.position).toEqual({ x: 0, y: 0 }) + }); +}) diff --git a/src/angular/popup-menu/popup-menu-item.component.ts b/src/angular/popup-menu/popup-menu-item.component.ts new file mode 100644 index 0000000..fb5a71d --- /dev/null +++ b/src/angular/popup-menu/popup-menu-item.component.ts @@ -0,0 +1,34 @@ +import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; +import { PopupMenuListComponent } from "./popup-menu-list.component"; + +@Component({ + selector: 'popup-menu-item', + template: + `
  • + +
  • ` +}) +export class PopupMenuItemComponent { + @Input() public className: string; + @Input() public type: undefined|'disabled'|'selected'|'separator'; + @Output() public action: EventEmitter = new EventEmitter(); + + public parentMenu: PopupMenuListComponent; + public index: number = 0; + + public performAction(evt) { + evt.stopPropagation(); + + if (['disabled', 'separator'].indexOf(this.type) !== -1) { + return; + } + + if (this.parentMenu instanceof PopupMenuListComponent) { + this.parentMenu.open = false; + } + + if (this.action) { + this.action.emit(); + } + } +} diff --git a/src/angular/popup-menu/popup-menu-list.component.ts b/src/angular/popup-menu/popup-menu-list.component.ts new file mode 100644 index 0000000..6a20423 --- /dev/null +++ b/src/angular/popup-menu/popup-menu-list.component.ts @@ -0,0 +1,65 @@ +import { Component, Input, Output, ContentChildren, SimpleChanges, QueryList, EventEmitter, OnChanges, AfterContentInit } from '@angular/core'; +import { PopupMenuItemComponent } from "./popup-menu-item.component"; + +export interface IPoint { + x: number; + y: number; +} + +@Component({ + selector: 'popup-menu-list', + template: + `
      + +
    ` +}) +export class PopupMenuListComponent implements AfterContentInit { + @Input() + public get open(): boolean { + return this._open; + } + public set open(isOpen: boolean) { + isOpen = isOpen !== undefined ? isOpen : false; + if (this._open !== isOpen) { + this._open = isOpen; + this.openChange.emit(this._open); + } + } + @Input() + public get position(): IPoint { + return this._position; + } + public set position(position: IPoint) { + position = position !== undefined ? position : {x: 0, y: 0}; + if (this._position.x !== position.x || this._position.y !== position.y) { + this._position = position; + this.positionChange.emit(this._position); + } + } + @Input() public className: string; + @Input() public relative: boolean = false; + @Output() public openChange: EventEmitter = new EventEmitter(); + @Output() public positionChange: EventEmitter = new EventEmitter(); + + @ContentChildren(PopupMenuItemComponent) private menuItems: QueryList; + + private _open: boolean = false; + private _position: IPoint = {x: 0, y: 0}; + + public ngAfterContentInit() { + this._updateMenuItemsList(this.menuItems); + this.menuItems.changes.subscribe(this._updateMenuItemsList); + } + + private _updateMenuItemsList(menuItemsList: QueryList) { + menuItemsList.forEach((c, idx) => { + c.parentMenu = this; + c.index = idx; + }); + } +} diff --git a/src/angular/popup-menu/popup-menu.module.ts b/src/angular/popup-menu/popup-menu.module.ts new file mode 100644 index 0000000..3a58b91 --- /dev/null +++ b/src/angular/popup-menu/popup-menu.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from "@angular/core"; +import { PopupMenuListComponent } from "./popup-menu-list.component"; +import { PopupMenuItemComponent } from "./popup-menu-item.component"; +import { CommonModule } from "@angular/common"; + + +@NgModule({ + declarations: [ + PopupMenuListComponent, + PopupMenuItemComponent + ], + imports: [ + CommonModule + ], + exports: [ + PopupMenuListComponent, + PopupMenuItemComponent + ], +}) +export class PopupMenuModule { +} diff --git a/src/angular/searchbar/search-bar.component.html.ts b/src/angular/searchbar/search-bar.component.html.ts new file mode 100644 index 0000000..79153f4 --- /dev/null +++ b/src/angular/searchbar/search-bar.component.html.ts @@ -0,0 +1,19 @@ +export default ` +
    + + + + + + + + + + + +
    +`; diff --git a/src/angular/searchbar/search-bar.component.ts b/src/angular/searchbar/search-bar.component.ts new file mode 100644 index 0000000..7f508d7 --- /dev/null +++ b/src/angular/searchbar/search-bar.component.ts @@ -0,0 +1,19 @@ +import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core'; +import template from "./search-bar.component.html"; + +@Component({ + selector: 'sdc-search-bar', + template: template +}) +export class SearchBarComponent { + + @HostBinding('class') classes = 'sdc-search-bar'; + @Input() public placeholder: string; + @Input() public label: string; + @Input() public searchQuery: string; + @Output() public searchQueryClick: EventEmitter = new EventEmitter(); + + private searchButtonClick = (): void => { + this.searchQueryClick.emit(this.searchQuery); + } +} diff --git a/src/angular/searchbar/search-bar.module.ts b/src/angular/searchbar/search-bar.module.ts new file mode 100644 index 0000000..27750f3 --- /dev/null +++ b/src/angular/searchbar/search-bar.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { SearchBarComponent } from "./search-bar.component"; +import { CommonModule } from "@angular/common"; +import { FormElementsModule } from "../form-elements/form-elements.module"; + +@NgModule({ + declarations: [ + SearchBarComponent + ], + imports: [CommonModule, + FormElementsModule], + exports: [ + SearchBarComponent + ], +}) +export class SearchBarModule { +} diff --git a/src/angular/svg-icon/svg-icon-label.component.html.ts b/src/angular/svg-icon/svg-icon-label.component.html.ts new file mode 100644 index 0000000..558b7c4 --- /dev/null +++ b/src/angular/svg-icon/svg-icon-label.component.html.ts @@ -0,0 +1,6 @@ +export default ` +
    + + {{ label }} +
    +`; diff --git a/src/angular/svg-icon/svg-icon-label.component.ts b/src/angular/svg-icon/svg-icon-label.component.ts new file mode 100644 index 0000000..5a00c3d --- /dev/null +++ b/src/angular/svg-icon/svg-icon-label.component.ts @@ -0,0 +1,26 @@ +import { Component, Input } from "@angular/core"; +import { SvgIconComponent } from './svg-icon.component'; +import { Mode, Size, Placement } from "../common/enums"; +import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; +import template from './svg-icon-label.component.html'; + +@Component({ + selector: 'svg-icon-label', + template: template, + styles: [` + :host { + display: inline-flex; + } + `] +}) +export class SvgIconLabelComponent extends SvgIconComponent { + + @Input() public label: string; + @Input() public labelPlacement: Placement; + @Input() public labelClassName: string; + + constructor(protected domSanitizer: DomSanitizer) { + super(domSanitizer); + this.labelPlacement = Placement.left; + } +} diff --git a/src/angular/svg-icon/svg-icon.component.html.ts b/src/angular/svg-icon/svg-icon.component.html.ts new file mode 100644 index 0000000..1baedbd --- /dev/null +++ b/src/angular/svg-icon/svg-icon.component.html.ts @@ -0,0 +1,3 @@ +export default ` +
    +`; diff --git a/src/angular/svg-icon/svg-icon.component.ts b/src/angular/svg-icon/svg-icon.component.ts new file mode 100644 index 0000000..d53981d --- /dev/null +++ b/src/angular/svg-icon/svg-icon.component.ts @@ -0,0 +1,77 @@ +import { Component, Input, OnChanges, SimpleChanges, HostBinding } from "@angular/core"; +import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; +import { Mode, Size } from "../common/enums"; +import iconsMap from '../../common/icons-map'; +import template from './svg-icon.component.html'; + +@Component({ + selector: 'svg-icon', + template: template, + styles: [` + :host { + display: inline-flex; + } + `] +}) +export class SvgIconComponent implements OnChanges { + + @Input() public name: string; + @Input() public mode: Mode; + @Input() public size: Size; + @Input() public disabled: boolean; + @Input() public clickable: boolean; + @Input() public className: any; + + public svgIconContent: string; + public svgIconContentSafeHtml: SafeHtml; + public svgIconCustomClassName: string; + private classes: string; + + constructor(protected domSanitizer: DomSanitizer) { + this.size = Size.medium; + this.disabled = false; + } + + static get Icons(): {[key: string]: string} { + return iconsMap; + } + + public ngOnChanges(changes: SimpleChanges) { + if (changes.name) { + this.updateSvgIconByName(); + this.buildClasses(); + } + } + + protected updateSvgIconByName() { + this.svgIconContent = SvgIconComponent.Icons[this.name] || null; + if (this.svgIconContent) { + this.svgIconContentSafeHtml = this.domSanitizer.bypassSecurityTrustHtml(this.svgIconContent); + this.svgIconCustomClassName = '__' + this.name.replace(/\s+/g, '_'); + } else { + this.svgIconContentSafeHtml = null; + this.svgIconCustomClassName = 'missing'; + } + } + + private buildClasses = (): void => { + const _classes = []; + _classes.push('svg-icon'); + if (this.mode) { + _classes.push('mode-' + this.mode); + } + if (this.size) { + _classes.push('size-' + this.size); + } + if (this.clickable) { + _classes.push('clickable'); + } + if (this.svgIconCustomClassName) { + _classes.push(this.svgIconCustomClassName); + } + if (this.className) { + _classes.push(this.className); + } + this.classes = _classes.join(" "); + } +} diff --git a/src/angular/svg-icon/svg-icon.module.ts b/src/angular/svg-icon/svg-icon.module.ts new file mode 100644 index 0000000..87a0d86 --- /dev/null +++ b/src/angular/svg-icon/svg-icon.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { SvgIconComponent } from "./svg-icon.component"; +import { SvgIconLabelComponent } from "./svg-icon-label.component"; + +@NgModule({ + declarations: [ + SvgIconComponent, + SvgIconLabelComponent + ], + imports: [ + CommonModule + ], + exports: [ + SvgIconComponent, + SvgIconLabelComponent + ], +}) + +export class SvgIconModule { +} diff --git a/src/angular/tabs/children/tab.component.html.ts b/src/angular/tabs/children/tab.component.html.ts new file mode 100644 index 0000000..36ff413 --- /dev/null +++ b/src/angular/tabs/children/tab.component.html.ts @@ -0,0 +1,5 @@ +export default ` +
    + +
    +`; diff --git a/src/angular/tabs/children/tab.component.ts b/src/angular/tabs/children/tab.component.ts new file mode 100644 index 0000000..3b96e87 --- /dev/null +++ b/src/angular/tabs/children/tab.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { Mode } from './../../common/enums'; +import template from "./tab.component.html"; + +@Component({ + selector: 'sdc-tab', + template: template +}) +export class TabComponent { + @Input() public title: string; + @Input() public titleIcon: string; + @Input() public active = false; + + public titleIconMode = Mode.secondary; + +} diff --git a/src/angular/tabs/tabs.component.html.ts b/src/angular/tabs/tabs.component.html.ts new file mode 100644 index 0000000..2333b86 --- /dev/null +++ b/src/angular/tabs/tabs.component.html.ts @@ -0,0 +1,14 @@ +export default ` +
      + +
    + +`; diff --git a/src/angular/tabs/tabs.component.ts b/src/angular/tabs/tabs.component.ts new file mode 100644 index 0000000..595f304 --- /dev/null +++ b/src/angular/tabs/tabs.component.ts @@ -0,0 +1,41 @@ +import { Component, Input, AfterContentInit, ContentChildren, QueryList, HostBinding } from '@angular/core'; +import { TabComponent } from './children/tab.component'; +import { SvgIconComponent } from "./../../../src/angular/svg-icon/svg-icon.component"; +import { Mode, Placement, Size } from './../common/enums'; +import template from "./tabs.component.html"; + +@Component({ + selector: 'sdc-tabs', + template: template +}) + +export class TabsComponent implements AfterContentInit { + + @HostBinding('class') classes = 'sdc-tabs sdc-tabs-header'; + @ContentChildren(TabComponent) private tabs: QueryList; + + public _size = Size.medium; + + public selectTab(tab: TabComponent) { + // deactivate all tabs + this.tabs.toArray().forEach((_tab: TabComponent) => { + _tab.active = false; + _tab.titleIconMode = Mode.secondary; + }); + + // activate the tab the user has clicked on. + tab.active = true; + tab.titleIconMode = Mode.primary; + } + + public ngAfterContentInit() { + // get all active tabs + const activeTabs = this.tabs.filter((tab) => tab.active); + + // if there is no active tab set, activate the first + if (activeTabs.length === 0) { + this.selectTab(this.tabs.first); + } + } + + } diff --git a/src/angular/tabs/tabs.module.ts b/src/angular/tabs/tabs.module.ts new file mode 100644 index 0000000..107942d --- /dev/null +++ b/src/angular/tabs/tabs.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormElementsModule } from "../form-elements/form-elements.module"; +import { TabsComponent } from "./tabs.component"; +import { TabComponent } from './children/tab.component'; +import { SvgIconModule } from './../svg-icon/svg-icon.module'; + +@NgModule({ + declarations: [ + TabsComponent, + TabComponent + ], + imports: [ + CommonModule, + SvgIconModule + ], + exports: [ + TabsComponent, + TabComponent + ] +}) +export class TabsModule { +} diff --git a/src/angular/tag-cloud/tag-cloud.component.html.ts b/src/angular/tag-cloud/tag-cloud.component.html.ts new file mode 100644 index 0000000..2ff4e8a --- /dev/null +++ b/src/angular/tag-cloud/tag-cloud.component.html.ts @@ -0,0 +1,30 @@ +export default ` +
    + +
    + + + + + + + + + + +
    +
    +
    + +
    +
    {{uniqueErrorMessage}}
    +`; diff --git a/src/angular/tag-cloud/tag-cloud.component.ts b/src/angular/tag-cloud/tag-cloud.component.ts new file mode 100644 index 0000000..1635b8d --- /dev/null +++ b/src/angular/tag-cloud/tag-cloud.component.ts @@ -0,0 +1,46 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import template from "./tag-cloud.component.html"; + +@Component({ + selector: 'sdc-tag-cloud', + template: template, +}) +export class TagCloudComponent { + @Input() public list: string[]; + @Input() public isViewOnly: boolean|number[]; // get a boolean parameter or array of specific items indexes. + @Input() public isUniqueList: boolean; + @Input() public uniqueErrorMessage: string = "Unique error"; + @Input() public label: string; + @Input() public placeholder: string; + @Output() public listChanged: EventEmitter = new EventEmitter(); + private newTagItem: string; + private uniqueError: boolean; + + private onKeyup = (e): void => { + if (e.keyCode === 13) { + this.insertItemToList(); + } + } + + private insertItemToList = (): void => { + this.validateTag(); + if (!this.uniqueError && this.newTagItem.length) { + this.list.push(this.newTagItem); + this.newTagItem = ""; + this.listChanged.emit(this.list); + } + } + + private deleteItemFromList = (index: number): void => { + this.list.splice(index, 1); + if (Array.isArray(this.isViewOnly)) { + this.isViewOnly = this.isViewOnly.map((i: number) => { + return i > index ? i - 1 : i; + }); + } + } + + private validateTag = (): void => { + this.uniqueError = this.list && this.list.indexOf(this.newTagItem) > -1; + } +} diff --git a/src/angular/tag-cloud/tag-cloud.module.ts b/src/angular/tag-cloud/tag-cloud.module.ts new file mode 100644 index 0000000..fd7efb4 --- /dev/null +++ b/src/angular/tag-cloud/tag-cloud.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from "@angular/core"; +import { TagItemComponent } from "./tag-item/tag-item.component"; +import { TagCloudComponent } from "./tag-cloud.component"; +import { CommonModule } from "@angular/common"; +import { FormElementsModule } from './../form-elements/form-elements.module'; + +@NgModule({ + declarations: [ + TagItemComponent, + TagCloudComponent + ], + imports: [ + CommonModule, + FormElementsModule + ], + exports: [ + TagCloudComponent + ] +}) +export class TagCloudModule { +} diff --git a/src/angular/tag-cloud/tag-item/tag-item.component.html.ts b/src/angular/tag-cloud/tag-item/tag-item.component.html.ts new file mode 100644 index 0000000..04112c1 --- /dev/null +++ b/src/angular/tag-cloud/tag-item/tag-item.component.html.ts @@ -0,0 +1,16 @@ +export default ` +
    + {{text}} + + + + + + + + + + +
    +`; + diff --git a/src/angular/tag-cloud/tag-item/tag-item.component.ts b/src/angular/tag-cloud/tag-item/tag-item.component.ts new file mode 100644 index 0000000..f2e2fa7 --- /dev/null +++ b/src/angular/tag-cloud/tag-item/tag-item.component.ts @@ -0,0 +1,15 @@ +import { Component, EventEmitter, Input, Output, HostBinding } from "@angular/core"; +import template from "./tag-item.component.html"; + +@Component({ + selector: 'sdc-tag-item', + template: template +}) + +export class TagItemComponent { + @HostBinding('class') classes = 'sdc-tag-item'; + @Input() public text: string; + @Input() public isViewOnly: boolean; + @Input() public index: number; + @Output() public clickOnDelete: EventEmitter = new EventEmitter(); +} diff --git a/src/angular/tiles/children/tile-content.component.ts b/src/angular/tiles/children/tile-content.component.ts new file mode 100644 index 0000000..741db26 --- /dev/null +++ b/src/angular/tiles/children/tile-content.component.ts @@ -0,0 +1,10 @@ +import { Component, HostBinding } from "@angular/core"; + +@Component({ + selector: 'sdc-tile-content', + template: '' +}) + +export class TileContentComponent { + @HostBinding('class') classes = 'sdc-tile-content'; +} diff --git a/src/angular/tiles/children/tile-footer.component.ts b/src/angular/tiles/children/tile-footer.component.ts new file mode 100644 index 0000000..519c5ba --- /dev/null +++ b/src/angular/tiles/children/tile-footer.component.ts @@ -0,0 +1,10 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + selector: 'sdc-tile-footer', + template: '' +}) + +export class TileFooterComponent { + @HostBinding('class') classes = 'sdc-tile-footer'; +} diff --git a/src/angular/tiles/children/tile-header.component.ts b/src/angular/tiles/children/tile-header.component.ts new file mode 100644 index 0000000..b040bcb --- /dev/null +++ b/src/angular/tiles/children/tile-header.component.ts @@ -0,0 +1,10 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + selector: "sdc-tile-header", + template: '' +}) + +export class TileHeaderComponent { + @HostBinding('class') classes = 'sdc-tile-header'; +} diff --git a/src/angular/tiles/tile.component.html.ts b/src/angular/tiles/tile.component.html.ts new file mode 100644 index 0000000..81803d5 --- /dev/null +++ b/src/angular/tiles/tile.component.html.ts @@ -0,0 +1,5 @@ +export default ` + + + +`; diff --git a/src/angular/tiles/tile.component.ts b/src/angular/tiles/tile.component.ts new file mode 100644 index 0000000..3791ca0 --- /dev/null +++ b/src/angular/tiles/tile.component.ts @@ -0,0 +1,11 @@ +import { Component, HostBinding } from '@angular/core'; +import template from "./tile.component.html"; + +@Component({ + selector: "sdc-tile", + template: template +}) + +export class TileComponent { + @HostBinding('class') classes = 'sdc-tile'; +} diff --git a/src/angular/tiles/tile.module.ts b/src/angular/tiles/tile.module.ts new file mode 100644 index 0000000..43c750b --- /dev/null +++ b/src/angular/tiles/tile.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from "@angular/core"; +import { TileComponent } from "./tile.component"; +import { CommonModule } from "@angular/common"; +import { TileContentComponent } from "./children/tile-content.component"; +import { TileFooterComponent } from "./children/tile-footer.component"; +import { TileHeaderComponent } from "./children/tile-header.component"; + +@NgModule({ + declarations: [ + TileComponent, + TileContentComponent, + TileFooterComponent, + TileHeaderComponent + ], + imports: [CommonModule], + entryComponents: [TileComponent], + exports: [ + TileComponent, + TileContentComponent, + TileFooterComponent, + TileHeaderComponent + ] +}) + +export class TileModule { + +} diff --git a/src/angular/tooltip/tooltip-template.component.ts b/src/angular/tooltip/tooltip-template.component.ts new file mode 100644 index 0000000..7cb7f72 --- /dev/null +++ b/src/angular/tooltip/tooltip-template.component.ts @@ -0,0 +1,20 @@ +import { Component, ViewChild, ViewContainerRef, AfterViewInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +@Component({ + selector: 'tooltip-template', + template: ` +
    + +
    ` +}) + +export class TooltipTemplateComponent implements AfterViewInit { + @ViewChild('templateContainer', {read: ViewContainerRef}) public container: ViewContainerRef; + + public viewReady: BehaviorSubject = new BehaviorSubject(false); + + ngAfterViewInit() : void { + this.viewReady.next(true); + } +} diff --git a/src/angular/tooltip/tooltip.directive.ts b/src/angular/tooltip/tooltip.directive.ts new file mode 100644 index 0000000..77cec62 --- /dev/null +++ b/src/angular/tooltip/tooltip.directive.ts @@ -0,0 +1,459 @@ +import { Directive, ElementRef, HostListener, OnInit, Input, Renderer, TemplateRef } from '@angular/core'; +import { TooltipTemplateComponent } from './tooltip-template.component'; +import { CreateDynamicComponentService } from '../utils/create-dynamic-component.service'; + +const pixel = 'px'; +const leftStyle = 'left'; +const topStyle = 'top'; +const showSuffix = 'show'; +const rightBottomSuffix = 'right__bottom'; +const centerMiddleSuffix = 'center__middle'; + +@Directive({ + selector: '[sdc-tooltip]' +}) +export class TooltipDirective implements OnInit { + @Input('tooltip-text') public text = 'tooltip'; + @Input('tooltip-placement') public placement: TooltipPlacement = TooltipPlacement.Top; + @Input('tooltip-css-class') public customCssClass: string; + @Input('tooltip-template') public template: TemplateRef; + @Input('tooltip-arrow-offset') public arrowOffset: number = 10; + @Input('tooltip-arrow-placement') public arrowPlacement: ArrowPlacement = ArrowPlacement.LeftTop; + @Input('tooltip-offset') public tooltipOffset: number = 3; + + private cssClass: string = 'sdc-tooltip'; // default css class + private tooltip: any; // tooltip html element + private elemPosition: any; + private tooltipTemplateContainer: any; + + private scrollEventHandler = () => {}; + + constructor( + private elementRef: ElementRef, + private service: CreateDynamicComponentService, + private renderer: Renderer) { + + this.elementRef.nativeElement.title = ""; + } + + @HostListener('mouseenter') + public onMouseEnter() { + this.show(); + this.activateScrollEvent(); + } + + @HostListener('mouseleave') + public onMouseLeave() { + this.hide(); + this.deactivateScrollEvent(); + } + + ngOnInit(): void { + this.initScrollEvent(); + } + + private get ScreenWidth() { + return document.documentElement.clientWidth; + } + + private get ScreenHeight() { + return document.documentElement.clientHeight; + } + + private create() { + this.tooltipTemplateContainer = this.service.createComponentDynamically(TooltipTemplateComponent, document.body); + + /** + * Creating a view (injecting our template) from template in our component. + */ + this.tooltip = this.tooltipTemplateContainer.location.nativeElement.querySelector( + '.sdc-tooltip-template-container'); + + if (this.template) { + this.tooltipTemplateContainer.instance.container.createEmbeddedView(this.template); + } else { + this.tooltip.textContent = this.text ? this.text : 'tooltip'; + } + + this.setCssClass(true); + } + + private destroy() { + this.tooltipTemplateContainer.destroy(); + this.tooltip = null; + } + + private show() { + this.create(); + + /** + * View is ready (AfterViewInit event in template component) + */ + this.tooltipTemplateContainer.instance.viewReady.subscribe((isReady) => { + if (isReady) { + this.setPosition(); + this.toggleShowCssClass(true); // add css class + } + }); + } + + private hide() { + this.toggleShowCssClass(false); // remove css class + + this.destroy(); + } + + private toggleShowCssClass(isAdd: boolean) { + if (this.tooltip) { + this.setCssClass(isAdd, '-' + showSuffix); + } + } + + /** + * Adds placement css class and sets tooltip position in style + */ + private setPosition() { + const tooltipPos: IPlacementData = this.getPlacementData(); + + const placementSuffix: string = TooltipPlacement[tooltipPos.placement].toLowerCase(); + + this.setCssClass(true, '-' + placementSuffix); + + this.setAdditionalCssClass(placementSuffix); + + this.renderer.setElementStyle(this.tooltip, topStyle, tooltipPos.top + pixel); + this.renderer.setElementStyle(this.tooltip, leftStyle, tooltipPos.left + pixel); + } + + private setAdditionalCssClass(placementSuffix: string) { + if (this.arrowPlacement === ArrowPlacement.RightBottom) { + this.setCssClass(true, '-' + placementSuffix + '-' + rightBottomSuffix); + } else if (this.arrowPlacement === ArrowPlacement.CenterMiddle) { + this.setCssClass(true, '-' + placementSuffix + '-' + centerMiddleSuffix); + } + } + + private setCssClass(isAdd: boolean, suffix: string = '') { + this.renderer.setElementClass(this.tooltip, this.cssClass + suffix, isAdd); + + if (this.customCssClass) { + this.renderer.setElementClass(this.tooltip, this.customCssClass + suffix, isAdd); + } + } + + /** + * Checks the specified placement (first element in array), if it is not valid - checks other placements + * @returns {IPlacementData} + */ + private getPlacementData(): IPlacementData { + const placement: TooltipPlacement = this.placement; + let tooltipPos: IPlacementData; + + const tooltipPosWithPlacement = this.getPlacement.bind(this, placement); + + // TODO add comments - done + switch (placement) { + case TooltipPlacement.Left: + tooltipPos = tooltipPosWithPlacement( + TooltipPlacement.Right, + TooltipPlacement.Top, + TooltipPlacement.Bottom); + break; + + case TooltipPlacement.Right: + tooltipPos = tooltipPosWithPlacement( + TooltipPlacement.Left, + TooltipPlacement.Top, + TooltipPlacement.Bottom); + break; + + case TooltipPlacement.Top: + tooltipPos = tooltipPosWithPlacement( + TooltipPlacement.Bottom, + TooltipPlacement.Left, + TooltipPlacement.Right); + break; + + case TooltipPlacement.Bottom: + tooltipPos = tooltipPosWithPlacement( + TooltipPlacement.Top, + TooltipPlacement.Left, + TooltipPlacement.Right); + break; + } + + return tooltipPos; + } + + /** + * Returns valid tooltip position data + * @param {TooltipPlacement} placement + * @param {TooltipPlacement} additionalPlacements + * @returns {IPlacementData} + */ + private getPlacement(placement: TooltipPlacement, + ...additionalPlacements: TooltipPlacement[], + ): IPlacementData { + const placements: TooltipPlacement[] = [placement, ...additionalPlacements]; + const filterPlacements = placements + .map((pl) => this.getPosition(pl)) + .filter((item) => this.validatePosition(item)); + return filterPlacements.length > 0 ? filterPlacements[0] : this.getPosition(placement); + } + + /** + * Returns input data for getPosition method + * @returns {ITooltipPositionParams} + */ + private getPlacementInputParams(): ITooltipPositionParams { + this.elemPosition = this.elementRef.nativeElement.getBoundingClientRect(); + + return { + elemHeight: this.elementRef.nativeElement.offsetHeight, + elemLeft: this.elemPosition.left, + elemTop: this.elemPosition.top, + elemWidth: this.elementRef.nativeElement.offsetWidth, + pageYOffset: window.pageYOffset, + tooltipHeight: this.tooltip.offsetHeight, // .clientHeight, + tooltipOffset: this.tooltipOffset, + tooltipWidth: this.tooltip.offsetWidth, + arrowOffset: this.arrowOffset + }; + } + + /** + * Returns tooltip position data + * @param {TooltipPlacement} placement (left, top, right, bottom) + * @returns {IPlacementData} + */ + private getPosition(placement: TooltipPlacement): IPlacementData { + switch(this.arrowPlacement) { + case ArrowPlacement.LeftTop: + return this.getLeftTopPosition(placement); + + case ArrowPlacement.RightBottom: + return this.getRightBottomPosition(placement); + } + + return this.getCenterMiddlePosition(placement); + } + + /** + * Returns tooltip position data (center / middle arrow) + * @param {TooltipPlacement} placement (left, top, right, bottom) + * @returns {IPlacementData} + */ + private getCenterMiddlePosition(placement: TooltipPlacement): IPlacementData { + let left = 0; + let top = 0; + + const inputPos: ITooltipPositionParams = this.getPlacementInputParams(); + switch (placement) { + case TooltipPlacement.Left: + left = inputPos.elemLeft - inputPos.tooltipWidth - inputPos.tooltipOffset; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight / 2 - inputPos.tooltipHeight / 2; + break; + + case TooltipPlacement.Right: + left = inputPos.elemLeft + inputPos.elemWidth + inputPos.tooltipOffset; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight / 2 - inputPos.tooltipHeight / 2; + break; + + case TooltipPlacement.Top: + left = inputPos.elemLeft + inputPos.elemWidth / 2 - inputPos.tooltipWidth / 2; + top = inputPos.elemTop + inputPos.pageYOffset - inputPos.tooltipHeight - inputPos.tooltipOffset; + break; + + case TooltipPlacement.Bottom: + left = inputPos.elemLeft + inputPos.elemWidth / 2 - inputPos.tooltipWidth / 2; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight + inputPos.tooltipOffset; + break; + } + + return { + height: inputPos.tooltipHeight, + left, + placement, + top, + width: inputPos.tooltipWidth, + pageYOffset: inputPos.pageYOffset + } as IPlacementData; + } + + /** + * Returns tooltip position data (left / top arrow) + * @param {TooltipPlacement} placement (left, top, right, bottom) + * @returns {IPlacementData} + */ + private getLeftTopPosition(placement: TooltipPlacement): IPlacementData { + let left = 0; + let top = 0; + + const inputPos: ITooltipPositionParams = this.getPlacementInputParams(); + switch (placement) { + case TooltipPlacement.Left: + left = inputPos.elemLeft - inputPos.tooltipWidth - inputPos.tooltipOffset; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight / 2 - inputPos.arrowOffset; + break; + + case TooltipPlacement.Right: + left = inputPos.elemLeft + inputPos.elemWidth + inputPos.tooltipOffset; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight / 2 - inputPos.arrowOffset; + break; + + case TooltipPlacement.Top: + left = inputPos.elemLeft + inputPos.elemWidth / 2 - inputPos.arrowOffset; + top = inputPos.elemTop + inputPos.pageYOffset - inputPos.tooltipHeight - inputPos.tooltipOffset; + break; + + case TooltipPlacement.Bottom: + left = inputPos.elemLeft + inputPos.elemWidth / 2 - inputPos.arrowOffset; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight + inputPos.tooltipOffset; + break; + } + + return { + height: inputPos.tooltipHeight, + left, + placement, + top, + width: inputPos.tooltipWidth, + pageYOffset: inputPos.pageYOffset + } as IPlacementData; + } + + /** + * Returns tooltip position data (right / bottom arrow) + * @param {TooltipPlacement} placement (left, top, right, bottom) + * @returns {IPlacementData} + */ + private getRightBottomPosition(placement: TooltipPlacement): IPlacementData { + let left = 0; + let top = 0; + + const inputPos: ITooltipPositionParams = this.getPlacementInputParams(); + switch (placement) { + case TooltipPlacement.Left: + left = inputPos.elemLeft - inputPos.tooltipWidth - inputPos.tooltipOffset; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight / 2 - inputPos.tooltipHeight + inputPos.arrowOffset; + break; + + case TooltipPlacement.Right: + left = inputPos.elemLeft + inputPos.elemWidth + inputPos.tooltipOffset; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight / 2 - inputPos.tooltipHeight + inputPos.arrowOffset; + break; + + case TooltipPlacement.Top: + left = inputPos.elemLeft + inputPos.elemWidth / 2 - inputPos.tooltipWidth + inputPos.arrowOffset; + top = inputPos.elemTop + inputPos.pageYOffset - inputPos.tooltipHeight - inputPos.tooltipOffset; + break; + + case TooltipPlacement.Bottom: + left = inputPos.elemLeft + inputPos.elemWidth / 2 - inputPos.tooltipWidth + inputPos.arrowOffset; + top = inputPos.elemTop + inputPos.pageYOffset + inputPos.elemHeight + inputPos.tooltipOffset; + break; + } + + return { + height: inputPos.tooltipHeight, + left, + placement, + top, + width: inputPos.tooltipWidth, + pageYOffset: inputPos.pageYOffset + } as IPlacementData; + } + + /** + * Checks if tooltip position is valid + * @param {IPlacementData} pos + * @returns {boolean} + */ + private validatePosition(pos: IPlacementData): boolean { + if (pos.left < 0 || pos.left + pos.width - 1 > this.ScreenWidth) { + return false; + } + + if (pos.top - pos.pageYOffset < 0 || pos.top - pos.pageYOffset + pos.height - 1 > this.ScreenHeight) { + return false; + } + + return true; + } + + /** + * Scrolling + */ + + private debounce(func: Function, wait: number, immediate?: boolean) { + let timeout; + return function() { + const context = this; + const args = arguments; + const later = () => { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; + } + + private initScrollEvent() { + this.scrollEventHandler = this.debounce(() => { + try { + this.setPosition(); + } catch (e) { + + } + }, 10); + } + + private activateScrollEvent() { + window.addEventListener('scroll', this.scrollEventHandler , true); + } + + private deactivateScrollEvent() { + window.removeEventListener('scroll', this.scrollEventHandler , true); + } +} + +export enum TooltipPlacement { + Left, + Right, + Top, + Bottom +} + +export enum ArrowPlacement { + CenterMiddle, + LeftTop, + RightBottom +} + +interface ITooltipPositionParams { + elemLeft: number; + elemTop: number; + elemWidth: number; + elemHeight: number; + tooltipWidth: number; + tooltipHeight: number; + tooltipOffset: number; + pageYOffset: number; + arrowOffset: number; +} + +interface IPlacementData { + left: number; + top: number; + width: number; + height: number; + pageYOffset: number; + placement?: TooltipPlacement; +} diff --git a/src/angular/tooltip/tooltip.module.ts b/src/angular/tooltip/tooltip.module.ts new file mode 100644 index 0000000..a4ad86d --- /dev/null +++ b/src/angular/tooltip/tooltip.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { TooltipDirective } from './tooltip.directive'; +import { TooltipTemplateComponent } from './tooltip-template.component'; + +@NgModule({ + declarations: [ + TooltipDirective, + TooltipTemplateComponent + ], + imports: [], + entryComponents: [TooltipTemplateComponent], + exports: [ + TooltipDirective + ], +}) +export class TooltipModule { +} diff --git a/src/angular/utils/create-dynamic-component.service.ts b/src/angular/utils/create-dynamic-component.service.ts new file mode 100644 index 0000000..428dd73 --- /dev/null +++ b/src/angular/utils/create-dynamic-component.service.ts @@ -0,0 +1,101 @@ +import { Injectable, Type, ApplicationRef, ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Injector } from '@angular/core'; +import { ViewContainerRef } from '@angular/core/src/linker/view_container_ref'; + +@Injectable() +export class CreateDynamicComponentService { + + constructor(private componentFactoryResolver: ComponentFactoryResolver, + private applicationRef: ApplicationRef, + private injector: Injector) { + } + + /** + * Gets the root view container to inject the component to. + * + * @returns {ComponentRef} + * + * @memberOf InjectionService + */ + private getRootViewContainer(): ComponentRef { + const rootComponents = this.applicationRef['_rootComponents']; // Angular2 + // const rootComponents = this.applicationRef['components']; // Angular5 + if (rootComponents.length) { + return rootComponents[0]; + } + throw new Error('View Container not found! ngUpgrade needs to manually set this via setRootViewContainer.'); + } + + /** + * Gets the html element for a component ref. + * + * @param {ComponentRef} componentRef + * @returns {HTMLElement} + * + * @memberOf InjectionService + */ + private getComponentRootNode(componentRef: ComponentRef): HTMLElement { + return (componentRef.hostView as EmbeddedViewRef).rootNodes[0] as HTMLElement; + } + + /** + * Gets the root component container html element. + * + * @returns {HTMLElement} + * + * @memberOf InjectionService + */ + private getRootViewContainerNode(): HTMLElement { + return this.getComponentRootNode(this.getRootViewContainer()); + } + + /** + * Projects the inputs onto the component + * + * @param {ComponentRef} component + * @param {*} options + * @returns {ComponentRef} + * + * @memberOf InjectionService + */ + private projectComponentInputs(component: ComponentRef, options: any): ComponentRef { + if (options) { + const props = Object.getOwnPropertyNames(options); + for (const prop of props) { + component.instance[prop] = options[prop]; + } + } + + return component; + } + + public createComponentDynamically(componentClass: Type, options: any = {}, location: Element = this.getRootViewContainerNode()): ComponentRef { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass); + const componentRef = componentFactory.create(this.injector); + const componentRootNode = this.getComponentRootNode(componentRef); + + // project the options passed to the component instance + this.projectComponentInputs(componentRef, options); + this.applicationRef.attachView(componentRef.hostView); + + componentRef.onDestroy(() => { + this.applicationRef.detachView(componentRef.hostView); + }); + + location.appendChild(componentRootNode); + return componentRef; + } + + /** + * Inserts a component into an existing viewContainer + * @param componentType - type of component to create + * @param options - Inputs to project on new component + * @param vcRef - viewContainerRef in which to insert the newly created component + */ + public insertComponentDynamically(componentType: Type, options: any = {}, vcRef: ViewContainerRef): ComponentRef { + const factory = this.componentFactoryResolver.resolveComponentFactory(componentType); + const dynamicComponent = factory.create(vcRef.parentInjector); + this.projectComponentInputs(dynamicComponent, options); + vcRef.insert(dynamicComponent.hostView); + return dynamicComponent; + } +} -- cgit 1.2.3-korg