diff options
Diffstat (limited to 'src/angular')
103 files changed, 3793 insertions, 0 deletions
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 ` +<div class="sdc-accordion" [ngClass]="customCSSClass"> + <div class="sdc-accordion-header" (click)="toggleAccordion()" [ngClass]="{'arrow-right': arrowDirection === accordionArrowDirection.right}"> + <div class="svg-icon-wrapper bottom" [ngClass]="{'down': open}"> + <svg class="svg-icon __chevronUp" version="1.1" id="chevron-up_icon" x="0px" y="0px" viewBox="0 0 10 6.3" style="enable-background:new 0 0 10 6.3;" + xml:space="preserve"> + <g transform="translate(0,-952.36218)"> + <path d="M10,958.2c0-0.1,0-0.2-0.1-0.2l-4.6-5.5c-0.1-0.2-0.4-0.2-0.5,0l0,0L0.1,958c-0.1,0.2-0.1,0.4,0,0.5s0.4,0.1,0.5,0l0,0 + l4.3-5.2l4.3,5.2c0.1,0.2,0.4,0.2,0.5,0.1C10,958.5,10,958.3,10,958.2z"> + </path> + </g> + </svg> + </div> + <div class="title"> + {{title}} + </div> + </div> + <div class="sdc-accordion-body" [ngClass]="{open: open}"> + <ng-content></ng-content> + </div> +</div>`; 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<boolean>(); + + 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 ` +<div class="sdc-autocomplete-container" [ngClass]="{'results-shown': autoCompleteResults.length}"> + <sdc-filter-bar + [placeholder]="placeholder" + [label]="label" + [searchQuery]="searchQuery" + (searchQueryChange)="onSearchQueryChanged($event)"> + </sdc-filter-bar> + <ul class="autocomplete-results" [@displayResultsAnimation]="autoCompleteResults.length ?'true':'false'"> + <li *ngFor="let item of autoCompleteResults" + (click)="onItemSelected(item)">{{item.value}}</li> + </ul> +</div> +`; 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<any> = new EventEmitter<any>(); + + 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 ` +<button class="sdc-button sdc-button__{{ type }} btn-{{ size }} {{ iconPositionClass }}" + [disabled] = "disabled || show_spinner" + [attr.data-tests-id]="testId"> + <svg-icon + *ngIf="icon_name" + [name]="icon_name" + [mode]="iconMode" + [size]="'medium'" + > + </svg-icon> + {{ text }} +</button> +<svg-icon *ngIf="show_spinner" name="spinner" [size]="'medium'" class="sdc-button__spinner" [ngClass]="{left: spinner_position === placement.right}"></svg-icon> +`; 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 ` +<div *ngFor="let checkbox of checklistModel.checkboxes" #currentCheckbox> + <div class="checkbox-item"> + <sdc-checkbox [label]="checkbox.label" + [(checked)]="checkbox.isChecked" + [disabled]="checkbox.disabled" + (checkedChange)="checkboxCheckedChange(checkbox, checklistModel)" + [ngClass]="{'semi-checked': !checkbox.isChecked && hasCheckedChild(currentCheckbox)}"></sdc-checkbox> + </div> + <div *ngIf="checkbox.subLevelChecklist" class="checkbox-sublist"> + <sdc-checklist [checklistModel]="checkbox.subLevelChecklist" + (checkedChange)="childCheckboxChange($event, checkbox)"></sdc-checklist> + </div> +</div> +`; 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<ChecklistItemModel> = new EventEmitter<ChecklistItemModel>(); + + 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 ` +<div class="search-bar-container"> + <sdc-input class="search-bar-input" + [label]="label" + [placeHolder]="placeholder" + [debounceTime]="debounceTime" + [(value)]="searchQuery" + (valueChange)="searchTextChange($event)"></sdc-input> + <span class="clear-search-x filter-button" *ngIf="searchQuery" (click)="clearSearchQuery()"> + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"> + <defs> + <path id="close-a" d="M13.5996,12 L19.6576,5.942 C20.1146,5.485 20.1146,4.8 19.6576,4.343 C19.2006,3.886 18.5146,3.886 18.0576,4.343 L11.9996,10.4 L5.9426,4.343 C5.4856,3.886 4.7996,3.886 4.3426,4.343 C3.8856,4.8 3.8856,5.485 4.3426,5.942 L10.4006,12 L4.3426,18.058 C3.8856,18.515 3.8856,19.2 4.3426,19.657 C4.5716,19.886 4.7996,20 5.1426,20 C5.4856,20 5.7136,19.886 5.9426,19.657 L11.9996,13.6 L18.0576,19.657 C18.2866,19.886 18.6286,20 18.8576,20 C19.0856,20 19.4286,19.886 19.6576,19.657 C20.1146,19.2 20.1146,18.515 19.6576,18.058 L13.5996,12 Z"/> + </defs> + <g fill="none" fill-rule="evenodd"> + <use fill="#000" xlink:href="#close-a"/> + </g> + </svg> + </span> + <span class="magnify-button filter-button" *ngIf="!searchQuery"> + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"> + <defs> + <path id="search-a" d="M2,8.5 C2,4.9 4.9,2 8.5,2 C12.1,2 15,4.9 15,8.5 C15,10.3 14.3,11.9 13.1,13.1 C11.9,14.3 10.3,15 8.5,15 C4.9,15 2,12.1 2,8.5 M19.7,18.3 L15.2,13.8 C16.3,12.4 17,10.5 17,8.5 C17,3.8 13.2,0 8.5,0 C3.8,0 0,3.8 0,8.5 C0,13.2 3.8,17 8.5,17 C10.5,17 12.3,16.3 13.8,15.2 L18.3,19.7 C18.5,19.9 18.8,20 19,20 C19.2,20 19.5,19.9 19.7,19.7 C20.1,19.3 20.1,18.7 19.7,18.3"/> + </defs> + <g fill="none" fill-rule="evenodd" transform="translate(2 2)"> + <use fill="#000" xlink:href="#search-a"/> + </g> + </svg> + </span> +</div> +`; 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<any> = new EventEmitter<any>(); + + 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 ` +<div class="sdc-checkbox"> + <label SdcRippleClickAnimation [rippleClickDisabled]="disabled"> + <input type="checkbox" class="sdc-checkbox__input" [ngModel]="checked" (ngModelChange)="toggleState($event)" [disabled]="disabled"> + <span class="sdc-checkbox__label">{{ label }}</span> + </label> +</div> +`; 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<any> = new EventEmitter<any>(); + + 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 ` +<div class="sdc-dropdown" #dropDownWrapper + [ngClass]="{ + 'headless': type === cIDropDownTypes.Headless, + 'sdc-dropdown__error': !valid, + 'open-bottom': show && bottomVisible, + 'open-top':show && !bottomVisible}"> + <label *ngIf="label" class="sdc-dropdown__label" [ngClass]="{'required':required}">{{label}}</label> + <div class="sdc-dropdown__component-container"> + + <!--[DROP-DOWN AUTO HEADER START]--> + <div *ngIf="type===cIDropDownTypes.Auto" class="sdc-dropdown-auto__wrapper"> + <input class="sdc-dropdown__header js-sdc-dropdown--toggle-hook" + [(ngModel)]="this.filterValue" + (ngModelChange)="filterOptions(this.filterValue)" + placeholder="{{this.selectedOption?.label || this.selectedOption?.value || placeHolder}}"> + <svg-icon name="caret1-down-o" mode="secondary" size="small" (click)="toggleDropdown($event)"></svg-icon> + </div> + <!--[DROP-DOWN AUTO HEADER END]--> + + <!--[DROP-DOWN REGULAR HEADER START]--> + <button *ngIf="type===cIDropDownTypes.Regular" + class="sdc-dropdown__header js-sdc-dropdown--toggle-hook" + (click)="toggleDropdown($event)" + [ngClass]="{'disabled': disabled, 'placeholder':!this.selectedOption}"> + {{ this.selectedOption?.label || this.selectedOption?.value || placeHolder}} + <svg-icon name="caret1-down-o" mode="secondary" size="small"></svg-icon> + </button> + <!--[DROP-DOWN HEADER END]--> + + <!--[DROP-DOWN OPTIONS START]--> + <div class="sdc-dropdown__options-wrapper--frame" [ngClass]="{ + 'sdc-dropdown__options-wrapper--top':!bottomVisible, + 'sdc-dropdown__options-wrapper--uncollapsed':show + }"> + <ul #optionsContainerElement *ngIf="options" class="sdc-dropdown__options-list" [ngClass]="{ + 'sdc-dropdown__options-list--headless': headless, + 'sdc-dropdown__options-list--animation-init':animation_init + }"> + <ng-container *ngFor="let option of options; let i = index"> + <!--[Drop down item list or string list start]--> + <li *ngIf="option" class="sdc-dropdown__option" + [ngClass]="{ + 'selected': option == selectedOption, + 'sdc-dropdown__option--group':isGroupDesign, + 'sdc-dropdown__option--header': option.type && option.type === cIDropDownOptionType.Header, + 'sdc-dropdown__option--disabled': option.type && option.type === cIDropDownOptionType.Disable, + 'sdc-dropdown__option--hr': option.type && option.type === cIDropDownOptionType.HorizontalLine + }" + (click)="selectOption(option.value, $event)">{{option.label || String(option.value)}}</li> + <!--[Drop down item list or string list end]--> + </ng-container> + </ul> + </div> + <!--[DROP-DOWN OPTIONS END]--> + + </div> +</div> +`; 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<DropDownComponent>; + 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<IDropDownOption> = new EventEmitter<IDropDownOption>(); + @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 ` +<div class="sdc-input "> + <label class="sdc-input__label" *ngIf="label" [ngClass]="{'required':required}">{{label}}</label> + <input + class="sdc-input__input {{classNames}}" + [ngClass]="{'error': !valid, 'disabled':disabled}" + [attr.name]="name ? name : null" + [placeholder]="placeHolder" + [(ngModel)]="value" + [maxlength]="maxLength" + [minlength]="minLength" + [type]="type" + [formControl]="control" + [attr.disabled]="disabled ? 'disabled' : null" + (input)="onKeyPress($event.target.value)" + [attr.data-tests-id]="testId" + /> +</div> +`; 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<any> = new EventEmitter<any>(); + @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 ` +<label class='sdc-radio-group__legend'>{{legend}}</label> +<div class='sdc-radio-group__radios {{direction}}'> + <template *ngFor="let item of options.items"> + <div class="sdc-radio"> + <label class="sdc-radio__animation-wrapper" SdcRippleClickAnimation [rippleClickDisabled]="disabled"> + <input class="sdc-radio__input" + type="radio" + name="{{item.name}}" + value="{{item.value}}" + disabled="{{disabled || item.disabled || false}}" + (change)="onValueChanged(item.value)" + [(ngModel)]="value" + /> + <span class="sdc-radio__label">{{ item.label }}</span> + </label> + </div> + </template> +</div> +`; 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 = [ <IRadioButtonModel> { + value: 'val1', + name: 'exp6', + label: 'Label of Radio1' + }, <IRadioButtonModel> { + 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<string> = new EventEmitter<string>(); + + @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<string>; + + 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 ` +<ng-content></ng-content> +`; 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<ValidationComponent>; + + private supportedValidator: Array<QueryList<ValidatorComponent>>; + + 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 ` +<ng-content *ngIf="!disabled"></ng-content> +`; 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<boolean> = new EventEmitter<boolean>(); + @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<ValidatorComponent>; + @ContentChildren(RequiredValidatorComponent) public requireValidator: QueryList<ValidatorComponent>; + @ContentChildren(CustomValidatorComponent) public customValidator: QueryList<ValidatorComponent>; + + private supportedValidator: Array<QueryList<ValidatorComponent>>; + + 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 ` +<svg-icon-label + *ngIf="!isValid" + name="alert-triangle" + mode="error" + size="small" + [label]="message" + labelPlacement="right"> +</svg-icon-label> +`; 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<void>; + + private scrollWasHit: boolean = false; + + constructor(private elemRef: ElementRef) { + this.infiniteScroll = new EventEmitter<void>(); + } + + @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: ` + <div class="sdc-modal__close-button" + SdcRippleClickAnimation + [ngClass]="disabled ? 'disabled' : ''" + [rippleOnAction]="!disabled && rippleAnimationAction" + [attr.data-tests-id]="testId" + (click)="!disabled && closeModal()" + > + <svg-icon name="close" [mode]="disabled? 'secondary' : 'info'" size="small"></svg-icon> + </div> + ` +}) +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 ` +<div class="sdc-modal {{size}}"> + <div class="sdc-modal__wrapper sdc-modal-type-{{type}}" [@toggleModal]="modalVisible" (@toggleModal.done)="modalToggled($event)"> + + <div class="sdc-modal__header sdc-{{type}}__header"> + <div class="sdc-modal__icon" *ngIf="type != 'custom'"> + <div *ngIf="type == 'alert'"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30" height="30" viewBox="0 0 24 24"><defs><path fill="#000" id="alert-a" d="M20.5815,18.7997 C20.3815,18.9997 20.0815,19.0997 19.8815,19.0997 L2.8815,19.0997 C2.6815,19.0997 2.5815,19.0997 2.3815,18.9997 C1.8815,18.6997 1.7815,18.0997 1.9815,17.5997 L10.4815,3.4997 C10.5815,3.4007 10.6815,3.1997 10.7815,3.1997 C11.2815,2.9007 11.8815,3.0997 12.1815,3.4997 L20.6825,17.5997 C20.7815,17.6997 20.7815,17.9007 20.7815,18.0997 C20.8815,18.4007 20.6825,18.5997 20.5815,18.7997 M22.3815,16.5997 L13.9815,2.4007 C13.5815,1.6997 12.8815,1.1997 12.0815,0.9997 C11.2815,0.7997 10.4815,0.9007 9.7815,1.2997 C9.3815,1.4997 8.9815,1.9007 8.7815,2.2997 L0.3815,16.5997 C-0.4185,17.9997 0.0815,19.9007 1.4815,20.6997 C1.8815,20.9997 2.3815,21.0997 2.8815,21.0997 L19.8815,21.0997 C20.6825,21.0997 21.4815,20.7997 21.9815,20.1997 C22.5815,19.5997 22.8815,18.9007 22.8815,18.0997 C22.7815,17.5997 22.6825,16.9997 22.3815,16.5997 M11,7 C10.4,7 10,7.4 10,8 L10,12 C10,12.601 10.4,13 11,13 C11.6,13 12,12.601 12,12 L12,8 C12,7.4 11.6,7 11,7 M10.3,15.3 C10.1,15.499 10,15.699 10,15.999 C10,16.3 10.1,16.499 10.3,16.699 C10.5,16.9 10.7,16.999 11,16.999 C11.3,16.999 11.5,16.9 11.7,16.699 C11.9,16.499 12,16.199 12,15.999 C12,15.8 11.9,15.499 11.7,15.3 C11.3,14.9 10.7,14.9 10.3,15.3"/></defs> + <g fill="none" fill-rule="evenodd" transform="translate(1 1)"><use class="sdc-modal__svg-use" xlink:href="#alert-a"/></g></svg></div> + <div *ngIf="type == 'info'"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30" height="30" viewBox="0 0 24 24"><defs><path fill="#000" id="info-a" d="M11,20 C6,20 2,16 2,11 C2,6 6,2 11,2 C16,2 20,6 20,11 C20,16 16,20 11,20 M11,0 C4.9,0 0,4.9 0,11 C0,17.101 4.9,22 11,22 C17.1,22 22,17.101 22,11 C22,4.9 17.1,0 11,0 M11,10 C10.4,10 10,10.4 10,11 L10,15 C10,15.601 10.4,16 11,16 C11.6,16 12,15.601 12,15 L12,11 C12,10.4 11.6,10 11,10 M10.2998,6.2998 C10.0998,6.4998 9.9998,6.6998 9.9998,6.9998 C9.9998,7.2998 10.0998,7.4998 10.2998,7.6998 C10.4998,7.9008 10.6998,7.9998 10.9998,7.9998 C11.2998,7.9998 11.4998,7.9008 11.6998,7.6998 C11.9008,7.4998 11.9998,7.2998 11.9998,6.9998 C11.9998,6.6998 11.9008,6.4998 11.6998,6.2998 C11.2998,5.9008 10.6998,5.9008 10.2998,6.2998"/></defs> + <g fill="none" fill-rule="evenodd" transform="translate(1 1)"><use class="sdc-modal__svg-use" xlink:href="#info-a"/></g></svg></div> + <div *ngIf="type == 'error'"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30" height="30" viewBox="0 0 24 24"><defs><path fill="#000" id="x-a" d="M11,20 C6,20 2,16 2,11 C2,6 6,2 11,2 C16,2 20,6 20,11 C20,16 16,20 11,20 M11,0 C4.9,0 0,4.9 0,11 C0,17.1 4.9,22 11,22 C17.1,22 22,17.1 22,11 C22,4.9 17.1,0 11,0 M14.2591,7.29935 C13.8591,6.90035 13.2591,6.90035 12.8591,7.29935 L10.5591,9.59935 L8.2591,7.29935 C7.8591,6.90035 7.2591,6.90035 6.8591,7.29935 C6.4591,7.69935 6.4591,8.29935 6.8591,8.69935 L9.1581,10.99935 L6.8591,13.29935 C6.4591,13.69935 6.4591,14.29935 6.8591,14.69935 C7.0591,14.90035 7.2591,14.99935 7.5591,14.99935 C7.8591,14.99935 8.0591,14.90035 8.2591,14.69935 L10.5591,12.40035 L12.8591,14.69935 C13.0591,14.90035 13.3591,14.99935 13.5591,14.99935 C13.7591,14.99935 14.0591,14.90035 14.2591,14.69935 C14.6581,14.29935 14.6581,13.69935 14.2591,13.29935 L11.9591,10.99935 L14.2591,8.69935 C14.6581,8.29935 14.6581,7.69935 14.2591,7.29935"/></defs> + <g fill="none" fill-rule="evenodd" transform="translate(1 1)"><use class="sdc-modal__svg-use" xlink:href="#x-a"/></g></svg></div> + </div> + <div *ngIf="title" class="title" >{{ title }}</div> + <sdc-modal-close-button #modalCloseButton [testId]="getCalculatedTestId('close')"></sdc-modal-close-button> + </div> + <div class="sdc-modal__content" > + <div *ngIf="message">{{message}}</div> + <div #dynamicContentContainer></div> + </div> + <div class="sdc-modal__footer"> + <sdc-modal-button *ngFor="let button of buttons" + [text]="button.text" + [type]="button.type || 'primary'" + [disabled]="button.disabled" + [size] = "button.size ? button.size : 'default'" + [closeModal]="button.closeModal" + [spinner_position]="button.spinner_position" + [show_spinner]="button.show_spinner" + [callback]="button.callback" + [testId]="getCalculatedTestId('button-' + button.text)" + > + </sdc-modal-button> + </div> + </div> +</div> +<div class="modal-background" [@toggleBackground]="modalVisible" ></div> +`; 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 = <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: `<div></div>` +}) + + + +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<any> = new EventEmitter<any>(); + + @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<ModalComponent>; + + 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<any>; + + 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<ModalComponent> = this.openModal(modalConfig); + this.currentModal = modalInstance; + return modalInstance; + } + + public openActionModal = (title: string, message: string, actionButtonText?: string, actionButtonCallback?: Function, testId?: string): ComponentRef<ModalComponent> => { + 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<ModalComponent> = this.openModal(modalConfig); + this.currentModal = modalInstance; + return modalInstance; + } + + public openErrorModal = (errorMessage?: string, testId?: string): ComponentRef<ModalComponent> => { + 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<ModalComponent> = this.openModal(modalConfig); + this.currentModal = modalInstance; + return modalInstance; + } + + public openCustomModal = (modalConfig: IModalConfig, dynamicComponentType: Type<any>, dynamicComponentInput?: any) => { + const modalInstance: ComponentRef<ModalComponent> = this.openModal(modalConfig); + this.createInnnerComponent(dynamicComponentType, dynamicComponentInput); + return modalInstance; + } + + public createInnnerComponent = (dynamicComponentType: Type<any>, dynamicComponentInput?: any): void => { + this.currentModal.instance.innerModalContent = this.createDynamicComponentService.insertComponentDynamically(dynamicComponentType, dynamicComponentInput, this.currentModal.instance.dynamicContentContainer); + } + + public openModal = (customModalData: IModalConfig): ComponentRef<ModalComponent> => { + const modalInstance: ComponentRef<ModalComponent> = 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<IModalButtonComponent> => { + const buttons: Array<IModalButtonComponent> = []; + 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 ` +<div id="containerid" class="sdc-notification-container ntns"> + <sdc-notification *ngFor="let notif of notifications" [notificationSetting]="notif" (destroyComponent)="onDestroyed(notif)" > + </sdc-notification> +</div> +`; 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: ` + <div> + <h4>Custom Notification</h4> + <div> + <span>{{notifyTitle}}</span> + </div> + <div> + <span>{{notifyText}}</span> + </div> + </div> +` +}) +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 ` +<div class="sdc-notification" (click)="fadeOut()"> + <div class="sdc-notification__wrapper {{'type-' + notificationSetting.type}}" [class.fade-out__animated]="fade" (animationend)="destroyMe()"> + <div *ngIf="!notificationSetting.hasCustomContent" class="sdc-notification__content"> + <div class="sdc-notification__icon" > + </div> + <div class="sdc-notification__message"> + <div class="sdc-notification__title"> + {{notificationSetting.notifyTitle}} + </div> + <div class="sdc-notification__text" > + {{notificationSetting.notifyText}} + </div> + </div> + </div> + <div #dynamicContentContainer></div> + </div> +</div> +`; 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<any>(); + @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<any> = new Subject<any>(); + + 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<any>; + public innerComponentOptions : any; + + constructor(type: NotificationType, notifyText: string, notifyTitle: string, duration: number = 10000, sticky: boolean = false, hasCustomContent:boolean = false, innerComponentType?:Type<any>, 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<PopupMenuListComponent>; + 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: + `<li [ngClass]="[className || '', type || '', type == 'separator'? '': 'sdc-menu-item']" (click)="performAction($event)"> + <ng-content *ngIf="type != 'separator'"></ng-content> +</li>` +}) +export class PopupMenuItemComponent { + @Input() public className: string; + @Input() public type: undefined|'disabled'|'selected'|'separator'; + @Output() public action: EventEmitter<any> = new EventEmitter<any>(); + + 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: + `<ul + class="sdc-menu-list" + *ngIf="open" + [ngClass]="[className || '', relative? 'relative': '']" + [ngStyle]="{'left': position.x + 'px', 'top': position.y + 'px'}" + (click)="$event.stopPropagation()"> + <ng-content></ng-content> + </ul>` +}) +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<boolean> = new EventEmitter<boolean>(); + @Output() public positionChange: EventEmitter<IPoint> = new EventEmitter<IPoint>(); + + @ContentChildren(PopupMenuItemComponent) private menuItems: QueryList<PopupMenuItemComponent>; + + 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<PopupMenuItemComponent>) { + 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 ` +<div class="search-bar-container" [ngClass]="{'not-empty': searchQuery}"> + <sdc-input class="sdc-input-wrapper" + [label]="label" + [placeHolder]="placeholder" + [debounceTime]="debounceTime" + [(value)]="searchQuery"></sdc-input> + <span class="magnify-button search-button" (click)="searchButtonClick()"> + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"> + <defs> + <path id="search-a" d="M2,8.5 C2,4.9 4.9,2 8.5,2 C12.1,2 15,4.9 15,8.5 C15,10.3 14.3,11.9 13.1,13.1 C11.9,14.3 10.3,15 8.5,15 C4.9,15 2,12.1 2,8.5 M19.7,18.3 L15.2,13.8 C16.3,12.4 17,10.5 17,8.5 C17,3.8 13.2,0 8.5,0 C3.8,0 0,3.8 0,8.5 C0,13.2 3.8,17 8.5,17 C10.5,17 12.3,16.3 13.8,15.2 L18.3,19.7 C18.5,19.9 18.8,20 19,20 C19.2,20 19.5,19.9 19.7,19.7 C20.1,19.3 20.1,18.7 19.7,18.3"/> + </defs> + <g fill="none" fill-rule="evenodd" transform="translate(2 2)"> + <use fill="#000" xlink:href="#search-a"/> + </g> + </svg> + </span> +</div> +`; 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<string> = new EventEmitter<string>(); + + 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 ` +<div class="svg-icon-wrapper" [ngClass]="[(mode) ? 'mode-'+mode : '', (size) ? 'size-'+size : '', (labelPlacement) ? 'label-placement-'+labelPlacement : '', (clickable) ? 'clickable' : '', className || '']" [attr.disabled]="disabled || undefined"> + <svg-icon [name]="name" className="svg-icon"></svg-icon> + <span class="svg-icon-label" [ngClass]="[labelClassName || '']">{{ label }}</span> +</div> +`; 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 ` +<div [ngClass]="classes" [attr.disabled]="disabled || undefined" [innerHtml]="svgIconContentSafeHtml"></div> +`; 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 ` +<div [hidden]="!active" class="sdc-tab-content" role="tabpanel"> + <ng-content></ng-content> +</div> +`; 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 ` +<ul class="sdc-tabs-list" role="tablist"> + <li *ngFor="let tab of tabs" class="sdc-tab" role="tab" (click)="selectTab(tab)" [class.sdc-tab-active]="tab.active"> + <span *ngIf="tab.title">{{tab.title}}</span> + <svg-icon-label + *ngIf="tab.titleIcon" + [name]="tab.titleIcon" + [mode]="tab.titleIconMode" + [size]="_size"> + </svg-icon-label> + </li> +</ul> +<ng-content></ng-content> +`; 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<TabComponent>; + + 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 ` +<div class="sdc-tag-cloud-new-item-field" [ngClass]="{'not-empty': newTagItem}"> + <sdc-input [label]="label" + [disabled]="(isViewOnly===true)" + [placeHolder]="placeholder" + [(value)]="newTagItem" + (keyup)="onKeyup($event)" + [ngClass]="{'error': uniqueError}"></sdc-input> + <div class="add-button" (click)="newTagItem && insertItemToList()" [ngClass]="{'disabled': !newTagItem || uniqueError}"> + <span class="plus-icon"> + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"> + <defs> + <path id="add-a" d="M15,7 L9,7 L9,1 C9,0.4 8.6,0 8,0 C7.4,0 7,0.4 7,1 L7,7 L1,7 C0.4,7 0,7.4 0,8 C0,8.6 0.4,9 1,9 L7,9 L7,15 C7,15.6 7.4,16 8,16 C8.6,16 9,15.6 9,15 L9,9 L15,9 C15.6,9 16,8.6 16,8 C16,7.4 15.6,7 15,7"/> + </defs> + <g fill="none" fill-rule="evenodd" transform="translate(4 4)"> + <use xlink:href="#add-a"/> + </g> + </svg> + </span> + </div> +</div> +<div class="sdc-list-container"> + <sdc-tag-item *ngFor="let item of list; let i = index;" + [text]="item" + [index]="i" + [isViewOnly]="isViewOnly && (isViewOnly === true || isViewOnly.indexOf(i) > -1)" + (clickOnDelete)="deleteItemFromList($event)"></sdc-tag-item> +</div> +<div class="error-message" *ngIf="uniqueError">{{uniqueErrorMessage}}</div> +`; 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<string[]> = new EventEmitter<string[]>(); + 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 ` +<div class="tag-item" [ngClass]="{'view-only':isViewOnly}"> + <span>{{text}}</span> + <span class="delete-item" *ngIf="!isViewOnly" (click)="clickOnDelete.emit(index)"> + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"> + <defs> + <path id="close-a" d="M13.5996,12 L19.6576,5.942 C20.1146,5.485 20.1146,4.8 19.6576,4.343 C19.2006,3.886 18.5146,3.886 18.0576,4.343 L11.9996,10.4 L5.9426,4.343 C5.4856,3.886 4.7996,3.886 4.3426,4.343 C3.8856,4.8 3.8856,5.485 4.3426,5.942 L10.4006,12 L4.3426,18.058 C3.8856,18.515 3.8856,19.2 4.3426,19.657 C4.5716,19.886 4.7996,20 5.1426,20 C5.4856,20 5.7136,19.886 5.9426,19.657 L11.9996,13.6 L18.0576,19.657 C18.2866,19.886 18.6286,20 18.8576,20 C19.0856,20 19.4286,19.886 19.6576,19.657 C20.1146,19.2 20.1146,18.515 19.6576,18.058 L13.5996,12 Z"/> + </defs> + <g fill="none" fill-rule="evenodd"> + <use xlink:href="#close-a"/> + </g> + </svg> + </span> +</div> +`; + 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<number> = new EventEmitter<number>(); +} 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: '<ng-content></ng-content>' +}) + +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: '<ng-content></ng-content>' +}) + +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: '<ng-content></ng-content>' +}) + +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 ` +<ng-content select="sdc-tile-header"></ng-content> +<ng-content select="sdc-tile-content"></ng-content> +<ng-content select="sdc-tile-footer"></ng-content> +`; 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: ` + <div class="sdc-tooltip-template-container"> + <ng-container #templateContainer></ng-container> + </div>` +}) + +export class TooltipTemplateComponent implements AfterViewInit { + @ViewChild('templateContainer', {read: ViewContainerRef}) public container: ViewContainerRef; + + public viewReady: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(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<any>; + @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<any>} + * + * @memberOf InjectionService + */ + private getRootViewContainer(): ComponentRef<any> { + 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<any>} componentRef + * @returns {HTMLElement} + * + * @memberOf InjectionService + */ + private getComponentRootNode(componentRef: ComponentRef<any>): HTMLElement { + return (componentRef.hostView as EmbeddedViewRef<any>).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<any>} component + * @param {*} options + * @returns {ComponentRef<any>} + * + * @memberOf InjectionService + */ + private projectComponentInputs(component: ComponentRef<any>, options: any): ComponentRef<any> { + if (options) { + const props = Object.getOwnPropertyNames(options); + for (const prop of props) { + component.instance[prop] = options[prop]; + } + } + + return component; + } + + public createComponentDynamically<T>(componentClass: Type<T>, options: any = {}, location: Element = this.getRootViewContainerNode()): ComponentRef<any> { + 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<T>(componentType: Type<T>, options: any = {}, vcRef: ViewContainerRef): ComponentRef<any> { + const factory = this.componentFactoryResolver.resolveComponentFactory(componentType); + const dynamicComponent = factory.create(vcRef.parentInjector); + this.projectComponentInputs(dynamicComponent, options); + vcRef.insert(dynamicComponent.hostView); + return dynamicComponent; + } +} |