refractor: shell (#287)
* Refractor components. * Shift theme picker component to shared module. * Shift progress bar service into its own folder. * Add footer to app shell. * Replace mat-icon by font-awesome icons. * Fix sidenav visibility bug for handsets. * Add comments. Closes #286, Fixes #285
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#footer {
|
||||
.divider {
|
||||
margin: 1rem 1rem 0.5rem;
|
||||
margin: 2.5rem 1rem 0.5rem;
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,25 +5,61 @@ import { NgModule } from '@angular/core';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import {
|
||||
faBars,
|
||||
faBell,
|
||||
faChartBar,
|
||||
faCheckCircle,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCog,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileUpload,
|
||||
faFillDrip,
|
||||
faHome,
|
||||
faLocationArrow,
|
||||
faLock,
|
||||
faMoneyBillAlt,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faShieldAlt,
|
||||
faSignOutAlt,
|
||||
faSitemap,
|
||||
faSync,
|
||||
faTachometerAlt,
|
||||
faUniversity,
|
||||
faUserCircle,
|
||||
faUsers,
|
||||
faUserShield
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
/** Add icons to the library for convenient access in other components. */
|
||||
library.add(
|
||||
faBars,
|
||||
faBell,
|
||||
faChartBar,
|
||||
faCheckCircle,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCog,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileUpload,
|
||||
faFillDrip,
|
||||
faHome,
|
||||
faLocationArrow,
|
||||
faLock,
|
||||
faMoneyBillAlt,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faShieldAlt,
|
||||
faSignOutAlt,
|
||||
faSitemap,
|
||||
faSync,
|
||||
faTachometerAlt,
|
||||
faUniversity,
|
||||
faUserCircle,
|
||||
faUsers,
|
||||
faUserShield
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
/** Custom Services */
|
||||
import { I18nService } from 'app/core';
|
||||
import { I18nService } from '../../core/i18n.service';
|
||||
|
||||
/**
|
||||
* Language selector component.
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DeleteDialogComponent } from './delete-dialog/delete-dialog.component';
|
||||
import { FileUploadComponent } from './file-upload/file-upload.component';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
import { LanguageSelectorComponent } from './language-selector/language-selector.component';
|
||||
import { ThemePickerComponent } from './theme-picker/theme-picker.component';
|
||||
|
||||
/** Custom Modules */
|
||||
import { IconsModule } from './icons.module';
|
||||
@@ -29,12 +30,14 @@ import { MaterialModule } from './material.module';
|
||||
DeleteDialogComponent,
|
||||
FileUploadComponent,
|
||||
FooterComponent,
|
||||
LanguageSelectorComponent
|
||||
LanguageSelectorComponent,
|
||||
ThemePickerComponent
|
||||
],
|
||||
exports: [
|
||||
FileUploadComponent,
|
||||
FooterComponent,
|
||||
LanguageSelectorComponent,
|
||||
ThemePickerComponent,
|
||||
CommonModule,
|
||||
IconsModule,
|
||||
MaterialModule,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { ThemeManagerService } from './theme-manager.service';
|
||||
|
||||
describe('ThemeManagerService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ThemeManagerService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ThemeManagerService], (service: ThemeManagerService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/** Angular Imports */
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Theme manager service.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ThemeManagerService {
|
||||
|
||||
/** Stylesheet class for the theme of the application. */
|
||||
private themeManagerClass = 'theme-manager';
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Sets a new theme for the application.
|
||||
* @param {string} href Link to stylesheet.
|
||||
*/
|
||||
setTheme(href: string) {
|
||||
this.getLinkElement().setAttribute('href', href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the current theme of the application.
|
||||
*/
|
||||
removeTheme() {
|
||||
const existingLinkElement = this.getExistingLinkElement();
|
||||
if (existingLinkElement) {
|
||||
document.head.removeChild(existingLinkElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns link element of stylesheet
|
||||
*/
|
||||
private getLinkElement() {
|
||||
return this.getExistingLinkElement() || this.createLinkElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns existing link element of stylesheet with theme manager class
|
||||
*/
|
||||
private getExistingLinkElement() {
|
||||
return document.head.querySelector(`link[rel="stylesheet"].${this.themeManagerClass}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns newly created link element of stylesheet with theme manager class
|
||||
*/
|
||||
private createLinkElement() {
|
||||
const linkElement = document.createElement('link');
|
||||
linkElement.setAttribute('rel', 'stylesheet');
|
||||
linkElement.classList.add(this.themeManagerClass);
|
||||
document.head.appendChild(linkElement);
|
||||
return linkElement;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<button mat-icon-button [matMenuTriggerFor]="themeMenu" matTooltip="Color Schemes">
|
||||
<fa-icon icon="fill-drip" size="lg"></fa-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu class="mifosx-theme-picker-menu" #themeMenu="matMenu" x-position="before">
|
||||
<mat-grid-list cols="2">
|
||||
<mat-grid-tile *ngFor="let theme of themes">
|
||||
<div mat-menu-item (click)="installTheme(theme)">
|
||||
<div class="mifosx-theme-picker-swatch">
|
||||
<fa-icon class="mifosx-theme-chosen-icon" *ngIf="currentTheme.href === theme.href" icon="check-circle" size="lg"></fa-icon>
|
||||
<div class="mifosx-theme-picker-primary" [style.background]="theme.primary"></div>
|
||||
<div class="mifosx-theme-picker-accent" [style.background]="theme.accent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
</mat-menu>
|
||||
@@ -0,0 +1,58 @@
|
||||
$theme-picker-menu-padding: 8px;
|
||||
$theme-picker-grid-cell-size: 48px;
|
||||
$theme-picker-grid-cells-per-row: 2;
|
||||
$theme-picker-swatch-size: 36px;
|
||||
$theme-picker-accent-stripe-size: 6px;
|
||||
|
||||
.mifosx-theme-picker-menu {
|
||||
.mat-menu-content {
|
||||
padding: $theme-picker-menu-padding;
|
||||
}
|
||||
|
||||
[mat-menu-item] {
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mifosx-theme-picker-swatch {
|
||||
position: relative;
|
||||
width: $theme-picker-swatch-size;
|
||||
height: $theme-picker-swatch-size;
|
||||
margin: ($theme-picker-grid-cell-size - $theme-picker-swatch-size) / 2;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
.mifosx-theme-chosen-icon {
|
||||
color: white;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.mifosx-theme-picker-primary {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mifosx-theme-picker-accent {
|
||||
position: absolute;
|
||||
bottom: $theme-picker-accent-stripe-size;
|
||||
width: 100%;
|
||||
height: $theme-picker-accent-stripe-size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ThemePickerComponent } from './theme-picker.component';
|
||||
|
||||
describe('ThemePickerComponent', () => {
|
||||
let component: ThemePickerComponent;
|
||||
let fixture: ComponentFixture<ThemePickerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ThemePickerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ThemePickerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
/** Custom Model */
|
||||
import { Theme } from './theme.model';
|
||||
|
||||
/** Custom Services */
|
||||
import { ThemeStorageService } from './theme-storage.service';
|
||||
|
||||
/**
|
||||
* Theme picker component.
|
||||
*
|
||||
* TODO: Customization of theme for every component and custom background with darker contrast.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-theme-picker',
|
||||
templateUrl: './theme-picker.component.html',
|
||||
styleUrls: ['./theme-picker.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ThemePickerComponent implements OnInit {
|
||||
|
||||
/** Default theme for the application. */
|
||||
currentTheme: Theme = {
|
||||
href: 'denim-yellowgreen.css',
|
||||
primary: '#1074B9',
|
||||
accent: '#B4D575',
|
||||
isDark: false,
|
||||
isDefault: true
|
||||
};
|
||||
/** Available themes for the application. */
|
||||
themes = [
|
||||
this.currentTheme,
|
||||
{
|
||||
href: 'pictonblue-yellowgreen.css',
|
||||
primary: '#1DAEEC',
|
||||
accent: '#B4D575',
|
||||
isDark: false
|
||||
},
|
||||
{
|
||||
href: 'indigo-pink.css',
|
||||
primary: '#3F51B5',
|
||||
accent: '#E91E63',
|
||||
isDark: false,
|
||||
},
|
||||
{
|
||||
href: 'deeppurple-amber.css',
|
||||
primary: '#673AB7',
|
||||
accent: '#FFC107',
|
||||
isDark: false
|
||||
},
|
||||
{
|
||||
href: 'pink-bluegrey.css',
|
||||
primary: '#E91E63',
|
||||
accent: '#607D8B',
|
||||
isDark: true
|
||||
},
|
||||
{
|
||||
href: 'purple-green.css',
|
||||
primary: '#9C27B0',
|
||||
accent: '#4CAF50',
|
||||
isDark: true
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {ThemeStorageService} themeStorageService Theme Storage Service.
|
||||
*/
|
||||
constructor(public themeStorageService: ThemeStorageService) { }
|
||||
|
||||
/**
|
||||
* Initializes the theme for the application.
|
||||
*/
|
||||
ngOnInit() {
|
||||
const theme = this.themeStorageService.getTheme();
|
||||
if (theme) {
|
||||
this.currentTheme = theme;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a new theme for the application.
|
||||
* @param {Theme} theme
|
||||
*/
|
||||
installTheme(theme: Theme) {
|
||||
this.currentTheme = theme;
|
||||
this.themeStorageService.installTheme(theme);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { ThemeStorageService } from './theme-storage.service';
|
||||
|
||||
describe('ThemeStorageService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ThemeStorageService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ThemeStorageService], (service: ThemeStorageService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/** Angular Imports */
|
||||
import {Injectable, EventEmitter} from '@angular/core';
|
||||
|
||||
/** Custom Model */
|
||||
import { Theme } from './theme.model';
|
||||
|
||||
/** Custom Services */
|
||||
import { ThemeManagerService } from './theme-manager.service';
|
||||
|
||||
/**
|
||||
* Theme storage service.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ThemeStorageService {
|
||||
|
||||
/** Key to store current theme of application in local storage. */
|
||||
private themeStorageKey = 'mifosXTheme';
|
||||
/** Theme update event. */
|
||||
onThemeUpdate: EventEmitter<Theme>;
|
||||
|
||||
/**
|
||||
* Initializes theme update event.
|
||||
* @param {ThemeManagerService} themeManagerService
|
||||
*/
|
||||
constructor(public themeManagerService: ThemeManagerService) {
|
||||
this.onThemeUpdate = new EventEmitter<Theme>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores current theme in local storage and emits a theme update event.
|
||||
* @param mifosXTheme
|
||||
*/
|
||||
storeTheme(mifosXTheme: Theme) {
|
||||
localStorage.setItem(this.themeStorageKey, JSON.stringify(mifosXTheme));
|
||||
this.onThemeUpdate.emit(mifosXTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns current theme from local storage
|
||||
*/
|
||||
getTheme(): Theme {
|
||||
return JSON.parse(localStorage.getItem(this.themeStorageKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes current theme from local storage.
|
||||
*/
|
||||
clearTheme() {
|
||||
localStorage.removeItem(this.themeStorageKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new theme for the application and stores in local storage.
|
||||
* @param {Theme} theme
|
||||
*/
|
||||
installTheme(theme: Theme) {
|
||||
if (theme.isDefault) {
|
||||
this.themeManagerService.removeTheme();
|
||||
} else {
|
||||
this.themeManagerService.setTheme(`theme/${theme.href}`);
|
||||
}
|
||||
this.storeTheme(theme);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Theme model.
|
||||
*/
|
||||
export interface Theme {
|
||||
href: string;
|
||||
primary: string;
|
||||
accent: string;
|
||||
isDark?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user