diff options
Diffstat (limited to 'src')
147 files changed, 6687 insertions, 0 deletions
diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..83d47bf --- /dev/null +++ b/src/README.md @@ -0,0 +1,9 @@ + +# Folder Structure + +### Angular 2 Framework +Contains **Angular 2** based components + + +### React Framework +Contains **React.js** based components 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; + } +} diff --git a/src/react/Accordion.js b/src/react/Accordion.js new file mode 100644 index 0000000..3acdd24 --- /dev/null +++ b/src/react/Accordion.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import SVGIcon from './SVGIcon.js'; + +class Accordion extends React.Component { + constructor(props) { + super(props); + this.state = { + open: props.defaultExpanded + }; + } + render() { + const { children, title, className, dataTestId } = this.props; + const { open } = this.state; + return ( + <div className={`sdc-accordion ${className}`}> + <div data-test-id={dataTestId} onClick={() => this.setState({ open: !open })} className='sdc-accordion-header'> + <SVGIcon name='chevronDown' iconClassName={open ? 'down' : ''} /> + <div className='title'>{title}</div> + </div> + <div className={`sdc-accordion-body ${open ? 'open' : ''}`}>{children}</div> + </div> + ); + } +} + +Accordion.propTypes = { + title: PropTypes.string, + children: PropTypes.node, + expandByDefault: PropTypes.bool, + dataTestId: PropTypes.string +}; + +Accordion.defaultProps = { + title: '', + className: '', + defaultExpanded: false +}; + +export default Accordion; diff --git a/src/react/Button.js b/src/react/Button.js new file mode 100644 index 0000000..c628455 --- /dev/null +++ b/src/react/Button.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import SVGIcon from './SVGIcon.js'; + +const Button = ({btnType, size, className, iconName, onClick, disabled, children, ...other}) => ( + <button + onClick={onClick} + className={`sdc-button sdc-button__${btnType} ${size && `btn-${size}`} ${className} ${iconName}`} + disabled={disabled} + {...other}> + { + iconName ? + <SVGIcon name={iconName} label={children} labelPosition='right' /> + : + children + } + </button> +); + +Button.propTypes = { + btnType: PropTypes.string, + size: PropTypes.oneOf(['', 'default', 'x-small', 'small', 'medium', 'large']), + className: PropTypes.string, + iconName: PropTypes.string, + onClick: PropTypes.func, + disabled: PropTypes.bool +}; + +Button.defaultProps = { + btnType: 'primary', + size: '', + className: '', + iconName: '', + disabled: false +}; + +export default Button; diff --git a/src/react/Checkbox.js b/src/react/Checkbox.js new file mode 100644 index 0000000..bef6945 --- /dev/null +++ b/src/react/Checkbox.js @@ -0,0 +1,45 @@ +import React from 'react'; + +class Checkbox extends React.Component { + + render() { + let {checked = false, disabled, value, label, inputRef, className, name} = this.props; + let dataTestId = this.props['data-test-id']; + + return ( + <div className={`sdc-checkbox ${className || ''}`}> + <label> + <input + className='sdc-checkbox__input' + ref={inputRef} + data-test-id={dataTestId} + type='checkbox' + checked={checked} + name={name} + value={value} + onChange={(e) => this.onChange(e)} + disabled={disabled} /> + <span className='sdc-checkbox__label'>{label}</span> + </label> + </div> + ); + } + + onChange(e) { + let {onChange} = this.props; + if (onChange) { + onChange(e.target.checked); + } + } + + getChecked() { + return this.props.checked; + } + + getValue() { + return this.props.value; + } + +} + +export default Checkbox; diff --git a/src/react/Checklist.js b/src/react/Checklist.js new file mode 100644 index 0000000..1a42aee --- /dev/null +++ b/src/react/Checklist.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Checkbox from './Checkbox.js'; + +const Checklist = ({ items = [], className, onChange }) => ( + <div className={className}> + {items.map((item, index) => { + return ( + <div key={`checkbox-item-${index}`} className='checkbox-item'> + <Checkbox + key={`${item.label}${index}`} + label={item.label} + value={item.value} + checked={item.checked} + disabled={item.disabled} + onChange={value => { + let obj = {}; + obj[item.value] = value; + onChange(obj); + }} + data-test-id={item.dataTestId} + /> + </div> + ); + })} + </div> +); + +Checklist.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.string, + checked: PropTypes.bool, + disabled: PropTypes.bool, + dataTestId: PropTypes.string + }) + ), + className: PropTypes.string, + onChange: PropTypes.func +}; + +export default Checklist;
\ No newline at end of file diff --git a/src/react/Input.js b/src/react/Input.js new file mode 100644 index 0000000..5760637 --- /dev/null +++ b/src/react/Input.js @@ -0,0 +1,88 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import SVGIcon from './SVGIcon.js'; + +class Input extends React.Component { + + render() { + let {className, disabled, errorMessage, readOnly, label, name, value, type, placeholder, isRequired} = this.props; + let dataTestId = this.props['data-test-id']; + let inputClasses = `sdc-input__input ${errorMessage ? 'error' : ''} ${readOnly ? 'view-only' : ''}`; + let labelClasses = `sdc-input__label ${readOnly ? 'view-only' : ''} ${isRequired ? 'required' : ''}`; + + return ( + <div className={`sdc-input ${className || ''}`}> + + <label className={labelClasses} htmlFor={name}>{label}</label> + <input className={inputClasses} + disabled={disabled} + readOnly={readOnly} + type={type} + id={name} + name={name} + value={this.props.value} + placeholder={placeholder} + data-test-id={dataTestId} + onBlur={(e) => this.onBlur(e)} + onKeyDown={(e) => this.onKeyDown(e)} + onChange={(e) => this.onChange(e)}/> + { errorMessage && <div className="sdc-label__error"> + <SVGIcon + label={errorMessage} + labelPosition='right' + color='negative' + name='exclamationTriangleFull' /> + </div>} + </div> + ); + } + + onChange(e) { + let {onChange, readOnly, disabled} = this.props; + if (onChange && !readOnly && !disabled) { + onChange(e.target.value); + } + } + + onBlur(e) { + let {onBlur, readOnly} = this.props; + if (!readOnly && onBlur) { + onBlur(e); + } + } + + onKeyDown(e) { + let {onKeyDown, readOnly} = this.props; + if (!readOnly && onKeyDown) { + onKeyDown(e); + } + } + + getValue() { + return this.props.value; + } + +} +Input.propTypes = { + name: PropTypes.string, + value: PropTypes.string, + type: PropTypes.oneOf(['text', 'number']), + placeholder : PropTypes.string, + onChange: PropTypes.func, + onBlur: PropTypes.func, + onKeyDown: PropTypes.func, + errorMessage: PropTypes.string, + readOnly: PropTypes.bool, + isRequired: PropTypes.bool, + disabled: PropTypes.bool, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + className: PropTypes.string +}; + +Input.defaultProps = { + type: 'text', + readOnly: false, + isRequired: false, + disabled: false +}; +export default Input; diff --git a/src/react/Modal.js b/src/react/Modal.js new file mode 100644 index 0000000..ab2f7d7 --- /dev/null +++ b/src/react/Modal.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Portal from './Portal.js'; +import Body from './ModalBody.js'; +import Header from './ModalHeader.js'; +import Footer from './ModalFooter.js'; +import Title from './ModalTitle.js'; + +export const modalSize = { + medium: 'md', + large: 'l', + extraLarge: 'xl', + small: 'sm', + extraSmall: 'xsm' +}; + + +class Modal extends React.Component { + + render() { + const {size, type, children, show} = this.props; + return ( + <Portal> + <div ref={el => { this.modalRef = el;}}> + {show && <div className={`sdc-modal ${modalSize[size]}`}> + <div className={`sdc-modal__wrapper sdc-modal-type-${type}`}> + {children} + </div> + </div>} + {show && <div className='modal-background' />} + </div> + </Portal> + ); + } +} + +Modal.defaultProps = { + show: false, + size: 'medium', + type: 'info' +}; + +Modal.propTypes = { + show: PropTypes.bool, + size: PropTypes.string, + children: PropTypes.node, + type: PropTypes.string +}; + +Modal.Body = Body; +Modal.Header = Header; +Modal.Footer = Footer; +Modal.Title = Title; +export default Modal;
\ No newline at end of file diff --git a/src/react/ModalBody.js b/src/react/ModalBody.js new file mode 100644 index 0000000..4fae0f6 --- /dev/null +++ b/src/react/ModalBody.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ModalBody = ({children, className}) => ( + <div className={`sdc-modal__content ${className}`} > + {children} + </div> +); + +ModalBody.propTypes = { + children: PropTypes.node, + className: PropTypes.string +}; + +ModalBody.defaultProps = { + className: '' +}; + +export default ModalBody;
\ No newline at end of file diff --git a/src/react/ModalFooter.js b/src/react/ModalFooter.js new file mode 100644 index 0000000..607895d --- /dev/null +++ b/src/react/ModalFooter.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Button from './Button.js'; + +const Footer = ({onClose, closeButtonText, actionButtonText, actionButtonClick, withButtons, children}) => { + const closeBtnType = actionButtonClick ? 'secondary' : 'primary'; + return ( + <div className='sdc-modal__footer'> + {children} + { + withButtons && <div> + {actionButtonClick && + <Button onClick={actionButtonClick}>{actionButtonText}</Button> + } + <Button btnType={closeBtnType} onClick={onClose}>{closeButtonText}</Button> + </div> + } + </div> + ); +}; + +Footer.propTypes = { + onClose: PropTypes.func, + closeButtonText: PropTypes.string, + actionButtonText: PropTypes.string, + actionButtonClick: PropTypes.func, + withButtons: PropTypes.bool, + children: PropTypes.node +}; + +Footer.defaultProps = { + closeButtonText: 'Close', + withButtons: true +}; + +export default Footer;
\ No newline at end of file diff --git a/src/react/ModalHeader.js b/src/react/ModalHeader.js new file mode 100644 index 0000000..c6be5ef --- /dev/null +++ b/src/react/ModalHeader.js @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import SVGIcon from './SVGIcon.js'; + +const iconMaper = { + error: 'error', + info: 'errorCircle', + alert: 'exclamationTriangleLine' +}; + +const headerTypes = { + error: 'sdc-error__header', + info: 'sdc-info__header', + alert: 'sdc-alert__header', + custom: 'sdc-custom__header' +} + + + +const Header = ({children, onClose, type}) => ( + <div className={ headerTypes[type] + ' sdc-modal__header'} > + {type !== 'custom' + && + <SVGIcon iconClassName='sdc-modal__icon' className='sdc-modal__svg-use' name={iconMaper[type]}/> + + } + {children} + <SVGIcon iconClassName ='sdc-modal__close-button-svg' className='sdc-modal__close-button' onClick={onClose} name='close'/> + </div> +); + +Header.propTypes = { + children: PropTypes.node, + onClose: PropTypes.func +}; + +Header.defaultProps = { + type: 'info' +}; + +export default Header;
\ No newline at end of file diff --git a/src/react/ModalTitle.js b/src/react/ModalTitle.js new file mode 100644 index 0000000..b48cc8a --- /dev/null +++ b/src/react/ModalTitle.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Title = ({children, className}) => ( + <div className={`title ${className}`} > + {children} + </div> +); + +Title.PropTypes = { + children: PropTypes.node, + className: PropTypes.string +}; + +Title.defaultProps = { + className: '' +}; + +export default Title;
\ No newline at end of file diff --git a/src/react/Panel.js b/src/react/Panel.js new file mode 100644 index 0000000..34d2e62 --- /dev/null +++ b/src/react/Panel.js @@ -0,0 +1,18 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Panel = ({ className, children }) => ( + <div className={`sdc-panel ${className}`}> + {children} + </div> +); + +Panel.propTypes = { + className: PropTypes.string, + children: PropTypes.node +}; + +Panel.defaultProps = { + className: '' +}; +export default Panel;
\ No newline at end of file diff --git a/src/react/PopupMenu.js b/src/react/PopupMenu.js new file mode 100644 index 0000000..d2cd29a --- /dev/null +++ b/src/react/PopupMenu.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import PopupMenuItem from './PopupMenuItem'; + +class PopupMenu extends React.Component { + render() { + const {children = [], onMenuItemClick, position = {}, relative} = this.props; + const style = relative ? {left: position.x, top: position.y} : {}; + + return ( + <ul className={`sdc-menu-list ${relative ? 'relative' : ''}`} style={style}> + {React.Children.toArray(children).map((child, i) => React.cloneElement(child, + { + onClick: child.props.onClick || onMenuItemClick, + key: i + }))} + </ul> + ); + } +} + +PopupMenu.propTypes = { + relative: PropTypes.bool, + position: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number + }), + onMenuItemClick: PropTypes.func +}; + +PopupMenu.defaultProps = { + relative: false +}; + +export const PopupMenuSeparator = () => <li className='separator' />; + +PopupMenu.Separator = PopupMenuSeparator; +PopupMenu.Item = PopupMenuItem; +export default PopupMenu; diff --git a/src/react/PopupMenuItem.js b/src/react/PopupMenuItem.js new file mode 100644 index 0000000..98e3f49 --- /dev/null +++ b/src/react/PopupMenuItem.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class PopupMenuItem extends React.Component { + render() { + const {itemId, value, onClick, selected, disabled} = this.props; + const additionalClasses = selected ? 'selected' : disabled ? 'disabled' : ''; + return ( + <li + className={`sdc-menu-item ${additionalClasses}`} + onClick={event => { + event.stopPropagation(); + onClick && !disabled && onClick(itemId); + }}> + {value} + </li> + ); + } +} + +PopupMenuItem.propTypes = { + itemId: PropTypes.any, + value: PropTypes.any, + selected: PropTypes.bool, + onClick: PropTypes.func, + disabled: PropTypes.bool +}; + +PopupMenuItem.defaultProps = { + selected: false, + disabled: false +}; + +export default PopupMenuItem; diff --git a/src/react/Portal.js b/src/react/Portal.js new file mode 100644 index 0000000..90e0675 --- /dev/null +++ b/src/react/Portal.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; + +class Portal extends React.Component { + componentDidMount() { + this.renderPortal(); + } + + componentDidUpdate() { + this.renderPortal(); + } + + componentWillUnmount() { + if (this.defaultNode) { + document.body.removeChild(this.defaultNode); + } + this.defaultNode = null; + this.portal = null; + } + + renderPortal() { + if (!this.defaultNode) { + this.defaultNode = document.createElement('div'); + this.defaultNode.className = 'onap-sdc-portal'; + document.body.appendChild(this.defaultNode); + } + + let children = this.props.children; + if (typeof this.props.children.type === 'function') { + children = React.cloneElement(this.props.children); + } + /** + * Change this to ReactDOM.CreatePortal after upgrading to React 16 + */ + this.portal = ReactDOM.unstable_renderSubtreeIntoContainer( + this, + children, + this.defaultNode + ); + } + render() { + return null; + } + +} + +Portal.propTypes = { + children: PropTypes.node.isRequired +}; + +export default Portal;
\ No newline at end of file diff --git a/src/react/Radio.js b/src/react/Radio.js new file mode 100644 index 0000000..483521a --- /dev/null +++ b/src/react/Radio.js @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class Radio extends React.Component { + render() { + let {checked, disabled, value, label, className, inputRef, name} = this.props; + let dataTestId = this.props['data-test-id']; + return ( + <div className={`sdc-radio ${className}`}> + <label> + <input + ref={inputRef} + className='sdc-radio__input' + value={value} + data-test-id={dataTestId} + type='radio' + name={name} + checked={checked} + onChange={(e) => this.onChange(e)} + disabled={disabled} /> + <span className='sdc-radio__label'>{label}</span> + </label> + </div> + ); + } + + onChange(e) { + let {onChange} = this.props; + if (onChange) { + onChange(e.target.checked); + } + } + + getChecked() { + return this.props.checked; + } + + getValue() { + return this.props.value; + } +} + +Radio.propTypes = { + checked: PropTypes.bool, + value: PropTypes.any, + label: PropTypes.string, + className: PropTypes.string, + inputRef: PropTypes.func, + name: PropTypes.string, + disabled: PropTypes.bool +}; + +Radio.defaultProps = { + checked: false, + className: '' +}; + +export default Radio; diff --git a/src/react/RadioGroup.js b/src/react/RadioGroup.js new file mode 100644 index 0000000..59eaca7 --- /dev/null +++ b/src/react/RadioGroup.js @@ -0,0 +1,40 @@ +import React from 'react'; +import Radio from './Radio.js'; + +class RadioGroup extends React.Component { + constructor(props) { + super(props); + this.radios = {}; + } + + render() { + let {name, disabled, title, options, value, className} = this.props; + let dataTestId = this.props['data-test-id']; + return (<div data-test-id={dataTestId} className={`sdc-radio-group ${className || ''}`}> + { title && <label className='sdc-radio-group__legend'>{title}</label> } + <div className='sdc-radio-group__radios'> + {options.map(option => { + let rName = name + '_' + option.value; + return (<Radio ref={(radio) => {this.radios[rName] = radio;}} data-test-id={dataTestId + '_' + option.value} + key={rName} value={option.value} + label={option.label} checked={value === option.value} disabled={disabled} + name={name} onChange={() => this.onChange(rName)} /> + );})} + </div> + </div>); + } + + onChange(rName) { + let {onChange} = this.props; + let val = this.radios[rName].getValue(); + if (onChange) { + onChange(val); + } + } + + getValue() { + return this.props.value; + } +} + +export default RadioGroup; diff --git a/src/react/SVGIcon.js b/src/react/SVGIcon.js new file mode 100644 index 0000000..8a5b1ae --- /dev/null +++ b/src/react/SVGIcon.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import iconMap from './utils/iconMap.js'; + +const SVGIcon = ({name, onClick, label, className, iconClassName, labelClassName, labelPosition, color, disabled, ...other}) => { + + let colorClass = (color !== '') ? '__' + color : ''; + let classes = `svg-icon-wrapper ${iconClassName} ${className} ${colorClass} ${onClick ? 'clickable' : ''} ${labelPosition}`; + let camelCasedName = name.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); + let IconComponent = iconMap[camelCasedName]; + if (!IconComponent) { + console.error('Icon by the name ' + camelCasedName + ' is missing.'); + } + + return ( + <div {...other} onClick={onClick} className={classes} disabled={disabled}> + { IconComponent && <IconComponent className={`svg-icon __${name}`} /> } + { !IconComponent && <span className='svg-icon-missing'>Missing Icon</span> } + {label && <span className={`svg-icon-label ${labelClassName}`}>{label}</span>} + </div> + ); + +}; + +SVGIcon.propTypes = { + name: PropTypes.string.isRequired, + onClick: PropTypes.func, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + labelPosition: PropTypes.string, + className: PropTypes.string, + iconClassName: PropTypes.string, + labelClassName: PropTypes.string, + color: PropTypes.string +}; + +SVGIcon.defaultProps = { + name: '', + label: '', + className: '', + iconClassName: '', + labelClassName: '', + labelPosition: 'bottom', + color: '' +}; + +export default SVGIcon; diff --git a/src/react/Tab.js b/src/react/Tab.js new file mode 100644 index 0000000..5aa0f16 --- /dev/null +++ b/src/react/Tab.js @@ -0,0 +1,20 @@ +import React from 'react'; + +class Tab extends React.Component { + render() { + const {activeTab, tabId, title, onClick, disabled, className = ''} = this.props; + const dataTestId = this.props['data-test-id']; + return ( + <li + className={`sdc-tab ${activeTab === tabId ? 'sdc-tab-active' : ''} ${className}`} + onClick={!disabled && onClick} + data-test-id={dataTestId} + role='tab' + disabled={disabled}> + {title} + </li> + ); + } +} + +export default Tab; diff --git a/src/react/TabPane.js b/src/react/TabPane.js new file mode 100644 index 0000000..56a4bf0 --- /dev/null +++ b/src/react/TabPane.js @@ -0,0 +1,12 @@ +import React from 'react'; + +class TabPane extends React.Component { + render() { + const {children} = this.props; + return (<div className='sdc-tab-content' role='tabpanel'> + {children} + </div>); + } +} + +export default TabPane; diff --git a/src/react/Tabs.js b/src/react/Tabs.js new file mode 100644 index 0000000..c502038 --- /dev/null +++ b/src/react/Tabs.js @@ -0,0 +1,29 @@ +import React from 'react'; +import TabPane from './TabPane.js'; + +class Tabs extends React.Component { + render() { + const {type, children = [], activeTab, onTabClick, className} = this.props; + return ( + <div className={type === 'header' ? `sdc-tabs sdc-tabs-header ${className || ''}` : `sdc-tabs sdc-tabs-menu ${className || ''}`} > + <ul className='sdc-tabs-list' role='tablist'> + {children.map(child => React.cloneElement(child, + { + key: child.props.tabId, + onClick: () => onTabClick(child.props.tabId), + activeTab + }))} + </ul> + <TabPane> + {children.map(child => { + if (child.props.tabId === activeTab) { + return child.props.children; + } + })} + </TabPane> + </div> + ); + } +} + +export default Tabs; diff --git a/src/react/Tile.js b/src/react/Tile.js new file mode 100644 index 0000000..f47f88d --- /dev/null +++ b/src/react/Tile.js @@ -0,0 +1,33 @@ +import React, {Children} from 'react'; +import PropTypes from 'prop-types'; +import TileInfo from './TileInfo.js'; +import TileFooter from './TileFooter.js'; +import SVGIcon from './SVGIcon.js'; + +const Tile = ({headerText, headerColor, iconName, iconColor, className, onClick, children, dataTestId}) => { + let childrenArr = Children.toArray(children); + return ( + <div className={`sdc-tile ${className || ''}`} onClick={onClick} data-test-id={dataTestId}> + <div className={`sdc-tile-header ${headerColor || ''}`}>{headerText}</div> + <div className='sdc-tile-content'> + <div className={`sdc-tile-content-icon ${iconColor || ''}`}> + {iconName && <SVGIcon name={iconName}/>} + </div> + {childrenArr.find(e => e.type === TileInfo)} + </div> + {childrenArr.find(e => e.type === TileFooter)} + </div> + ); +}; + +Tile.propTypes = { + headerText: PropTypes.string, + headerColor: PropTypes.string, + iconName: PropTypes.string, + iconColor: PropTypes.string, + className: PropTypes.string, + onClick: PropTypes.func, + dataTestId: PropTypes.string +}; + +export default Tile; diff --git a/src/react/TileFooter.js b/src/react/TileFooter.js new file mode 100644 index 0000000..3a56908 --- /dev/null +++ b/src/react/TileFooter.js @@ -0,0 +1,10 @@ +import React, {Children} from 'react'; +import TileFooterCell from './TileFooterCell.js'; + +const TileFooter = ({children, align}) => ( + <div className={`sdc-tile-footer ${align === 'center' ? 'centered' : ''}`}> + {Children.toArray(children).filter(e => e.type === TileFooterCell)} + </div> +); + +export default TileFooter; diff --git a/src/react/TileFooterCell.js b/src/react/TileFooterCell.js new file mode 100644 index 0000000..37e6416 --- /dev/null +++ b/src/react/TileFooterCell.js @@ -0,0 +1,7 @@ +import React from 'react'; + +const TileFooterCell = ({className, children, dataTestId}) => ( + <span className={`sdc-tile-footer-cell ${className || ''}`} data-test-id={dataTestId}>{children}</span> +); + +export default TileFooterCell; diff --git a/src/react/TileInfo.js b/src/react/TileInfo.js new file mode 100644 index 0000000..bda8e74 --- /dev/null +++ b/src/react/TileInfo.js @@ -0,0 +1,10 @@ +import React, {Children} from 'react'; +import TileInfoLine from './TileInfoLine.js'; + +const TileInfo = ({align, children}) => ( + <div className={`sdc-tile-content-info ${align === 'center' ? 'centered' : ''}`}> + {Children.toArray(children).filter(e => e.type === TileInfoLine)} + </div> +); + +export default TileInfo; diff --git a/src/react/TileInfoLine.js b/src/react/TileInfoLine.js new file mode 100644 index 0000000..5b0e2c9 --- /dev/null +++ b/src/react/TileInfoLine.js @@ -0,0 +1,7 @@ +import React from 'react'; + +const TileInfoLine = ({type, className, children, dataTestId}) => ( + <div className={`sdc-tile-info-line ${type || ''} ${className || ''}`} data-test-id={dataTestId}>{children}</div> +); + +export default TileInfoLine; diff --git a/src/react/index.js b/src/react/index.js new file mode 100644 index 0000000..cbe0161 --- /dev/null +++ b/src/react/index.js @@ -0,0 +1,74 @@ +import Accordion from './Accordion.js'; +import Button from './Button.js'; +import Checkbox from './Checkbox.js'; +import Checklist from './Checklist.js'; +import Input from './Input.js'; +import Modal from './Modal.js'; +import ModalBody from './ModalBody.js'; +import ModalFooter from './ModalFooter.js'; +import ModalHeader from './ModalHeader.js'; +import ModalTitle from './ModalTitle.js'; +import Panel from './Panel.js'; +import PopupMenu from './PopupMenu.js'; +import Portal from './Portal.js'; +import Radio from './Radio.js'; +import RadioGroup from './RadioGroup.js'; +import SVGIcon from './SVGIcon.js'; +import Tab from './Tab.js'; +import Tabs from './Tabs.js'; +import Tile from './Tile.js'; +import TileInfo from './TileInfo.js'; +import TileInfoLine from './TileInfoLine.js'; +import TileFooter from './TileFooter.js'; +import TileFooterCell from './TileFooterCell.js'; + + +export { Accordion }; +export { Button }; +export { Checkbox }; +export { Checklist }; +export { Input }; +export { Modal }; +export { ModalBody }; +export { ModalFooter }; +export { ModalHeader }; +export { ModalTitle }; +export { Panel }; +export { PopupMenu }; +export { Portal }; +export { Radio }; +export { RadioGroup }; +export { SVGIcon }; +export { Tab }; +export { Tabs }; +export { Tile }; +export { TileInfo }; +export { TileInfoLine }; +export { TileFooter }; +export { TileFooterCell }; + +export default { + Accordion, + Button, + Checkbox, + Checklist, + Input, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalTitle, + Panel, + PopupMenu, + Portal, + Radio, + RadioGroup, + SVGIcon, + Tab, + Tabs, + Tile, + TileInfo, + TileInfoLine, + TileFooter, + TileFooterCell +}; diff --git a/src/style/scss/_common.scss b/src/style/scss/_common.scss new file mode 100644 index 0000000..7daac20 --- /dev/null +++ b/src/style/scss/_common.scss @@ -0,0 +1,7 @@ +@import "common/normalize"; +@import "common/variables"; +@import "common/mixins"; +@import "common/typography"; +@import "common/base"; +@import "common/icons"; +@import "common/animation"; diff --git a/src/style/scss/_components.scss b/src/style/scss/_components.scss new file mode 100644 index 0000000..3b0d28d --- /dev/null +++ b/src/style/scss/_components.scss @@ -0,0 +1,22 @@ +@import "../../../components/button/button"; +@import "../../../components/tile/tile"; +@import "../../../components/checkbox/checkbox"; +@import "../../../components/radio/radio"; +@import "../../../components/radioGroup/radioGroup"; +@import "../../../components/tabs/tabs"; +@import "../../../components/icon/icon"; +@import "../../../components/input/input"; +@import "../../../components/dropdown/dropdown"; +@import "../../../components/modal/modal"; +@import "../../../components/menu/menu"; +@import "../../../components/filter-bar/_filter-bar"; +@import "../../../components/search-bar/_search-bar"; +@import "../../../components/checklist/checklist"; +@import "../../../components/autocomplete/autocomplete"; +@import "../../../components/tooltip/tooltip"; +@import "../../../components/tag-cloud/_tag-cloud"; +@import "../../../components/notification/notification"; +@import "../../../components/notifications-container/notifications-container"; +@import "../../../components/accordion/accordion"; +@import "../../../components/panel/panel"; +@import "../../../components/validation/validation"; diff --git a/src/style/scss/angular/_svg_icon.scss b/src/style/scss/angular/_svg_icon.scss new file mode 100644 index 0000000..16be14b --- /dev/null +++ b/src/style/scss/angular/_svg_icon.scss @@ -0,0 +1,210 @@ +@mixin color-icon($primary-color) { + color: $primary-color; + fill: $primary-color; +} + +@mixin color-icon-hover($secondary-color) { + &.clickable { + &:not([disabled]):hover, &:active, &:focus { + @include color-icon($secondary-color); + } + } +} + +@mixin color-icon-label($primary-color) { + @include color-icon($primary-color); + + .svg-icon { + @include color-icon($primary-color); + } +} + +@mixin color-icon-label-hover($secondary-color) { + &.clickable { + &:not([disabled]):hover, &:active, &:focus { + @include color-icon-label($secondary-color); + } + } +} + +.svg-icon { + display: inline-flex; + width: 24px; + height: 24px; + + & > svg { + width: 100%; + height: 100%; + } + + &[disabled] { + opacity: 0.7; + } + + &.mode-primary { + @include color-icon($blue); + @include color-icon-hover($light-blue); + } + + &.mode-secondary { + @include color-icon($gray); + @include color-icon-hover($dark-gray); + } + + &.mode-success { + @include color-icon($green); + } + + &.mode-error { + @include color-icon($red); + } + + &.mode-warning { + @include color-icon($yellow); + } + + &.mode-info { + @include color-icon($text-black); + @include color-icon-hover($dark-blue); + } + + &.size-x_small { + width: 12px; + height: 12px; + } + + &.size-small { + width: 16px; + height: 16px; + } + + &.size-medium { + width: 24px; + height: 24px; + } + + &.size-large { + width: 36px; + height: 36px; + } + + &.size-x_large { + width: 48px; + height: 48px; + } +} + +.svg-icon-wrapper { + display: inline-flex; + justify-content: center; + align-items: center; + + &.svg-icon-label { + } + + &.svg-icon { + } + + &[disabled] { + opacity: 0.7; + } + + &.label-placement-bottom { + flex-direction: column; + .svg-icon-label { + margin-top: 0.25em; + } + } + + &.label-placement-right { + .svg-icon-label { + margin-left: 0.25em; + } + } + + &.label-placement-top { + flex-direction: column-reverse; + .svg-icon-label { + margin-bottom: 0.25em; + } + } + + &.label-placement-left { + flex-direction: row-reverse; + .svg-icon-label { + margin-right: 0.25em; + } + } + + &.mode-primary { + @include color-icon-label($blue); + @include color-icon-label-hover($light-blue); + } + + &.mode-secondary { + @include color-icon-label($gray); + @include color-icon-label-hover($dark-gray); + } + + &.mode-success { + @include color-icon-label($green); + } + + &.mode-error { + @include color-icon-label($red); + } + + &.mode-warning { + @include color-icon-label($yellow); + } + + &.mode-info { + @include color-icon-label($text-black); + @include color-icon-label-hover($dark-blue); + } + + &.size-x_small { + font-size: 8px; + line-height: 10px; + + .svg-icon { + @extend .svg-icon.size-x_small; + } + } + + &.size-small { + font-size: 12px; + line-height: 14px; + + .svg-icon { + @extend .svg-icon.size-small; + } + } + + &.size-medium { + font-size: 16px; + line-height: 20px; + + .svg-icon { + @extend .svg-icon.size-medium; + } + } + + &.size-large { + font-size: 24px; + line-height: 28px; + + .svg-icon { + @extend .svg-icon.size-large; + } + } + + &.size-x_large { + font-size: 34px; + line-height: 40px; + + .svg-icon { + @extend .svg-icon.size-x_large; + } + } +} diff --git a/src/style/scss/angular/_tooltip_custom_style.scss b/src/style/scss/angular/_tooltip_custom_style.scss new file mode 100644 index 0000000..886b1dc --- /dev/null +++ b/src/style/scss/angular/_tooltip_custom_style.scss @@ -0,0 +1,9 @@ +.sdc-custom-tooltip { + background-color: $dark-blue; + border-color: $dark-blue; + border-radius: 10px; + + &:after { + border-color: $dark-blue transparent transparent transparent; + } +} diff --git a/src/style/scss/common/_animation.scss b/src/style/scss/common/_animation.scss new file mode 100644 index 0000000..659bd3b --- /dev/null +++ b/src/style/scss/common/_animation.scss @@ -0,0 +1,149 @@ +/*********************************************************************************** + VERTICAL COLLAPSE-EXPEND TRANSITION ANIMATION PAIR. + + We use the 'transition-vertical-collapse' for the collapse/idle block element, + and the 'transition-vertical-expand' to expend that element. + + -important: The element that will be used for the animation should be + a block element, adn have a content or width and height settings for it to work. +*********************************************************************************/ + +/** +Enable to fold an expended block element +@param $offsetY - The top position from which the drop down should fold + */ +@mixin keyframes-expand-animation($name, $maxHeight, $boxShadow:0 0 12px 0px rgba(0,0,0,.3), $margin:0){ + @keyframes #{$name} { + 0% { + opacity: 0; + max-height: 0; + overflow: hidden; + box-shadow: 0 0 0px 0px rgba(0,0,0,.3); + margin:0; + } + 10% { + opacity: 1; + margin: $margin; + } + 50% { + box-shadow: $boxShadow; + } + 99%{ + max-height:$maxHeight; + + overflow: hidden; + } + 100%{ + opacity: 1; + max-height:$maxHeight; + overflow: auto; + } + } +} + +/** +Enable to expend a folded block element +@param $maxHeight - most of the animation is done over the max-height property + so we have to set the maximum height the expended element can expend to. + */ +@mixin keyframes-collapse-animation($name, $maxHeight, $boxShadow:0 0 12px 0px rgba(0,0,0,.3)){ + @keyframes #{$name} { + 0% { + opacity: 1; + max-height:$maxHeight; + box-shadow: $boxShadow; + overflow: hidden; + } + 40%{ + opacity: 1; + } + 99%{ + opacity: 0; + max-height: 0; + overflow: hidden; + box-shadow: 0 0 0px 0px rgba(0,0,0,.3); + } + 100%{ + opacity: 0; + max-height: 0; + overflow: auto; + } + } +} + +/******************************************************************************** + SIMPLE FADE-IN KEYFRAMES ANIMATION (Used in tooltip for example) + + we use 'mixin-keyframes-fade-in-vertically' to create css @keyframes rule that + we later can use with animation property inside our prefered css rules: + .our_class { + ... + animation: keyframes-fade-in-vertically 1s ease-out; + ... + } +*********************************************************************************/ +@mixin mixin-keyframes-fade-in-vertically($fromRelativeHeight, $keyframesName:keyframes-fade-in-vertically){ + @keyframes #{$keyframesName} { + from { + transform: translateY($fromRelativeHeight); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } +} + +/******************************************************************************** + SIMPLE FADE-OUT KEYFRAMES ANIMATION (Opposite of fade-in mixin above) +*********************************************************************************/ +@mixin mixin-keyframes-fade-out-vertically($toRelativeHeight, $keyframesName:keyframes-fade-out-vertically){ + @keyframes #{$keyframesName} { + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY($toRelativeHeight); + opacity: 0; + } + } +} + + + +/******************************************************************************** + RIPPLE ANIMATION (Used for ripple-click directive) +*********************************************************************************/ +@keyframes ripple-animation { + from { + transform: scale(0,0); + opacity: 1; + } + to { + transform: scale(2,2); + opacity: 0; + } +} + +.sdc-ripple-click__animated { + position:relative; +} +.sdc-ripple-click__animated::before{ + display: inline-block; + position:absolute; + top: 0; + left: 0; + content: ''; + animation: ripple-animation .3s ease-out; + background-color: $blue; + width: 14px; + height: 14px; + border-radius: 50%; + pointer-events: none; + opacity: 0; +} + + + diff --git a/src/style/scss/common/_icons.scss b/src/style/scss/common/_icons.scss new file mode 100644 index 0000000..00f425d --- /dev/null +++ b/src/style/scss/common/_icons.scss @@ -0,0 +1,19 @@ +.sdc-icon { + display: inline-block; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + width: 16px; + height: 16px; +} + +.sdc-icon-locked {background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='11' height='15' viewBox='0 0 11 15' id='locked_icon'> <metadata><?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?><x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Adobe XMP Core 5.6-c138 79.159824, 2016/09/14-01:09:01 '> <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> <rdf:Description rdf:about=''/> </rdf:RDF></x:xmpmeta><?xpacket end='w'?></metadata><defs> <style> .cls-1 { fill: #959595; fill-rule: evenodd; } </style> </defs> <path id='Shape_77_copy_10' data-name='Shape 77 copy 10' class='cls-1' d='M445,359a16.71,16.71,0,0,0-2.1-.009c-1.945.045-3.195,0.049-3.9,0.009v-5a1.743,1.743,0,0,1,2-2h1a1.743,1.743,0,0,1,2,2v5c0.474,0.063.343-.073,1,0,0.266,0.029,0,.279,0,0v-5a2.726,2.726,0,0,0-3-3h-1.142c-1.72-.125-2.715,1.562-2.858,3,0.088,0.009,0,7.338,0,5h0a1.891,1.891,0,0,0-2,1.689v3.461A1.823,1.823,0,0,0,437.775,366h7.448A1.823,1.823,0,0,0,447,364.15v-3.461A2.018,2.018,0,0,0,445,359Z' transform='translate(-436 -351)'/></svg>"); background-repeat: no-repeat;} +.sdc-icon-plus {background-image: url("data:image/svg+xml;utf8,<?xml version='1.0' encoding='utf-8'?><!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg version='1.1' id='plus_icon' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 19 19' style='enable-background:new 0 0 19 19;' xml:space='preserve'><g><rect y='8' width='19' height='3'/><path id='Rectangle_2139_copy' d='M8,19V0h3v19H8z'/></g></svg>"); background-repeat: no-repeat;} +.sdc-icon-unlocked {background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='11' height='18' viewBox='0 0 11 18' id='unlocked_icon'> <metadata><?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?><x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Adobe XMP Core 5.6-c138 79.159824, 2016/09/14-01:09:01 '> <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> <rdf:Description rdf:about=''/> </rdf:RDF></x:xmpmeta><?xpacket end='w'?></metadata><defs> <style> .cls-1 { fill: #959595; fill-rule: evenodd; } </style> </defs> <path id='Shape_77_copy_16' data-name='Shape 77 copy 16' class='cls-1' d='M663,358a16.723,16.723,0,0,0-2.1-.009c-1.944.045-3.194,0.049-3.9,0.009v-7a1.743,1.743,0,0,1,2-2h1a1.743,1.743,0,0,1,2,2v2c0.474,0.064.343-.073,1,0,0.266,0.029,0,.279,0,0v-2a2.726,2.726,0,0,0-3-3h-1.142c-1.72-.125-2.715,1.562-2.858,3,0.088,0.009,0,9.338,0,7h0a1.891,1.891,0,0,0-2,1.689v4.461a1.823,1.823,0,0,0,1.775,1.85h7.448A1.823,1.823,0,0,0,665,364.15v-4.461A2.018,2.018,0,0,0,663,358Zm1.05,6.15a0.827,0.827,0,0,1-.8.836H655.8a0.827,0.827,0,0,1-.8-0.836l0-4.15a1.164,1.164,0,0,1,.8-1.147h7.448A1.129,1.129,0,0,1,664,360Z' transform='translate(-654 -348)'/></svg>"); background-repeat: no-repeat;} +.sdc-icon-vendor {background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 53 47' id='vendor_icon'><title>vendor</title><g id='Layer_2' data-name='Layer 2'><g id='vlm_icon' data-name='vlm icon'><path d='M49,7,38.5,7V5.92A5.92,5.92,0,0,0,32.58,0H20.42A5.92,5.92,0,0,0,14.5,5.92V7.15L4,7.2a3.8,3.8,0,0,0-4,3.5V43.5C0,45.4,2,47,4.2,47L49,46.8a3.8,3.8,0,0,0,4-3.5V10.5A3.8,3.8,0,0,0,49,7ZM16.5,5.92A3.92,3.92,0,0,1,20.42,2H32.58A3.92,3.92,0,0,1,36.5,5.92V7.06l-20,.09ZM2,10.8A1.9,1.9,0,0,1,4,9l45-.2a1.9,1.9,0,0,1,2,1.8v8.87L32.94,24.18a6.49,6.49,0,0,0-12.89,0L2,19.51V10.8ZM31,25a4.5,4.5,0,1,1-4.5-4.5A4.5,4.5,0,0,1,31,25ZM49,45,4,45.2A1.9,1.9,0,0,1,2,43.4V21.57l18.13,4.73a6.5,6.5,0,0,0,12.74,0L51,21.53V43.21A1.9,1.9,0,0,1,49,45Z'/></g></g></svg>"); background-repeat: no-repeat;} +.sdc-icon-vlm {background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 45 53'><title>vlm_new_icon</title><g id='Layer_2' data-name='Layer 2'><g id='vlm_icon' data-name='vlm icon'><path d='M41,2a2,2,0,0,1,2,2l.19,45a2,2,0,0,1-2,2H4a2,2,0,0,1-2-2L1.81,4a2,2,0,0,1,2-2H41m-.15-2H4A4.2,4.2,0,0,0,0,4.24L.19,49a4,4,0,0,0,4,4H41a4,4,0,0,0,4-4L44.81,4a4,4,0,0,0-4-4Z'/><rect x='14' y='11' width='17' height='2'/><rect x='14' y='18' width='10' height='2'/><polygon points='20.56 38.85 13.87 33.14 15.16 31.62 20.39 36.08 29.08 26.63 30.55 27.98 20.56 38.85'/></g></g></svg>"); background-repeat: no-repeat;} +.sdc-icon-vsp {background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 59.5 40' id='vsp_icon'><title>vsp_new_icon</title><g id='Layer_2' data-name='Layer 2'><g id='vlm_icon' data-name='vlm icon'><path d='M58.28,30.74c-1.49-1.82-3-2.7-4.67-2.74a8.5,8.5,0,0,0-16.22-2.44,6.93,6.93,0,0,0-4.06.66A7.23,7.23,0,0,0,36.42,40H53.5a6,6,0,0,0,6-6A5.18,5.18,0,0,0,58.28,30.74ZM53.5,38H36.42a5.25,5.25,0,0,1-5.21-5.91,5.32,5.32,0,0,1,3-4.06,5,5,0,0,1,2.21-.53,5.25,5.25,0,0,1,1.35.18l.92.24L39,27A6.5,6.5,0,0,1,51.67,29v1.3l1.17-.2c1-.17,2.17-.17,3.91,2a3.18,3.18,0,0,1,.76,2A4,4,0,0,1,53.5,38Z'/><path d='M49,0,4,.17A3.79,3.79,0,0,0,0,3.69V7.94H0v2H0V36.31C0,38.35,2,40,4.25,40l20.84-.08a1,1,0,0,0,0-1.92L4,38.08a1.89,1.89,0,0,1-2-1.76V10H51v7a1,1,0,0,0,2,0V3.53A3.79,3.79,0,0,0,49,0ZM2,8V3.76A1.89,1.89,0,0,1,4,2l45-.16a1.89,1.89,0,0,1,2,1.76V8Z'/></g></g></svg>"); background-repeat: no-repeat;} + +.sdc-icon-transform{ + transform: rotate(180deg); +} diff --git a/src/style/scss/common/_normalize.scss b/src/style/scss/common/_normalize.scss new file mode 100644 index 0000000..9375ee9 --- /dev/null +++ b/src/style/scss/common/_normalize.scss @@ -0,0 +1,578 @@ +/* ========================================================================== + Normalize.scss settings + ========================================================================== */ +/** + * Includes legacy browser support IE6/7 + * + * Set to false if you want to drop support for IE6 and IE7 + */ + +$legacy_browser_support: false !default; + +/* Base + ========================================================================== */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS and IE text size adjust after device orientation change, + * without disabling user zoom. + * 3. Corrects text resizing oddly in IE 6/7 when body `font-size` is set using + * `em` units. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ + @if $legacy_browser_support { + *font-size: 100%; /* 3 */ + } +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ + @if $legacy_browser_support { + *display: inline; + *zoom: 1; + } +} + +/** + * Prevents modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability of focused elements when they are also in an + * active/hover state. + */ + +a { + &:active, &:hover { + outline: 0; + }; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +@if $legacy_browser_support { + blockquote { + margin: 1em 40px; + } +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +@if $legacy_browser_support { + h2 { + font-size: 1.5em; + margin: 0.83em 0; + } + + h3 { + font-size: 1.17em; + margin: 1em 0; + } + + h4 { + font-size: 1em; + margin: 1.33em 0; + } + + h5 { + font-size: 0.83em; + margin: 1.67em 0; + } + + h6 { + font-size: 0.67em; + margin: 2.33em 0; + } +} + +/** + * Addresses styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +@if $legacy_browser_support { + + /** + * Addresses margins set differently in IE 6/7. + */ + + p, + pre { + *margin: 1em 0; + } + + /* + * Addresses CSS quotes not supported in IE 6/7. + */ + + q { + *quotes: none; + } + + /* + * Addresses `quotes` property not supported in Safari 4. + */ + + q:before, + q:after { + content: ''; + content: none; + } +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +@if $legacy_browser_support { + + /* ========================================================================== + Lists + ========================================================================== */ + + /* + * Addresses margins set differently in IE 6/7. + */ + + dl, + menu, + ol, + ul { + *margin: 1em 0; + } + + dd { + *margin: 0 0 0 40px; + } + + /* + * Addresses paddings set differently in IE 6/7. + */ + + menu, + ol, + ul { + *padding: 0 0 0 40px; + } + + /* + * Corrects list images handled incorrectly in IE 7. + */ + + nav ul, + nav ol { + *list-style: none; + *list-style-image: none; + } + +} + +/* Embedded content + ========================================================================== */ + +/** + * 1. Remove border when inside `a` element in IE 8/9/10. + * 2. Improves image quality when scaled in IE 7. + */ + +img { + border: 0; + @if $legacy_browser_support { + *-ms-interpolation-mode: bicubic; /* 2 */ + } +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + * Correct font family set oddly in IE 6, Safari 4/5, and Chrome. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + @if $legacy_browser_support { + _font-family: 'courier new', monospace; + } + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + * 4. Improves appearance and consistency in all browsers. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ + @if $legacy_browser_support { + vertical-align: baseline; /* 3 */ + *vertical-align: middle; /* 3 */ + } +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + * 4. Removes inner spacing in IE 7 without affecting normal text inputs. + * Known issue: inner spacing remains in IE 6. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ + @if $legacy_browser_support { + *overflow: visible; /* 4 */ + } +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + * Known issue: excess padding remains in IE 6. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + @if $legacy_browser_support { + *height: 13px; /* 3 */ + *width: 13px; /* 3 */ + } +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + box-sizing: content-box; /* 2 */ +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + * 3. Corrects text not wrapping in Firefox 3. + * 4. Corrects alignment displayed oddly in IE 6/7. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ + @if $legacy_browser_support { + white-space: normal; /* 3 */ + *margin-left: -7px; /* 4 */ + } +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/src/style/scss/common/_typography.scss b/src/style/scss/common/_typography.scss new file mode 100644 index 0000000..6fd59cc --- /dev/null +++ b/src/style/scss/common/_typography.scss @@ -0,0 +1,96 @@ +$heading-font-1: 28px; +$heading-font-2: 24px; +$heading-font-3: 20px; +$heading-font-4: 16px; +$heading-font-5: 14px; + +$body-font-1: 14px; +$body-font-2: 13px; +$body-font-3: 12px; +$body-font-4: 10px; + +@mixin base-font-regular() { + font-family: OpenSans-Regular, Arial, sans-serif; + font-style: normal; + font-weight: 400; +} + +@mixin base-font-italic(){ + font-family: OpenSans-Italic, OpenSans-Regular, Arial, sans-serif; + font-style: normal; + font-weight: 400; +} + +@mixin base-font-semibold() { + font-family: OpenSans-Semibold, Arial, sans-serif; + font-style: normal; + font-weight: 400; +} + +@mixin font-error() { + color: $red; +} + +@mixin heading-1() { + @include base-font-regular; + font-size: $heading-font-1; +} + +@mixin heading-2() { + @include base-font-regular; + font-size: $heading-font-2; +} + +@mixin heading-3 { + @include base-font-regular; + font-size: $heading-font-3; +} + +@mixin heading-4 { + @include base-font-regular; + font-size: $heading-font-4; +} + +@mixin heading-4-emphasis { + @include base-font-semibold; + font-size: $heading-font-4; +} + +@mixin heading-5 { + @include base-font-semibold; + font-size: $heading-font-5; +} + +@mixin body-1 { + @include base-font-regular; + font-size: $body-font-1; +} + +@mixin body-1-italic { + @include base-font-italic; + font-size: $body-font-1; +} + +@mixin body-2 { + @include base-font-regular; + font-size: $body-font-2; +} + +@mixin body-2-emphasis { + @include base-font-semibold; + font-size: $body-font-2; +} + +@mixin body-3 { + @include base-font-regular; + font-size: $body-font-3; +} +@mixin body-3-emphasis { + @include base-font-semibold; + font-size: $body-font-3; +} + +@mixin body-4 { + @include base-font-regular; + font-size: $body-font-4; +}
\ No newline at end of file diff --git a/src/style/scss/common/base.scss b/src/style/scss/common/base.scss new file mode 100644 index 0000000..02baf81 --- /dev/null +++ b/src/style/scss/common/base.scss @@ -0,0 +1,96 @@ +html { + font-size: 100%; + height: 100%; +} + +body { + /* scrollbar styling for Internet Explorer */ + scrollbar-face-color: $light-gray; + scrollbar-track-color: $white; + scrollbar-shadow-color:$white; + scrollbar-arrow-color: $gray; + + height: 100%; + @extend %noselect; +} + +/* scrollbar styling for Google Chrome | Safari | Opera */ +::-webkit-scrollbar { + width: 11px; + height: 8px; +} + +::-webkit-scrollbar-track { + background-color: $white; + border: 1px solid $light-gray; + border-top:none; + border-bottom:none; +} + +::-webkit-scrollbar-thumb { + border-radius: 6px; + background-color: $gray; + border: 2px solid rgba(0,0,0,0); + background-clip: padding-box; + + &:hover { + border-width:1px 0px 1px 1px; + } +} + +/* Mozilla Firefox currently doesn't support scrollbar styling */ + +ul { + list-style: none; +} + +h1, h2, h3, h4, h5, h6, ul { + margin: 0; + padding: 0; +} + +input[type='text'] { + padding: 4px; + width: 100%; +} + +input[type="checkbox"] { + width: auto; +} + +input, select, button { + @include body-1; + box-sizing: border-box; +} + +fieldset { + border: none; +} + +fieldset { + label { + display: inline-block; + } +} + +.nav-tabs > li > a:focus, +.btn:focus, +.btn:active:focus, +.btn.active:focus { + outline: none; +} + +.error-message{ + color: $red; + @include body-3; + margin-top: 3px; + &:before{ + content: ""; + display: inline-block; + width: 14px; + height: 14px; + margin-right: 6px; + //not correct icon + background: no-repeat url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 24 24"><defs><path id="alert-copy-a" d="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 fill="'+ $red +'" xlink:href="#alert-copy-a"/></g></svg>'); + } +} diff --git a/src/style/scss/common/mixins.scss b/src/style/scss/common/mixins.scss new file mode 100644 index 0000000..c4cb733 --- /dev/null +++ b/src/style/scss/common/mixins.scss @@ -0,0 +1,337 @@ +/* Colors */ +.sdc-bc-white { background-color: $white; } +.sdc-bc-blue { background-color: $blue; } +.sdc-bc-light-blue { background-color: $light-blue; } +.sdc-bc-lighter-blue { background-color: $lighter-blue; } +.sdc-bc-blue-disabled { background-color: $blue-disabled; } +.sdc-bc-dark-blue { background-color: $dark-blue; } +.sdc-bc-black { background-color: $black; } +.sdc-bc-rich-black { background-color: $rich-black; } +.sdc-bc-text-black { background-color: $text-black; } +.sdc-bc-dark-gray { background-color: $dark-gray; } +.sdc-bc-gray { background-color: $gray; } +.sdc-bc-light-gray { background-color: $light-gray; } +.sdc-bc-silver { background-color: $silver; } +.sdc-bc-light-silver { background-color: $light-silver; } +.sdc-bc-green { background-color: $green; } +.sdc-bc-red { background-color: $red; } +.sdc-bc-disabled-red { background-color: $disabled-red; } +.sdc-bc-light-red { background-color: $light-red; } +.sdc-bc-yellow { background-color: $yellow; } +.sdc-bc-dark-purple { background-color: $dark-purple; } +.sdc-bc-purple { background-color: $purple; } +.sdc-bc-light-purple { background-color: $light-purple; } +.sdc-bc-lighter-silver { background-color: $lighter-silver; } +/* Prefix */ +$box-sizing-prefix: webkit moz spec; +$border-radius-prefix: webkit spec; +$box-shadow-radius-prefix: webkit moz spec; +$text-shadow-radius-prefix: spec; +$text-shadow-prefix: spec; +$box-shadow-prefix: all; +$linear-gradient-prefix: all; +$transition-prefix: webkit moz o spec; +$flex-prefix: webkit spec; +$browserPrefixes: webkit moz o ms; + +@mixin prefix($property, $value, $prefixeslist: 'all') { + @if $prefixeslist == all { + -webkit-#{$property}: $value; + -moz-#{$property}: $value; + -ms-#{$property}: $value; + -o-#{$property}: $value; + #{$property}: $value; + } @else { + @each $prefix in $prefixeslist { + @if $prefix == webkit { + -webkit-#{$property}: $value; + } @else if $prefix == moz { + -moz-#{$property}: $value; + } @else if $prefix == ms { + -ms-#{$property}: $value; + } @else if $prefix == o { + -o-#{$property}: $value; + } @else if $prefix == spec { + #{$property}: $value; + } @else { + @warn "No such prefix: #{$prefix}"; + } + } + } +} + +/* Value Prefix*/ +@mixin value-suffix-with-range($property, $valuesuffix, $from, $to, $prefixeslist) { + + @if $prefixeslist == all { + #{property} : -webkit-#{$valuesuffix}($from, $to); + #{property} : -moz-#{$valuesuffix}($from, $to); + #{property} : -o-#{$valuesuffix}($from, $to); + #{property} : -ms-#{$valuesuffix}($from, $to); + + } @else { + @each $prefix in $prefixeslist { + @if $prefix == webkit { + #{property} : -webkit-#{$valuesuffix}($from, $to); + } @else if $prefix == moz { + #{property} : -moz-#{$valuesuffix}($from, $to); + } @else if $prefix == ms { + #{property} : -ms-#{$valuesuffix}($from, $to); + } @else if $prefix == o { + #{property} : -o-#{$valuesuffix}($from, $to); + } @else { + @warn "No such prefix: #{$prefix}"; + } + } + } +} + +/* Box sizing */ +@mixin box-sizing($value: border-box) { + @include prefix(box-sizing, $value, $box-sizing-prefix); +} + +/* Borders & Shadows */ +@mixin box-shadow($value) { + @include prefix(box-shadow, $value, $box-shadow-radius-prefix); +} + +@mixin text-shadow($value) { + @include prefix(text-shadow, $value, $text-shadow-radius-prefix); +} + +@mixin border-radius($value, $positions: all) { + @if ($positions == all) { + @include prefix(border-radius, $value, $border-radius-prefix); + } @else { + @each $position in $positions { + @include prefix(border-#{$position}-radius, $value, $border-radius-prefix); + } + } + +} + +@mixin transition($value) { + @include prefix(transition, $value, $transition-prefix); +} + +/* Opacity */ +@mixin opacity($alpha) { + $ie-opacity: round($alpha * 100); + opacity: $alpha; + filter: unquote("alpha(opacity = #{$ie-opacity})"); +} + +/* Ellipsis */ +@mixin ellipsis($width: 100%, $display: inline-block, $max-width: none) { + overflow: hidden; + text-overflow: ellipsis; + width: $width; + white-space: nowrap; + display: $display; + max-width: $max-width; +} + +@mixin multiline-ellipsis($lineHeight: 1.3em, $lineCount: 2, $bgColor: $white){ + overflow: hidden; + position: relative; + line-height: $lineHeight; + max-height: $lineHeight * $lineCount; + text-align: justify; + // margin-right: -1em; + padding-right: 1em; + &:before { + content: '...'; + position: absolute; + right: 3px; + bottom: 0; + } + &:after { + content: ''; + position: absolute; + right: 0; + width: 1em; + height: 1em; + margin-top: 0.2em; + background: $bgColor; + } +} + +@mixin gradient($from, $to) { + /* fallback/image non-cover color */ + background-color: $from; + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from($from), to($to)); + @include value-suffix-with-range(background-color, linear-gradient, $from, $to, $linear-gradient-prefix); +} + +/* Vertical placement of multuple lines of text */ +@mixin vertical-text($height) { + position: absolute; + top: 50%; + margin-top: -$height/2; +} + +@mixin text-vertical-align($align: middle) { + display: table; + width: 100%; + + & > * { + vertical-align: $align; + display: table-cell; + } +} + +@mixin center-element($width) { + width: $width; + margin-left: auto; + margin-right: auto; +} + +@mixin center-content($width) { + & > * { + @include center-element($width); + } +} + +/* transform-rotate */ +// @mixin +// Defines a 2D rotation, the angle is specified in the parameter +// @param +// $deg - angle in degrees +@mixin transform-rotate($deg) { + transform: rotate($deg + deg); /* IE10 and Mozilla */ + -ms-transform: rotate($deg + deg); /* IE 9 */ + -webkit-transform: rotate($deg + deg); /* Safari and Chrome */ +} + +/* transform-translate */ +// @mixin +// Defines a 2D rotation, the angle is specified in the parameter +// @param +// $deg - angle in degrees +@mixin transform-translate($x, $y) { + transform: translate($x, $y); /* IE10 and Mozilla */ + -ms-transform: translate($x, $y); /* IE 9 */ + -webkit-transform: translate($x, $y); /* Safari and Chrome */ +} + +/* transform-scale */ +// @mixin +// Defines a 2D scale transformation, changing the elements width and height +// @param +// $width - width +// @param +// $height - height +@mixin transform-scale($width, $height) { + transform: scale($width, $height); /* IE10 and Mozilla */ + -ms-transform: scale($width, $height); /* IE 9 */ + -webkit-transform: scale($width, $height); /* Safari and Chrome */ +} + +@mixin scrollable() { + ::-webkit-scrollbar { + width: 8px; + } +} + +@mixin create-circle($size, $bgcolor, $content) { + border-radius: 50%; + width: $size; + height: $size; + background: $bgcolor; + border: 3px solid $bgcolor; + &:after { + content: $content; + position: relative; + left: 9px; + top: 9px; + @include base-font-semibold; + font-size: $body-font-1; + } +} + +/**/ +@mixin keyframe-animation($animationType, $properties, $fromValue, $toValue) { + + @keyframes #{$animationType} { + from { + $startIndex: 1; + @each $property in $properties { + #{$property}: nth($fromValue, $startIndex); + $startIndex: $startIndex + 1; + } + } + to { + $startIndex: 1; + @each $property in $properties { + #{$property}: nth($toValue, $startIndex); + $startIndex: $startIndex + 1; + } + } + } + @-moz-keyframes #{$animationType}{ + /* Firefox */ + from { + $startIndex: 1; + @each $property in $properties { + #{$property}: nth($fromValue, $startIndex); + $startIndex: $startIndex + 1; + } + } + to { + $startIndex: 1; + @each $property in $properties { + #{$property}: nth($toValue, $startIndex); + $startIndex: $startIndex + 1; + } + } + } + @-webkit-keyframes #{$animationType} { + /* Safari and Chrome */ + from { + $startIndex: 1; + @each $property in $properties { + #{$property}: nth($fromValue, $startIndex); + $startIndex: $startIndex + 1; + } + } + to { + $startIndex: 1; + @each $property in $properties { + #{$property}: nth($toValue, $startIndex); + $startIndex: $startIndex + 1; + } + } + } + @-o-keyframes #{$animationType} { + /* Opera */ + from { + $startIndex: 1; + @each $property in $properties { + #{$property}: nth($fromValue, $startIndex); + $startIndex: $startIndex + 1; + } + } + to { + $startIndex: 1; + @each $property in $properties { + #{$property}: nth($toValue, $startIndex); + $startIndex: $startIndex + 1; + } + } + } +} + + +/**/ +@mixin border-shadow($xShadow: 0.545px, $yShadow: 0.839px, $blur: 4px, $spread: 0, $color: $light-gray, $opacity: 0.2) { + @include box-shadow($xShadow $yShadow $blur $spread rgba($color, $opacity)); +} + +%noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/src/style/scss/common/variables.scss b/src/style/scss/common/variables.scss new file mode 100644 index 0000000..38eded4 --- /dev/null +++ b/src/style/scss/common/variables.scss @@ -0,0 +1,35 @@ +// Colors +$black: #000000; +$rich-black: #323943; +$text-black: #191919; +$blue: #009fdb; +$dark-blue: #0568ae; +$light-blue: #1eb9f3; +$lighter-blue: #e6f6fb; +$blue-disabled: #9dd9ef; +$red: #cf2a2a; +$light-red:#ed4141; +$disabled-red:#f4adad; +$purple: #9063cd; +$dark-purple: #702f8a; +$yellow: #ffb81c; +$green: #4ca90c; +$gray: #959595; +$dark-gray: #5a5a5a; +$light-gray: #d2d2d2; +$light-silver: #f2f2f2; +$silver: #eaeaea; + + +$light-purple: #caa2dd; +$lighter-silver: #f8f8f8; +$white: #ffffff; + +$scroll-bar-color: $text-black; + +// Button Sizes +$btn-extra-small: 90px; +$btn-small: 110px; +$btn-medium: 140px; +$btn-large: 180px; +$btn-default: auto; diff --git a/src/style/scss/style.scss b/src/style/scss/style.scss new file mode 100644 index 0000000..5512776 --- /dev/null +++ b/src/style/scss/style.scss @@ -0,0 +1,6 @@ +@import "common"; +@import "components"; + +// for angular +@import "angular/svg_icon"; +@import "angular/tooltip_custom_style"; diff --git a/src/style/scss/themes/1802/_components.scss b/src/style/scss/themes/1802/_components.scss new file mode 100644 index 0000000..6800005 --- /dev/null +++ b/src/style/scss/themes/1802/_components.scss @@ -0,0 +1,23 @@ +/* Deafult theme */ +@import "../../../../../components/tile/tile"; +@import "../../../../../components/checkbox/checkbox"; +@import "../../../../../components/radio/radio"; +@import "../../../../../components/radioGroup/radioGroup"; +@import "../../../../../components/icon/icon"; +@import "../../../../../components/input/input"; +@import "../../../../../components/dropdown/dropdown"; +@import "../../../../../components/menu/menu"; +@import "../../../../../components/filter-bar/_filter-bar"; +@import "../../../../../components/search-bar/_search-bar"; +@import "../../../../../components/checklist/checklist"; +@import "../../../../../components/autocomplete/autocomplete"; +@import "../../../../../components/tooltip/tooltip"; +@import "../../../../../components/tag-cloud/_tag-cloud"; +@import "../../../../../components/notification/notification"; +@import "../../../../../components/notifications-container/notifications-container"; +@import "../../../../../components/validation/validation"; + +/* 1802 theme */ +@import "button"; +@import "modal"; +@import "tabs"; diff --git a/src/style/scss/themes/1802/button.scss b/src/style/scss/themes/1802/button.scss new file mode 100644 index 0000000..05d91d5 --- /dev/null +++ b/src/style/scss/themes/1802/button.scss @@ -0,0 +1,148 @@ +.sdc-button { + @include box-sizing; + display: inline-block; + + outline: none; + border-radius: 2px; + padding: 0 16px; + + height: 32px; + line-height: 32px; + width: 120px; + min-width: 90px; + + cursor: pointer; + text-align: center; + @include body-1; + &:disabled { + cursor: default; + } + + // Primary button + &.sdc-button__primary { + border: none; + background-color: $blue; + color: $white; + + &:not(:disabled) { + &:hover, &:active { + background-color: $light-blue; + } + &:focus:not(:active) { + border: 0.5px solid $white; + background-color: $light-blue; + box-shadow: 0px 0px 0px 1px $light-blue; + } + } + + &:disabled{ + background: $blue-disabled; + } + } + + // Secondary button + &.sdc-button__secondary { + border: 1px solid $light-gray; + background-color: transparent; + color: $text-black; + + &:not(:disabled) { + &:hover, &:active { + background-color: transparent; + color:$text-black; + border: 1px solid $gray; + } + &:focus:not(:active) { + color: $text-black; + box-shadow: inset 0px 0px 0px 0px $light-gray, 0px 0px 0px 1px $gray; + } + } + + &:disabled { + color: $blue-disabled; + border-color: $blue-disabled; + } + } + + // Link button + &.sdc-button__link { + background-color: transparent; + color: $blue; + fill: $blue; + border: none; + + &:not(:disabled) { + &:hover, &:active { + color: $light-blue; + } + &:focus:not(:active) { + border: 1px solid $dark-blue; + color: $light-blue; + } + } + + &:disabled{ + color: $blue-disabled; + } + } + + + // alert button + &.sdc-button__alert { + border: none; + background-color: $red; + color: $white; + + &:not(:disabled) { + &:hover, &:active { + background-color: $light-red; + } + &:focus:not(:active) { + border: 0.5px solid $white; + background-color: $light-red; + box-shadow: 0px 0px 0px 1px $light-red; + } + } + + &:disabled{ + background: $disabled-red; + } + } + + + /*** Sizes ***/ + &.btn-large{ + width: $btn-large; + } + + &.btn-medium{ + width: $btn-medium; + } + + &.btn-small{ + width: $btn-small; + } + + &.btn-x-small{ + width: $btn-extra-small; + } + + &.btn-default{ + width: $btn-default; + } + + /*** Buttons with icons ***/ + .sdc-icon-right{ + margin-left: 15px; + } + + .sdc-icon-left{ + margin-right: 15px; + } + + svg { + display: inline-block; + vertical-align: middle; + } +} + diff --git a/src/style/scss/themes/1802/modal.scss b/src/style/scss/themes/1802/modal.scss new file mode 100644 index 0000000..de99d52 --- /dev/null +++ b/src/style/scss/themes/1802/modal.scss @@ -0,0 +1,193 @@ + +.sdc-modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + + overflow: auto; + margin: auto; + display: flex; + align-items: center; + z-index: 1001; + + + svg path { + fill: inherit; + } + + .sdc-modal__wrapper { + @include body-1; + background: $white; + width: 100%; + + @include box-shadow(0 0 4px 0 rgba(0,0,0,0.50)); + color: $text-black; + display: flex; + flex-direction: column; + &.sdc-modal-type-info { + border-top: solid 6px $blue; + .sdc-modal__svg-use { + fill: $blue; + } + .svg-icon { + &.__errorCircle { + width: 30px; + height: 30px; + } + } + } + &.sdc-modal-type-alert { + border-top: solid 6px $yellow; + .sdc-modal__svg-use { + fill: $yellow; + } + .svg-icon { + &.__exclamationTriangleLine { + width: 30px; + height: 30px; + } + } + } + &.sdc-modal-type-error { + border-top: solid 6px $red; + .sdc-modal__svg-use { + fill: $red; + } + .svg-icon { + &.__error { + width: 30px; + height: 30px; + } + } + } + &.sdc-modal-type-custom { + padding: 0 30px; + border-radius: 4px; + + .sdc-custom__header { + @include box-sizing; + color: $dark-gray; + height: 50px; + border-bottom: solid 3px $blue; + padding: 0; + + .title { + @include heading-3; + color: $dark-gray; + } + + .sdc-modal__close-button { + margin-top: 0px; + width: 20px; + height: 14px; + } + .sdc-modal__close-button-svg { + width: 20px; + height: 20px; + .sdc-modal__svg-use { + fill: $white; + } + .svg-icon { + height: 14px; + width: 14px; + fill: $white; + } + } + } + .sdc-modal__content { + padding: 20px 40px; + } + } + .sdc-modal__header { + padding: 0px 10px 8px 14px; + display: flex; + justify-content: space-between; + text-align: left; + .sdc-modal__icon { + padding: 20px 12px 0px 6px; + } + + .title { + @include heading-2; + flex: 1 1 auto; + color: $text-black; + padding-top: 19px; + } + + .sdc-modal__close-button { + order:3; + width: 14px; + height: 14px; + margin-top:10px; + cursor: pointer; + .sdc-modal__svg-use { + fill: $black; + } + } + } + .sdc-modal__content { + order:2; + padding-left: 63px; + padding-right: 68px; + padding-bottom: 26px; + } + + .sdc-modal__footer { + order:3; + background-color: $light-silver; + border-top: solid 1px $silver; + padding: 17px 30px; + display: flex; + justify-content: flex-end; + margin: 0 -30px; + button{ + margin-left: 10px; + } + } + } +} + +.modal-background { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: $black; + opacity: 0.70; + z-index: 1000; + + &.show { + z-index: 1000; + opacity: 0.70; + transition: opacity .3s ease, z-index .3s; + } + &.hide { + z-index: -1; + opacity: 0; + transition: opacity .35s ease, z-index .35s; + } +} + +.xl { + width: 1200px; +} + +.l { + width: 875px; +} + +.md { + width: 650px; +} + +.sm { + width: 500px; +} + +.xsm { + width: 432px; +} + diff --git a/src/style/scss/themes/1802/style.scss b/src/style/scss/themes/1802/style.scss new file mode 100644 index 0000000..ae314d8 --- /dev/null +++ b/src/style/scss/themes/1802/style.scss @@ -0,0 +1,5 @@ +@import "../../common"; +@import "components"; + +// for angular +@import "../../angular/svg_icon"; diff --git a/src/style/scss/themes/1802/tabs.scss b/src/style/scss/themes/1802/tabs.scss new file mode 100644 index 0000000..70ee4cb --- /dev/null +++ b/src/style/scss/themes/1802/tabs.scss @@ -0,0 +1,39 @@ +.sdc-tabs { + .sdc-tab { + background-color: $white; + border: 1px solid $silver; + border-left: none; + display: inline-block; + height: 36px; + text-align: center; + cursor: pointer; + padding: 2px 10px 0 10px; + margin: 0; + + + &:first-child { + border-left: 1px solid $silver; + } + &.sdc-tab-active { + background-color: $silver; + } + &[disabled] { + opacity: 0.3; + cursor: default; + } + } + &.sdc-tabs-header { + .sdc-tab { + @include heading-2; + } + } + &.sdc-tabs-menu { + .sdc-tab { + @include body-1; + padding: 0px 10px 4px 10px; + } + } + .sdc-tab-content { + margin-top: 30px; + } +} |