This commit is contained in:
Jason Kulatunga
2020-08-19 16:04:21 -07:00
commit 8482272d45
336 changed files with 197309 additions and 0 deletions
@@ -0,0 +1,77 @@
<!-- Open button, 'bar' only -->
<button class="search-toggle-open"
mat-icon-button
*ngIf="appearance === 'bar' && !opened"
(click)="open()">
<mat-icon [svgIcon]="'search'"></mat-icon>
</button>
<!-- Search container -->
<div class="search-container"
*ngIf="appearance === 'basic' || (appearance === 'bar' && opened)"
[@.disabled]="appearance === 'basic'"
@slideInTop
@slideOutTop>
<mat-form-field class="treo-mat-no-subscript search-input"
#searchInput>
<mat-icon matPrefix
[svgIcon]="'search'"></mat-icon>
<input matInput
[formControl]="searchControl"
[placeholder]="'Search for a page or a contact'"
[matAutocomplete]="matAutocomplete"
(keydown)="onKeydown($event)">
</mat-form-field>
<mat-autocomplete [class]="'search-results search-results-appearance-' + appearance"
#matAutocomplete="matAutocomplete"
[disableRipple]="true">
<mat-option class="no-results"
*ngIf="results && !results.length">
No results found!
</mat-option>
<mat-option *ngFor="let result of results"
[routerLink]="result.link">
<!-- Page result -->
<div class="result page-result"
*ngIf="result.resultType === 'page'">
<div class="badge">Page</div>
<div class="title">
<span [innerHTML]="result.title"></span>
<span class="link"
[routerLink]="result.link">{{result.link}}</span>
</div>
</div>
<!-- Contact result -->
<div class="result contact-result"
*ngIf="result.resultType === 'contact'">
<div class="badge">Contact</div>
<div class="title">
<span [innerHTML]="result.title"></span>
</div>
<div class="image">
<img *ngIf="result.avatar"
[src]="result.avatar">
<mat-icon *ngIf="!result.avatar"
[svgIcon]="'account_circle'"></mat-icon>
</div>
</div>
</mat-option>
</mat-autocomplete>
<!-- Close button, 'bar' only -->
<button class="search-toggle-close"
mat-icon-button
*ngIf="appearance === 'bar'"
(click)="close()">
<mat-icon [svgIcon]="'close'"></mat-icon>
</button>
</div>
@@ -0,0 +1,335 @@
@import 'treo';
search {
display: flex;
// Bar appearance
&.search-appearance-bar {
.search-container {
position: absolute;
display: flex;
align-items: center;
flex: 1 0 auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 99;
.search-input {
flex: 1 0 auto;
height: 100%;
.mat-form-field-wrapper {
height: 100%;
.mat-form-field-flex {
height: 100%;
padding: 0 72px 0 32px;
border: none;
border-radius: 0 !important;
@include treo-breakpoint('xs') {
padding: 0 56px 0 24px
}
}
}
}
.search-toggle-close {
position: absolute;
top: 50%;
right: 32px;
margin-top: -20px;
min-width: 40px;
width: 40px;
min-height: 40px;
height: 40px;
@include treo-breakpoint('xs') {
right: 8px;
}
}
}
}
// Basic appearance
&.search-appearance-basic {
width: 100%;
max-width: 400px;
.search-container {
display: flex;
align-items: center;
flex: 1 0 auto;
overflow: hidden;
.search-icon {
margin-left: 16px;
}
.search-input {
width: 100%;
}
}
}
}
// Search results panel
.search-results {
max-height: 512px !important;
&:before,
&:after {
content: ' ';
position: absolute;
width: 0;
height: 0;
bottom: 100%;
left: 30px;
border: solid transparent;
pointer-events: none;
}
&:before {
border-width: 9px;
margin-left: -9px;
}
&:after {
border-width: 8px;
margin-left: -8px;
}
// Bar appearance
&.search-results-appearance-bar {
border-top-width: 1px;
border-radius: 0 0 4px 4px;
@include treo-elevation('md', true);
.mat-option {
padding: 0 40px;
@include treo-breakpoint('xs') {
padding: 0 24px
}
}
}
// Basic appearance
&.search-results-appearance-basic {
margin-top: 8px;
border-radius: 4px;
@include treo-elevation('2xl', true);
.mat-option {
padding: 0 32px;
@include treo-breakpoint('xs') {
padding: 0 24px
}
}
}
.mat-option {
height: 56px;
line-height: 56px;
font-size: 14px;
&.no-results {
pointer-events: none;
}
.mat-option-text {
.result {
display: flex;
align-items: center;
&.contact-result {
.image {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
min-width: 32px;
max-width: 32px;
height: 32px;
min-height: 32px;
max-height: 32px;
margin-left: auto;
border-radius: 50%;
overflow: hidden;
.mat-icon {
margin: 0;
@include treo-icon-size(20);
}
}
}
&.page-result {
.title {
display: flex;
flex-direction: column;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: normal;
}
.link {
margin-top: 4px;
line-height: normal;
font-size: 12px;
text-decoration: none !important;
}
}
}
.badge {
padding: 3px 6px;
margin-right: 16px;
border-radius: 3px;
font-size: 11px;
line-height: normal;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
mark {
font-weight: 500;
}
}
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$primary: map-get($theme, primary);
$is-dark: map-get($theme, is-dark);
search {
// Basic appearance
&.search-appearance-basic {
background: transparent;
}
// Bar appearance
&.search-appearance-bar {
.search-container {
background: map-get($background, card);
.search-input {
.mat-form-field-wrapper {
.mat-form-field-flex {
background: transparent;
}
}
}
}
}
}
// Search results panel
.search-results {
&:before {
border-color: transparent;
border-bottom-color: map-get($foreground, divider);
}
&:after {
border-color: transparent;
border-bottom-color: map-get($background, card);
}
.mat-option {
@include treo-breakpoint('xs') {
background: transparent !important;
}
@include treo-breakpoint('gt-xs') {
&:hover:not(.mat-option-disabled),
&:focus:not(.mat-option-disabled) {
box-shadow: inset 4px 0 0 map-get($primary, default);
}
}
&.no-results {
.mat-option-text {
color: map-get($foreground, secondary-text);
}
}
.mat-option-text {
.result {
&.contact-result {
.badge {
background: treo-color('blue', 500);
color: treo-contrast('blue', 500);
}
}
&.page-result {
.badge {
background: treo-color('purple', 500);
color: treo-contrast('purple', 500);
}
.title {
.link {
color: map-get($foreground, secondary-text);
}
}
}
.image {
@if ($is-dark) {
background: rgba(0, 0, 0, 0.05);
} @else {
background: map-get($primary, 100);
}
.mat-icon {
color: map-get($primary, default);
}
}
.title {
mark {
background: transparent;
color: map-get($primary, default);
}
}
}
}
}
}
}
@@ -0,0 +1,277 @@
import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, Renderer2, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormControl } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { MatFormField } from '@angular/material/form-field';
import { Subject } from 'rxjs';
import { debounceTime, filter, map, takeUntil } from 'rxjs/operators';
import { TreoAnimations } from '@treo/animations/public-api';
@Component({
selector : 'search',
templateUrl : './search.component.html',
styleUrls : ['./search.component.scss'],
encapsulation: ViewEncapsulation.None,
exportAs : 'treoSearch',
animations : TreoAnimations
})
export class SearchComponent implements OnInit, OnDestroy
{
results: any[] | null;
searchControl: FormControl;
// Debounce
@Input()
debounce: number;
// Min. length
@Input()
minLength: number;
// Search
@Output()
search: EventEmitter<any>;
// Private
private _appearance: 'basic' | 'bar';
private _opened: boolean;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {ElementRef} _elementRef
* @param {HttpClient} _httpClient
* @param {Renderer2} _renderer2
*/
constructor(
private _elementRef: ElementRef,
private _httpClient: HttpClient,
private _renderer2: Renderer2
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.appearance = 'basic';
this.debounce = this.debounce || 300;
this.minLength = this.minLength || 2;
this.opened = false;
this.results = null;
this.searchControl = new FormControl();
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for appearance
*
* @param value
*/
@Input()
set appearance(value: 'basic' | 'bar')
{
// If the value is the same, return...
if ( this._appearance === value )
{
return;
}
// Make sure the search is closed, before
// changing the appearance to prevent issues
this.close();
let appearanceClassName;
// Remove the previous appearance class
appearanceClassName = 'search-appearance-' + this.appearance;
this._renderer2.removeClass(this._elementRef.nativeElement, appearanceClassName);
// Store the appearance
this._appearance = value;
// Add the new appearance class
appearanceClassName = 'search-appearance-' + this.appearance;
this._renderer2.addClass(this._elementRef.nativeElement, appearanceClassName);
}
get appearance(): 'basic' | 'bar'
{
return this._appearance;
}
/**
* Setter and getter for opened
*
* @param value
*/
set opened(value: boolean)
{
// If the value is the same, return...
if ( this._opened === value )
{
return;
}
// Store the opened status
this._opened = value;
// If opened...
if ( value )
{
// Add opened class
this._renderer2.addClass(this._elementRef.nativeElement, 'search-opened');
}
else
{
// Remove opened class
this._renderer2.removeClass(this._elementRef.nativeElement, 'search-opened');
}
}
get opened(): boolean
{
return this._opened;
}
/**
* Setter and getter for search input
*
* @param value
*/
@ViewChild('searchInput')
set searchInput(value: MatFormField)
{
// Return if the appearance is basic, since we don't want
// basic search to be focused as soon as the page loads
if ( this.appearance === 'basic' )
{
return;
}
// If the value exists, it means that the search input
// is now in the DOM and we can focus on the input..
if ( value )
{
// Give Angular time to complete the change detection cycle
setTimeout(() => {
// Focus to the input element
value._inputContainerRef.nativeElement.children[0].focus();
});
}
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Subscribe to the search field value changes
this.searchControl.valueChanges
.pipe(
debounceTime(this.debounce),
takeUntil(this._unsubscribeAll),
map((value) => {
// Set the search results to null if there is no value or
// the length of the value is smaller than the minLength
// so the autocomplete panel can be closed
if ( !value || value.length < this.minLength )
{
this.results = null;
}
// Continue
return value;
}),
filter((value) => {
// Filter out undefined/null/false statements and also
// filter out the values that are smaller than minLength
return value && value.length >= this.minLength;
})
)
.subscribe((value) => {
this._httpClient.post('api/common/search', {query: value})
.subscribe((response: any) => {
this.results = response.results;
});
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* On keydown of the search input
*
* @param event
*/
onKeydown(event): void
{
// Listen for escape to close the search
// if the appearance is 'bar'
if ( this.appearance === 'bar' )
{
// Escape
if ( event.keyCode === 27 )
{
// Close the search
this.close();
}
}
}
/**
* Open the search
* Used in 'bar'
*/
open(): void
{
// Return, if it's already opened
if ( this.opened )
{
return;
}
// Open the search
this.opened = true;
}
/**
* Close the search
* * Used in 'bar'
*/
close(): void
{
// Return, if it's already closed
if ( !this.opened )
{
return;
}
// Clear the search input
this.searchControl.setValue('');
// Close the search
this.opened = false;
}
}
@@ -0,0 +1,40 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import { SearchComponent } from 'app/layout/common/search/search.component';
@NgModule({
declarations: [
SearchComponent
],
imports : [
RouterModule.forChild([]),
MatAutocompleteModule,
MatButtonModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
SharedModule
],
exports : [
SearchComponent
],
providers : [
{
provide : MAT_AUTOCOMPLETE_SCROLL_STRATEGY,
useFactory: (overlay: Overlay) => {
return () => overlay.scrollStrategies.block();
},
deps : [Overlay]
}
]
})
export class SearchModule
{
}
@@ -0,0 +1,11 @@
<!-- ----------------------------------------------------------------------------------------------------- -->
<!-- Empty layout -->
<!-- ----------------------------------------------------------------------------------------------------- -->
<empty-layout *ngIf="layout === 'empty'"></empty-layout>
<!-- ----------------------------------------------------------------------------------------------------- -->
<!-- Layouts with horizontal navigation -->
<!-- ----------------------------------------------------------------------------------------------------- -->
<!-- Material -->
<material-layout *ngIf="layout === 'material'"></material-layout>
@@ -0,0 +1,7 @@
layout {
display: flex;
flex: 1 1 auto;
width: 100%;
max-width: 100%;
min-width: 0;
}
@@ -0,0 +1,192 @@
import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { TreoConfigService } from '@treo/services/config';
import { TreoDrawerService } from '@treo/components/drawer';
import { Layout } from 'app/layout/layout.types';
import { AppConfig, Theme } from 'app/core/config/app.config';
@Component({
selector : 'layout',
templateUrl : './layout.component.html',
styleUrls : ['./layout.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class LayoutComponent implements OnInit, OnDestroy
{
config: AppConfig;
layout: Layout;
theme: Theme;
// Private
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {ActivatedRoute} _activatedRoute
* @param {TreoConfigService} _treoConfigService
* @param {TreoDrawerService} _treoDrawerService
* @param {DOCUMENT} _document
* @param {Router} _router
*/
constructor(
private _activatedRoute: ActivatedRoute,
private _treoConfigService: TreoConfigService,
private _treoDrawerService: TreoDrawerService,
@Inject(DOCUMENT) private _document: any,
private _router: Router
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Subscribe to config changes
this._treoConfigService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
// Store the config
this.config = config;
// Store the theme
this.theme = config.theme;
// Update the selected theme class name on body
const themeName = 'treo-theme-' + config.theme;
this._document.body.classList.forEach((className) => {
if ( className.startsWith('treo-theme-') && className !== themeName )
{
this._document.body.classList.remove(className);
this._document.body.classList.add(themeName);
return;
}
});
// Update the layout
this._updateLayout();
});
// Subscribe to NavigationEnd event
this._router.events.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Update the layout
this._updateLayout();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Update the selected layout
*/
private _updateLayout(): void
{
// Get the current activated route
let route = this._activatedRoute;
while ( route.firstChild )
{
route = route.firstChild;
}
// 1. Set the layout from the config
this.layout = this.config.layout;
// 2. Get the query parameter from the current route and
// set the layout and save the layout to the config
const layoutFromQueryParam = (route.snapshot.queryParamMap.get('layout') as Layout);
if ( layoutFromQueryParam )
{
this.config.layout = this.layout = layoutFromQueryParam;
}
// 3. Iterate through the paths and change the layout as we find
// a config for it.
//
// The reason we do this is that there might be empty grouping
// paths or componentless routes along the path. Because of that,
// we cannot just assume that the layout configuration will be
// in the last path's config or in the first path's config.
//
// So, we get all the paths that matched starting from root all
// the way to the current activated route, walk through them one
// by one and change the layout as we find the layout config. This
// way, layout configuration can live anywhere within the path and
// we won't miss it.
//
// Also, this will allow overriding the layout in any time so we
// can have different layouts for different routes.
const paths = route.pathFromRoot;
paths.forEach((path) => {
// Check if there is a 'layout' data
if ( path.routeConfig && path.routeConfig.data && path.routeConfig.data.layout )
{
// Set the layout
this.layout = path.routeConfig.data.layout;
}
});
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Set the layout on the config
*
* @param layout
*/
setLayout(layout: string): void
{
// Clear the 'layout' query param to allow layout changes
this._router.navigate([], {
queryParams : {
layout: null
},
queryParamsHandling: 'merge'
}).then(() => {
// Set the config
this._treoConfigService.config = {layout};
});
}
/**
* Set the theme on the config
*
* @param change
*/
setTheme(change: MatSlideToggleChange): void
{
this._treoConfigService.config = {theme: change.checked ? 'dark' : 'light'};
}
}
@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { TreoDrawerModule } from '@treo/components/drawer';
import { LayoutComponent } from 'app/layout/layout.component';
import { EmptyLayoutModule } from 'app/layout/layouts/empty/empty.module';
import { MaterialLayoutModule } from 'app/layout/layouts/horizontal/material/material.module';
import { SharedModule } from 'app/shared/shared.module';
const modules = [
// Empty
EmptyLayoutModule,
// Horizontal navigation
MaterialLayoutModule,
];
@NgModule({
declarations: [
LayoutComponent
],
imports : [
TreoDrawerModule,
SharedModule,
...modules
],
exports : [
...modules
]
})
export class LayoutModule
{
}
@@ -0,0 +1,3 @@
export type Layout = 'empty' |
'centered' | 'enterprise' | 'material' | 'modern' |
'basic' | 'classic' | 'classy' | 'compact' | 'dense' | 'futuristic' | 'thin';
@@ -0,0 +1,13 @@
<!-- Container -->
<div class="container">
<!-- Content -->
<div class="content">
<!-- *ngIf="true" hack is required here for router-outlet to work correctly. Otherwise,
it won't register the changes on the layout and won't update the view. -->
<router-outlet *ngIf="true"></router-outlet>
</div>
</div>
@@ -0,0 +1,39 @@
@import 'treo';
empty-layout {
position: relative;
display: flex;
flex: 1 1 auto;
width: 100%;
// Container
> .container {
display: flex;
flex-direction: column;
flex: 1 1 auto;
width: 100%;
// Content
> .content {
display: flex;
flex-direction: column;
flex: 1 0 auto;
> *:not(router-outlet) {
position: relative;
display: flex;
flex: 1 0 auto;
flex-wrap: wrap;
width: 100%;
min-width: 100%;
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
}
@@ -0,0 +1,45 @@
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
selector : 'empty-layout',
templateUrl : './empty.component.html',
styleUrls : ['./empty.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class EmptyLayoutComponent implements OnInit, OnDestroy
{
// Private
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*/
constructor()
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SharedModule } from 'app/shared/shared.module';
import { EmptyLayoutComponent } from 'app/layout/layouts/empty/empty.component';
@NgModule({
declarations: [
EmptyLayoutComponent
],
imports : [
RouterModule,
SharedModule
],
exports : [
EmptyLayoutComponent
]
})
export class EmptyLayoutModule
{
}
@@ -0,0 +1,67 @@
<!-- Navigation -->
<treo-vertical-navigation class="bg-cool-gray-900 theme-dark"
*ngIf="isScreenSmall"
[appearance]="'classic'"
[mode]="'over'"
[name]="'mainNavigation'"
[navigation]="data.navigation.default"
[opened]="false">
<div treoVerticalNavigationContentHeader>
<a class="logo" routerLink="/dashboard">
<img src="assets/images/logo/scrutiny-logo-white-text.png">
</a>
</div>
</treo-vertical-navigation>
<!-- Wrapper -->
<div class="wrapper">
<!-- Header -->
<div class="header">
<!-- Header container -->
<div class="container">
<!-- Top bar -->
<div class="top-bar">
<!-- Logo -->
<a class="logo"
routerLink="/dashboard"
*ngIf="!isScreenSmall">
<img class="logo-text"
src="assets/images/logo/scrutiny-logo-dark-text.png">
<img class="logo-text-on-dark"
src="assets/images/logo/scrutiny-logo-white-text.png">
</a>
<!-- Spacer -->
<div class="spacer"></div>
<!-- Shortcuts -->
<!-- <shortcuts [shortcuts]="data.shortcuts"></shortcuts>-->
<!-- Notifications -->
<!-- <notifications [notifications]="data.notifications"></notifications>-->
</div>
</div>
</div>
<!-- Content -->
<div class="content">
<!-- *ngIf="true" hack is required here for router-outlet to work correctly. Otherwise,
it won't register the changes on the layout and won't update the view. -->
<router-outlet *ngIf="true"></router-outlet>
</div>
</div>
@@ -0,0 +1,259 @@
@import 'treo';
material-layout {
position: relative;
display: flex;
flex: 1 1 auto;
width: 100%;
> treo-vertical-navigation {
.treo-vertical-navigation-content-header {
.logo {
display: flex;
align-items: center;
height: 80px;
min-height: 80px;
max-height: 80px;
padding: 24px 32px 0 32px;
img {
max-width: 96px;
}
}
}
}
> .wrapper {
display: flex;
flex-direction: column;
align-items: center;
flex: 1 1 auto;
min-width: 0;
> .header {
position: relative;
display: flex;
justify-content: center;
width: 100%;
overflow: hidden;
z-index: 49;
.container {
position: relative;
max-width: 1440px;
width: calc(100% - 64px);
margin: 48px 32px 0 32px;
padding: 16px 0 12px 0;
border-bottom-width: 1px;
border-radius: 12px 12px 0 0;
box-shadow: 0 0 25px 0 rgba(0, 0, 0, 0.1), 0 0 10px 0 rgba(0, 0, 0, 0.04);
overflow: hidden;
@include treo-breakpoint('lt-md') {
margin-top: 32px;
padding: 12px 0;
}
@include treo-breakpoint('xs') {
width: 100%;
margin: 0;
padding: 0;
border-radius: 0;
box-shadow: none;
}
.top-bar,
.bottom-bar {
display: flex;
flex: 1 1 auto;
align-items: center;
height: 64px;
max-height: 64px;
min-height: 64px;
}
.top-bar {
position: relative;
padding: 0 24px;
@include treo-breakpoint('lt-md') {
padding: 0 16px;
}
}
.bottom-bar {
padding: 0 16px;
}
.logo {
display: flex;
align-items: center;
margin: 0 8px;
img {
width: 150px;
min-width: 100px;
max-width: 175px;
}
}
.navigation-toggle-button {
margin-right: 8px;
}
.spacer {
display: flex;
flex: 1 1 auto;
height: 1px;
}
search {
margin-right: 8px;
}
shortcuts {
margin-right: 8px;
}
messages {
margin-right: 8px;
}
notifications {
margin-right: 8px;
}
}
}
> .content {
display: flex;
flex-direction: column;
flex: 1 1 auto;
max-width: 1440px;
width: calc(100% - 64px);
margin: 0 32px;
box-shadow: 0 0 25px 0 rgba(0, 0, 0, 0.1), 0 0 10px 0 rgba(0, 0, 0, 0.04);
@include treo-breakpoint('xs') {
width: 100%;
margin: 0;
box-shadow: none;
}
> *:not(router-outlet) {
position: relative;
display: flex;
flex: 1 1 auto;
}
}
> .footer {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: flex-start;
max-width: 1440px;
width: calc(100% - 64px);
height: 80px;
max-height: 80px;
min-height: 80px;
margin: 0 32px;
padding: 0 24px;
z-index: 49;
border-top-width: 1px;
box-shadow: 0 0 25px 0 rgba(0, 0, 0, 0.1), 0 0 10px 0 rgba(0, 0, 0, 0.04);
@include treo-breakpoint('xs') {
width: 100%;
margin: 0;
box-shadow: none;
}
@include treo-breakpoint('xs') {
height: 56px;
max-height: 56px;
min-height: 56px;
}
}
}
&.fixed-header {
> .wrapper {
> .header {
position: sticky;
top: 0;
}
}
}
&.fixed-footer {
> .wrapper {
> .footer {
position: sticky;
bottom: 0;
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$primary: map-get($theme, primary);
$is-dark: map-get($theme, is-dark);
material-layout {
> .wrapper {
@if ($is-dark) {
background: map-get($background, card);
} @else {
background: treo-color('cool-gray', 200);
}
> .header {
background: map-get($primary, 700);
.container {
background: map-get($background, card);
.logo {
.logo-text {
@if ($is-dark) {
display: none;
}
}
.logo-text-on-dark {
@if (not $is-dark) {
display: none;
}
}
}
}
}
> .content {
background: map-get($background, background);
}
> .footer {
@if (not $is-dark) {
background: map-get($background, card);
}
color: map-get($foreground, secondary-text);
}
}
}
}
@@ -0,0 +1,117 @@
import { Component, HostBinding, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Data, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoMediaWatcherService } from '@treo/services/media-watcher';
import { TreoNavigationService } from '@treo/components/navigation';
@Component({
selector : 'material-layout',
templateUrl : './material.component.html',
styleUrls : ['./material.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MaterialLayoutComponent implements OnInit, OnDestroy
{
data: any;
isScreenSmall: boolean;
@HostBinding('class.fixed-header')
fixedHeader: boolean;
@HostBinding('class.fixed-footer')
fixedFooter: boolean;
// Private
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {ActivatedRoute} _activatedRoute
* @param {TreoMediaWatcherService} _treoMediaWatcherService
* @param {TreoNavigationService} _treoNavigationService
* @param {Router} _router
*/
constructor(
private _activatedRoute: ActivatedRoute,
private _treoMediaWatcherService: TreoMediaWatcherService,
private _treoNavigationService: TreoNavigationService,
private _router: Router
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.fixedHeader = false;
this.fixedFooter = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Getter for current year
*/
get currentYear(): number
{
return new Date().getFullYear();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Subscribe to the resolved route data
this._activatedRoute.data.subscribe((data: Data) => {
this.data = data.initialData;
});
// Subscribe to media changes
this._treoMediaWatcherService.onMediaChange$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe(({matchingAliases}) => {
// Check if the breakpoint is 'lt-md'
this.isScreenSmall = matchingAliases.includes('lt-md');
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Toggle navigation
*
* @param key
*/
toggleNavigation(key): void
{
// Get the navigation
const navigation = this._treoNavigationService.getComponent(key);
if ( navigation )
{
// Toggle the opened status
navigation.toggle();
}
}
}
@@ -0,0 +1,34 @@
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { TreoNavigationModule } from '@treo/components/navigation';
import { SearchModule } from 'app/layout/common/search/search.module';
import { SharedModule } from 'app/shared/shared.module';
import { MaterialLayoutComponent } from 'app/layout/layouts/horizontal/material/material.component';
@NgModule({
declarations: [
MaterialLayoutComponent
],
imports : [
HttpClientModule,
RouterModule,
MatButtonModule,
MatDividerModule,
MatIconModule,
MatMenuModule,
TreoNavigationModule,
SearchModule,
SharedModule
],
exports : [
MaterialLayoutComponent
]
})
export class MaterialLayoutModule
{
}