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,14 @@
export class TreoAnimationCurves
{
static STANDARD_CURVE = 'cubic-bezier(0.4, 0.0, 0.2, 1)';
static DECELERATION_CURVE = 'cubic-bezier(0.0, 0.0, 0.2, 1)';
static ACCELERATION_CURVE = 'cubic-bezier(0.4, 0.0, 1, 1)';
static SHARP_CURVE = 'cubic-bezier(0.4, 0.0, 0.6, 1)';
}
export class TreoAnimationDurations
{
static COMPLEX = '375ms';
static ENTERING = '225ms';
static EXITING = '195ms';
}
@@ -0,0 +1,34 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
// -----------------------------------------------------------------------------------------------------
// @ Expand / collapse
// -----------------------------------------------------------------------------------------------------
const expandCollapse = trigger('expandCollapse',
[
state('void, collapsed',
style({
height: '0'
})
),
state('*, expanded',
style('*')
),
// Prevent the transition if the state is false
transition('void <=> false, collapsed <=> false, expanded <=> false', []),
// Transition
transition('void <=> *, collapsed <=> expanded',
animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
export { expandCollapse };
@@ -0,0 +1,330 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
// -----------------------------------------------------------------------------------------------------
// @ Fade in
// -----------------------------------------------------------------------------------------------------
const fadeIn = trigger('fadeIn',
[
state('void',
style({
opacity: 0
})
),
state('*',
style({
opacity: 1
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade in top
// -----------------------------------------------------------------------------------------------------
const fadeInTop = trigger('fadeInTop',
[
state('void',
style({
opacity : 0,
transform: 'translate3d(0, -100%, 0)'
})
),
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade in bottom
// -----------------------------------------------------------------------------------------------------
const fadeInBottom = trigger('fadeInBottom',
[
state('void',
style({
opacity : 0,
transform: 'translate3d(0, 100%, 0)'
})
),
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade in left
// -----------------------------------------------------------------------------------------------------
const fadeInLeft = trigger('fadeInLeft',
[
state('void',
style({
opacity : 0,
transform: 'translate3d(-100%, 0, 0)'
})
),
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade in right
// -----------------------------------------------------------------------------------------------------
const fadeInRight = trigger('fadeInRight',
[
state('void',
style({
opacity : 0,
transform: 'translate3d(100%, 0, 0)'
})
),
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out
// -----------------------------------------------------------------------------------------------------
const fadeOut = trigger('fadeOut',
[
state('*',
style({
opacity: 1
})
),
state('void',
style({
opacity: 0
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out top
// -----------------------------------------------------------------------------------------------------
const fadeOutTop = trigger('fadeOutTop',
[
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
opacity : 0,
transform: 'translate3d(0, -100%, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out bottom
// -----------------------------------------------------------------------------------------------------
const fadeOutBottom = trigger('fadeOutBottom',
[
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
opacity : 0,
transform: 'translate3d(0, 100%, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out left
// -----------------------------------------------------------------------------------------------------
const fadeOutLeft = trigger('fadeOutLeft',
[
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
opacity : 0,
transform: 'translate3d(-100%, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out right
// -----------------------------------------------------------------------------------------------------
const fadeOutRight = trigger('fadeOutRight',
[
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
opacity : 0,
transform: 'translate3d(100%, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
export { fadeIn, fadeInTop, fadeInBottom, fadeInLeft, fadeInRight, fadeOut, fadeOutTop, fadeOutBottom, fadeOutLeft, fadeOutRight };
@@ -0,0 +1 @@
export * from './public-api';
@@ -0,0 +1,15 @@
import { expandCollapse } from './expand-collapse';
import { fadeIn, fadeInBottom, fadeInLeft, fadeInRight, fadeInTop, fadeOut, fadeOutBottom, fadeOutLeft, fadeOutRight, fadeOutTop } from './fade';
import { shake } from './shake';
import { slideInBottom, slideInLeft, slideInRight, slideInTop, slideOutBottom, slideOutLeft, slideOutRight, slideOutTop } from './slide';
import { zoomIn, zoomOut } from './zoom';
export const TreoAnimations = [
expandCollapse,
fadeIn, fadeInTop, fadeInBottom, fadeInLeft, fadeInRight,
fadeOut, fadeOutTop, fadeOutBottom, fadeOutLeft, fadeOutRight,
shake,
slideInTop, slideInBottom, slideInLeft, slideInRight,
slideOutTop, slideOutBottom, slideOutLeft, slideOutRight,
zoomIn, zoomOut
];
@@ -0,0 +1,73 @@
import { animate, keyframes, style, transition, trigger } from '@angular/animations';
// -----------------------------------------------------------------------------------------------------
// @ Shake
// -----------------------------------------------------------------------------------------------------
const shake = trigger('shake',
[
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *, * => true',
[
animate('{{timings}}',
keyframes([
style({
transform: 'translate3d(0, 0, 0)',
offset : 0
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.1
}),
style({
transform: 'translate3d(10px, 0, 0)',
offset : 0.2
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.3
}),
style({
transform: 'translate3d(10px, 0, 0)',
offset : 0.4
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.5
}),
style({
transform: 'translate3d(10px, 0, 0)',
offset : 0.6
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.7
}),
style({
transform: 'translate3d(10px, 0, 0)',
offset : 0.8
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.9
}),
style({
transform: 'translate3d(0, 0, 0)',
offset : 1
})
])
)
],
{
params: {
timings: '0.8s cubic-bezier(0.455, 0.03, 0.515, 0.955)'
}
}
)
]
);
export { shake };
@@ -0,0 +1,252 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
// -----------------------------------------------------------------------------------------------------
// @ Slide in top
// -----------------------------------------------------------------------------------------------------
const slideInTop = trigger('slideInTop',
[
state('void',
style({
transform: 'translate3d(0, -100%, 0)'
})
),
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide in bottom
// -----------------------------------------------------------------------------------------------------
const slideInBottom = trigger('slideInBottom',
[
state('void',
style({
transform: 'translate3d(0, 100%, 0)'
})
),
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide in left
// -----------------------------------------------------------------------------------------------------
const slideInLeft = trigger('slideInLeft',
[
state('void',
style({
transform: 'translate3d(-100%, 0, 0)'
})
),
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide in right
// -----------------------------------------------------------------------------------------------------
const slideInRight = trigger('slideInRight',
[
state('void',
style({
transform: 'translate3d(100%, 0, 0)'
})
),
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide out top
// -----------------------------------------------------------------------------------------------------
const slideOutTop = trigger('slideOutTop',
[
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
transform: 'translate3d(0, -100%, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide out bottom
// -----------------------------------------------------------------------------------------------------
const slideOutBottom = trigger('slideOutBottom',
[
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
transform: 'translate3d(0, 100%, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide out left
// -----------------------------------------------------------------------------------------------------
const slideOutLeft = trigger('slideOutLeft',
[
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
transform: 'translate3d(-100%, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide out right
// -----------------------------------------------------------------------------------------------------
const slideOutRight = trigger('slideOutRight',
[
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
transform: 'translate3d(100%, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
export { slideInTop, slideInBottom, slideInLeft, slideInRight, slideOutTop, slideOutBottom, slideOutLeft, slideOutRight };
@@ -0,0 +1,73 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
// -----------------------------------------------------------------------------------------------------
// @ Zoom in
// -----------------------------------------------------------------------------------------------------
const zoomIn = trigger('zoomIn',
[
state('void',
style({
opacity : 0,
transform: 'scale(0.5)'
})
),
state('*',
style({
opacity : 1,
transform: 'scale(1)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Zoom out
// -----------------------------------------------------------------------------------------------------
const zoomOut = trigger('zoomOut',
[
state('*',
style({
opacity : 1,
transform: 'scale(1)'
})
),
state('void',
style({
opacity : 0,
transform: 'scale(0.5)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
export { zoomIn, zoomOut };
@@ -0,0 +1,29 @@
<!-- Flippable card -->
<ng-container *ngIf="flippable">
<!-- Front -->
<div class="treo-card-front">
<ng-content select="[treoCardFront]"></ng-content>
</div>
<!-- Back -->
<div class="treo-card-back">
<ng-content select="[treoCardBack]"></ng-content>
</div>
</ng-container>
<!-- Normal card -->
<ng-container *ngIf="!flippable">
<!-- Content -->
<ng-content></ng-content>
<!-- Expansion -->
<div class="treo-card-expansion"
*ngIf="expanded"
[@expandCollapse]>
<ng-content select="[treoCardExpansion]"></ng-content>
</div>
</ng-container>
@@ -0,0 +1,85 @@
@import 'treo';
treo-card {
position: relative;
display: flex;
border-radius: 8px;
overflow: hidden;
@include treo-elevation('md');
// Flippable
&.treo-card-flippable {
border-radius: 0;
overflow: visible;
transform-style: preserve-3d;
transition: transform 1s;
@include treo-elevation('none');
&.treo-card-flipped {
.treo-card-front {
visibility: hidden;
opacity: 0;
transform: rotateY(180deg);
}
.treo-card-back {
visibility: visible;
opacity: 1;
transform: rotateY(360deg);
}
}
.treo-card-front,
.treo-card-back {
display: flex;
flex-direction: column;
flex: 1 1 auto;
z-index: 10;
border-radius: 8px;
transition: transform 0.5s ease-out 0s, visibility 0s ease-in 0.2s, opacity 0s ease-in 0.2s;
backface-visibility: hidden;
@include treo-elevation('md');
}
.treo-card-front {
position: relative;
opacity: 1;
visibility: visible;
transform: rotateY(0deg);
overflow: hidden;
}
.treo-card-back {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
visibility: hidden;
transform: rotateY(180deg);
overflow: hidden auto;
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
treo-card {
background: map-get($background, card);
&.treo-card-flippable {
background: transparent;
.treo-card-front,
.treo-card-back {
background: map-get($background, card);
}
}
}
}
@@ -0,0 +1,125 @@
import { Component, ElementRef, Input, Renderer2, ViewEncapsulation } from '@angular/core';
import { TreoAnimations } from '@treo/animations';
@Component({
selector : 'treo-card',
templateUrl : './card.component.html',
styleUrls : ['./card.component.scss'],
encapsulation: ViewEncapsulation.None,
animations : TreoAnimations,
exportAs : 'treoCard'
})
export class TreoCardComponent
{
expanded: boolean;
flipped: boolean;
// Private
private _flippable: boolean;
/**
* Constructor
*
* @param {Renderer2} _renderer2
* @param {ElementRef} _elementRef
*/
constructor(
private _renderer2: Renderer2,
private _elementRef: ElementRef
)
{
// Set the defaults
this.expanded = false;
this.flippable = false;
this.flipped = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for flippable
*
* @param value
*/
@Input()
set flippable(value: boolean)
{
// If the value is the same, return...
if ( this._flippable === value )
{
return;
}
// Update the class name
if ( value )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-card-flippable');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-card-flippable');
}
// Store the value
this._flippable = value;
}
get flippable(): boolean
{
return this._flippable;
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Expand the details
*/
expand(): void
{
this.expanded = true;
}
/**
* Collapse the details
*/
collapse(): void
{
this.expanded = false;
}
/**
* Toggle the expand/collapse status
*/
toggleExpanded(): void
{
this.expanded = !this.expanded;
}
/**
* Flip the card
*/
flip(): void
{
// Return if not flippable
if ( !this.flippable )
{
return;
}
this.flipped = !this.flipped;
// Update the class name
if ( this.flipped )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-card-flipped');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-card-flipped');
}
}
}
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TreoCardComponent } from '@treo/components/card/card.component';
@NgModule({
declarations: [
TreoCardComponent
],
imports : [
CommonModule
],
exports : [
TreoCardComponent
]
})
export class TreoCardModule
{
}
@@ -0,0 +1 @@
export * from '@treo/components/card/public-api';
@@ -0,0 +1,2 @@
export * from '@treo/components/card/card.component';
export * from '@treo/components/card/card.module';
@@ -0,0 +1,90 @@
<div class="range"
(click)="openPickerPanel()"
#pickerPanelOrigin>
<div class="start">
<div class="date">{{range.startDate}}</div>
<div class="time"
*ngIf="range.startTime">{{range.startTime}}</div>
</div>
<div class="separator">-</div>
<div class="end">
<div class="date">{{range.endDate}}</div>
<div class="time"
*ngIf="range.endTime">{{range.endTime}}</div>
</div>
</div>
<ng-template #pickerPanel>
<!-- Start -->
<div class="start">
<div class="month">
<div class="month-header">
<button class="previous-button"
mat-icon-button
(click)="prev()"
tabindex="1">
<mat-icon [svgIcon]="'chevron_left'"></mat-icon>
</button>
<div class="month-label">{{getMonthLabel(1)}}</div>
</div>
<mat-month-view [(activeDate)]="activeDates.month1"
[dateFilter]="dateFilter()"
[dateClass]="dateClass()"
(click)="$event.stopImmediatePropagation()"
(selectedChange)="onSelectedDateChange($event)"
#matMonthView1>
</mat-month-view>
</div>
<mat-form-field class="treo-mat-no-subscript time start-time"
*ngIf="timeRange">
<input matInput
[autocomplete]="'off'"
[formControl]="startTimeFormControl"
(blur)="updateStartTime($event)"
tabindex="3">
<mat-label>Start time</mat-label>
</mat-form-field>
</div>
<!-- End -->
<div class="end">
<div class="month">
<div class="month-header">
<div class="month-label">{{getMonthLabel(2)}}</div>
<button class="next-button"
mat-icon-button
(click)="next()"
tabindex="2">
<mat-icon [svgIcon]="'chevron_right'"></mat-icon>
</button>
</div>
<mat-month-view [(activeDate)]="activeDates.month2"
[dateFilter]="dateFilter()"
[dateClass]="dateClass()"
(click)="$event.stopImmediatePropagation()"
(selectedChange)="onSelectedDateChange($event)"
#matMonthView2>
</mat-month-view>
</div>
<mat-form-field class="treo-mat-no-subscript time end-time"
*ngIf="timeRange">
<input matInput
[formControl]="endTimeFormControl"
(blur)="updateEndTime($event)"
tabindex="4">
<mat-label>End time</mat-label>
</mat-form-field>
</div>
</ng-template>
@@ -0,0 +1,351 @@
@import 'treo';
// Variables
$body-cell-padding: 2px;
treo-date-range {
display: flex;
.range {
display: flex;
align-items: center;
height: 48px;
min-height: 48px;
max-height: 48px;
cursor: pointer;
.start,
.end {
display: flex;
align-items: center;
height: 100%;
padding: 0 16px;
border-radius: 5px;
border-width: 1px;
line-height: 1;
.date {
white-space: nowrap;
+ .time {
margin-left: 8px;
}
}
.time {
white-space: nowrap;
}
}
.separator {
margin: 0 12px;
@include treo-breakpoint('xs') {
margin: 0 2px;
}
}
}
}
.treo-date-range-panel {
border-radius: 4px;
padding: 24px;
.start,
.end {
display: flex;
flex-direction: column;
.month {
max-width: 196px;
min-width: 196px;
width: 196px;
.month-header {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 32px;
margin-bottom: 16px;
.previous-button,
.next-button {
position: absolute;
width: 24px !important;
height: 24px !important;
min-height: 24px !important;
max-height: 24px !important;
line-height: 24px !important;
.mat-icon {
@include treo-icon-size(20);
}
}
.previous-button {
left: 0;
}
.next-button {
right: 0;
}
.month-label {
font-weight: 500;
}
}
mat-month-view {
display: flex;
min-height: 188px;
.mat-calendar-table {
width: 100%;
border-collapse: collapse;
tbody {
tr {
&[aria-hidden=true] {
display: none !important;
}
&:first-child {
td:first-child {
&[aria-hidden=true] {
visibility: hidden;
pointer-events: none;
opacity: 0;
}
}
}
td.mat-calendar-body-cell {
width: 28px !important;
height: 28px !important;
padding: $body-cell-padding !important;
&.treo-date-range {
position: relative;
&:before {
content: '';
position: absolute;
top: $body-cell-padding;
right: 0;
bottom: $body-cell-padding;
left: 0;
}
&.treo-date-range-start {
&:before {
left: $body-cell-padding;
border-radius: 999px 0 0 999px;
}
&.treo-date-range-end,
&:last-child {
&:before {
right: $body-cell-padding;
border-radius: 999px;
}
}
}
&.treo-date-range-end {
&:before {
right: $body-cell-padding;
border-radius: 0 999px 999px 0;
}
&:first-child {
&:before {
left: $body-cell-padding;
border-radius: 999px;
}
}
}
&:first-child {
&:before {
border-radius: 999px 0 0 999px;
}
}
&:last-child {
&:before {
border-radius: 0 999px 999px 0;
}
}
}
.mat-calendar-body-cell-content {
position: relative;
top: 0;
left: 0;
width: 24px;
height: 24px;
font-size: 12px;
}
}
td.mat-calendar-body-label {
+ td.mat-calendar-body-cell {
&.treo-date-range {
&:before {
border-radius: 999px 0 0 999px;
}
&.treo-date-range-start {
&.treo-date-range-end {
border-radius: 999px;
}
}
&.treo-date-range-end {
&:before {
left: $body-cell-padding;
border-radius: 999px;
}
}
}
}
}
}
}
}
}
}
.time {
width: 100%;
max-width: 196px;
}
}
.start {
align-items: flex-start;
margin-right: 20px;
.month {
.month-label {
margin-left: 8px;
}
}
}
.end {
align-items: flex-end;
margin-left: 20px;
.month {
.month-label {
margin-right: 8px;
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ 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);
treo-date-range {
.range {
.start,
.end {
@if ($is-dark) {
background-color: rgba(0, 0, 0, 0.05);
border-color: treo-color('cool-gray', 500);
} @else {
background-color: treo-color('cool-gray', 50);
border-color: treo-color('cool-gray', 300);
}
}
}
}
.treo-date-range-panel {
background: map-get($background, card);
@include treo-elevation('2xl');
.start,
.end {
.month {
.month-header {
.month-label {
color: map-get($foreground, secondary-text);
}
}
mat-month-view {
.mat-calendar-table {
tbody {
tr {
td,
td:hover {
&.treo-date-range {
&:before {
background-color: map-get($primary, 200);
}
.mat-calendar-body-cell-content {
background-color: transparent;
}
}
&.treo-date-range-start,
&.treo-date-range-end {
.mat-calendar-body-cell-content {
background-color: map-get($primary, default);
color: map-get($primary, default-contrast);
}
}
.mat-calendar-body-today {
border: none;
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,715 @@
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, HostBinding, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Overlay } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { MatCalendarCellCssClasses, MatMonthView } from '@angular/material/datepicker';
import { Subject } from 'rxjs';
import * as moment from 'moment';
import { Moment } from 'moment';
@Component({
selector : 'treo-date-range',
templateUrl : './date-range.component.html',
styleUrls : ['./date-range.component.scss'],
encapsulation: ViewEncapsulation.None,
exportAs : 'treoDateRange',
providers : [
{
provide : NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TreoDateRangeComponent),
multi : true
}
]
})
export class TreoDateRangeComponent implements ControlValueAccessor, OnInit, OnDestroy
{
// Range changed
@Output()
readonly rangeChanged: EventEmitter<{ start: string, end: string }>;
activeDates: { month1: Moment, month2: Moment };
setWhichDate: 'start' | 'end';
startTimeFormControl: FormControl;
endTimeFormControl: FormControl;
// Private
@HostBinding('class.treo-date-range')
private _defaultClassNames;
@ViewChild('matMonthView1')
private _matMonthView1: MatMonthView<any>;
@ViewChild('matMonthView2')
private _matMonthView2: MatMonthView<any>;
@ViewChild('pickerPanelOrigin', {read: ElementRef})
private _pickerPanelOrigin: ElementRef;
@ViewChild('pickerPanel')
private _pickerPanel: TemplateRef<any>;
private _dateFormat: string;
private _onChange: (value: any) => void;
private _onTouched: (value: any) => void;
private _programmaticChange: boolean;
private _range: { start: Moment, end: Moment };
private _timeFormat: string;
private _timeRange: boolean;
private readonly _timeRegExp: RegExp;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {ElementRef} _elementRef
* @param {Overlay} _overlay
* @param {Renderer2} _renderer2
* @param {ViewContainerRef} _viewContainerRef
*/
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef,
private _overlay: Overlay,
private _renderer2: Renderer2,
private _viewContainerRef: ViewContainerRef
)
{
// Set the private defaults
this._defaultClassNames = true;
this._onChange = () => {
};
this._onTouched = () => {
};
this._range = {
start: null,
end : null
};
this._timeRegExp = new RegExp('^(0[0-9]|1[0-9]|2[0-4]|[0-9]):([0-5][0-9])(A|(?:AM)|P|(?:PM))?$', 'i');
this._unsubscribeAll = new Subject();
// Set the defaults
this.activeDates = {
month1: null,
month2: null
};
this.dateFormat = 'DD/MM/YYYY';
this.rangeChanged = new EventEmitter();
this.setWhichDate = 'start';
this.timeFormat = '12';
// Initialize the component
this._init();
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for dateFormat input
*
* @param value
*/
@Input()
set dateFormat(value: string)
{
// Return, if the values are the same
if ( this._dateFormat === value )
{
return;
}
// Store the value
this._dateFormat = value;
}
get dateFormat(): string
{
return this._dateFormat;
}
/**
* Setter and getter for timeFormat input
*
* @param value
*/
@Input()
set timeFormat(value: string)
{
// Return, if the values are the same
if ( this._timeFormat === value )
{
return;
}
// Set format based on the time format input
this._timeFormat = value === '12' ? 'hh:mmA' : 'HH:mm';
}
get timeFormat(): string
{
return this._timeFormat;
}
/**
* Setter and getter for timeRange input
*
* @param value
*/
@Input()
set timeRange(value: boolean)
{
// Return, if the values are the same
if ( this._timeRange === value )
{
return;
}
// Store the value
this._timeRange = value;
// If the time range turned off...
if ( !value )
{
this.range = {
start: this._range.start.clone().startOf('day'),
end : this._range.end.clone().endOf('day')
};
}
}
get timeRange(): boolean
{
return this._timeRange;
}
/**
* Setter and getter for range input
*
* @param value
*/
@Input()
set range(value)
{
if ( !value )
{
return;
}
// Check if the value is an object and has 'start' and 'end' values
if ( !value.start || !value.end )
{
console.error('Range input must have "start" and "end" properties!');
return;
}
// Check if we are setting an individual date or both of them
const whichDate = value.whichDate || null;
// Get the start and end dates as moment
const start = moment(value.start);
const end = moment(value.end);
// If we are only setting the start date...
if ( whichDate === 'start' )
{
// Set the start date
this._range.start = start.clone();
// If the selected start date is after the end date...
if ( this._range.start.isAfter(this._range.end) )
{
// Set the end date to the start date but keep the end date's time
const endDate = start.clone().hours(this._range.end.hours()).minutes(this._range.end.minutes()).seconds(this._range.end.seconds());
// Test this new end date to see if it's ahead of the start date
if ( this._range.start.isBefore(endDate) )
{
// If it's, set the new end date
this._range.end = endDate;
}
else
{
// Otherwise, set the end date same as the start date
this._range.end = start.clone();
}
}
}
// If we are only setting the end date...
if ( whichDate === 'end' )
{
// Set the end date
this._range.end = end.clone();
// If the selected end date is before the start date...
if ( this._range.start.isAfter(this._range.end) )
{
// Set the start date to the end date but keep the start date's time
const startDate = end.clone().hours(this._range.start.hours()).minutes(this._range.start.minutes()).seconds(this._range.start.seconds());
// Test this new end date to see if it's ahead of the start date
if ( this._range.end.isAfter(startDate) )
{
// If it's, set the new start date
this._range.start = startDate;
}
else
{
// Otherwise, set the start date same as the end date
this._range.start = end.clone();
}
}
}
// If we are setting both dates...
if ( !whichDate )
{
// Set the start date
this._range.start = start.clone();
// If the start date is before the end date, set the end date as normal.
// If the start date is after the end date, set the end date same as the start date.
this._range.end = start.isBefore(end) ? end.clone() : start.clone();
}
// Prepare another range object that holds the ISO formatted range dates
const range = {
start: this._range.start.clone().toISOString(),
end : this._range.end.clone().toISOString()
};
// Emit the range changed event with the range
this.rangeChanged.emit(range);
// Update the model with the range if the change was not a programmatic change
// Because programmatic changes trigger writeValue which triggers onChange and onTouched
// internally causing them to trigger twice which breaks the form's pristine and touched
// statuses.
if ( !this._programmaticChange )
{
this._onTouched(range);
this._onChange(range);
}
// Set the active dates
this.activeDates = {
month1: this._range.start.clone(),
month2: this._range.start.clone().add(1, 'month')
};
// Set the time form controls
this.startTimeFormControl.setValue(this._range.start.clone().format(this._timeFormat).toString());
this.endTimeFormControl.setValue(this._range.end.clone().format(this._timeFormat).toString());
// Run ngAfterContentInit on month views to trigger
// re-render on month views if they are available
if ( this._matMonthView1 && this._matMonthView2 )
{
this._matMonthView1.ngAfterContentInit();
this._matMonthView2.ngAfterContentInit();
}
// Reset the programmatic change status
this._programmaticChange = false;
}
get range(): any
{
// Clone the range start and end
const start = this._range.start.clone();
const end = this._range.end.clone();
// Build and return the range object
return {
startDate: start.clone().format(this.dateFormat),
startTime: this.timeRange ? start.clone().format(this.timeFormat) : null,
endDate : end.clone().format(this.dateFormat),
endTime : this.timeRange ? end.clone().format(this.timeFormat) : null
};
}
// -----------------------------------------------------------------------------------------------------
// @ Control Value Accessor
// -----------------------------------------------------------------------------------------------------
/**
* Update the form model on change
*
* @param fn
*/
registerOnChange(fn: any): void
{
this._onChange = fn;
}
/**
* Update the form model on blur
*
* @param fn
*/
registerOnTouched(fn: any): void
{
this._onTouched = fn;
}
/**
* Write to view from model when the form model changes programmatically
*
* @param range
*/
writeValue(range: { start: string, end: string }): void
{
// Set this change as a programmatic one
this._programmaticChange = true;
// Set the range
this.range = range;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
// @ TODO: Workaround until "angular/issues/20007" resolved
this.writeValue = () => {
};
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Initialize
*
* @private
*/
private _init(): void
{
// Start and end time form controls
this.startTimeFormControl = new FormControl('', [Validators.pattern(this._timeRegExp)]);
this.endTimeFormControl = new FormControl('', [Validators.pattern(this._timeRegExp)]);
// Set the default range
this._programmaticChange = true;
this.range = {
start: moment().startOf('day').toISOString(),
end : moment().add(1, 'day').endOf('day').toISOString()
};
// Set the default time range
this._programmaticChange = true;
this.timeRange = true;
}
/**
* Parse the time from the inputs
*
* @param value
* @private
*/
private _parseTime(value: string): Moment
{
// Parse the time using the time regexp
const timeArr = value.split(this._timeRegExp).filter((part) => part !== '');
// Get the meridiem
const meridiem = timeArr[2] || null;
// If meridiem exists...
if ( meridiem )
{
// Create a moment using 12-hours format and return it
return moment(value, 'hh:mmA').seconds(0);
}
// If meridiem doesn't exist, create a moment using 24-hours format and return in
return moment(value, 'HH:mm').seconds(0);
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Open the picker panel
*/
openPickerPanel(): void
{
// Create the overlay
const overlayRef = this._overlay.create({
panelClass : 'treo-date-range-panel',
backdropClass : '',
hasBackdrop : true,
scrollStrategy : this._overlay.scrollStrategies.reposition(),
positionStrategy: this._overlay.position()
.flexibleConnectedTo(this._pickerPanelOrigin)
.withPositions([
{
originX : 'start',
originY : 'bottom',
overlayX: 'start',
overlayY: 'top',
offsetY : 8
},
{
originX : 'start',
originY : 'top',
overlayX: 'start',
overlayY: 'bottom',
offsetY : -8
}
])
});
// Create a portal from the template
const templatePortal = new TemplatePortal(this._pickerPanel, this._viewContainerRef);
// On backdrop click
overlayRef.backdropClick().subscribe(() => {
// If template portal exists and attached...
if ( templatePortal && templatePortal.isAttached )
{
// Detach it
templatePortal.detach();
}
// If overlay exists and attached...
if ( overlayRef && overlayRef.hasAttached() )
{
// Detach it
overlayRef.detach();
overlayRef.dispose();
}
});
// Attach the portal to the overlay
overlayRef.attach(templatePortal);
}
/**
* Get month label
*
* @param month
*/
getMonthLabel(month: number): string
{
if ( month === 1 )
{
return this.activeDates.month1.clone().format('MMMM Y');
}
return this.activeDates.month2.clone().format('MMMM Y');
}
/**
* Date class function to add/remove class names to calendar days
*/
dateClass(): any
{
return (date: Moment): MatCalendarCellCssClasses => {
// If the date is both start and end date...
if ( date.isSame(this._range.start, 'day') && date.isSame(this._range.end, 'day') )
{
return ['treo-date-range', 'treo-date-range-start', 'treo-date-range-end'];
}
// If the date is the start date...
if ( date.isSame(this._range.start, 'day') )
{
return ['treo-date-range', 'treo-date-range-start'];
}
// If the date is the end date...
if ( date.isSame(this._range.end, 'day') )
{
return ['treo-date-range', 'treo-date-range-end'];
}
// If the date is in between start and end dates...
if ( date.isBetween(this._range.start, this._range.end, 'day') )
{
return ['treo-date-range', 'treo-date-range-mid'];
}
return undefined;
};
}
/**
* Date filter to enable/disable calendar days
*/
dateFilter(): any
{
return (date: Moment): boolean => {
// If we are selecting the end date, disable all the dates that comes before the start date
return !(this.setWhichDate === 'end' && date.isBefore(this._range.start, 'day'));
};
}
/**
* On selected date change
*
* @param date
*/
onSelectedDateChange(date: Moment): void
{
// Create a new range object
const newRange = {
start : this._range.start.clone().toISOString(),
end : this._range.end.clone().toISOString(),
whichDate: null
};
// Replace either the start or the end date with the new one
// depending on which date we are setting
if ( this.setWhichDate === 'start' )
{
newRange.start = moment(newRange.start).year(date.year()).month(date.month()).date(date.date()).toISOString();
}
else
{
newRange.end = moment(newRange.end).year(date.year()).month(date.month()).date(date.date()).toISOString();
}
// Append the which date to the new range object
newRange.whichDate = this.setWhichDate;
// Switch which date to set on the next run
this.setWhichDate = this.setWhichDate === 'start' ? 'end' : 'start';
// Set the range
this.range = newRange;
}
/**
* Go to previous month on both views
*/
prev(): void
{
this.activeDates.month1 = moment(this.activeDates.month1).subtract(1, 'month');
this.activeDates.month2 = moment(this.activeDates.month2).subtract(1, 'month');
}
/**
* Go to next month on both views
*/
next(): void
{
this.activeDates.month1 = moment(this.activeDates.month1).add(1, 'month');
this.activeDates.month2 = moment(this.activeDates.month2).add(1, 'month');
}
/**
* Update the start time
*
* @param event
*/
updateStartTime(event): void
{
// Parse the time
const parsedTime = this._parseTime(event.target.value);
// Go back to the previous value if the form control is not valid
if ( this.startTimeFormControl.invalid )
{
// Override the time
const time = this._range.start.clone().format(this._timeFormat);
// Set the time
this.startTimeFormControl.setValue(time);
// Do not update the range
return;
}
// Append the new time to the start date
const startDate = this._range.start.clone().hours(parsedTime.hours()).minutes(parsedTime.minutes());
// If the new start date is after the current end date,
// use the end date's time and set the start date again
if ( startDate.isAfter(this._range.end) )
{
const endDateHours = this._range.end.hours();
const endDateMinutes = this._range.end.minutes();
// Set the start date
startDate.hours(endDateHours).minutes(endDateMinutes);
}
// If everything is okay, set the new date
this.range = {
start : startDate.toISOString(),
end : this._range.end.clone().toISOString(),
whichDate: 'start'
};
}
/**
* Update the end time
*
* @param event
*/
updateEndTime(event): void
{
// Parse the time
const parsedTime = this._parseTime(event.target.value);
// Go back to the previous value if the form control is not valid
if ( this.endTimeFormControl.invalid )
{
// Override the time
const time = this._range.end.clone().format(this._timeFormat);
// Set the time
this.endTimeFormControl.setValue(time);
// Do not update the range
return;
}
// Append the new time to the end date
const endDate = this._range.end.clone().hours(parsedTime.hours()).minutes(parsedTime.minutes());
// If the new end date is before the current start date,
// use the start date's time and set the end date again
if ( endDate.isBefore(this._range.start) )
{
const startDateHours = this._range.start.hours();
const startDateMinutes = this._range.start.minutes();
// Set the end date
endDate.hours(startDateHours).minutes(startDateMinutes);
}
// If everything is okay, set the new date
this.range = {
start : this._range.start.clone().toISOString(),
end : endDate.toISOString(),
whichDate: 'end'
};
}
}
@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMomentDateModule } from '@angular/material-moment-adapter';
import { TreoDateRangeComponent } from '@treo/components/date-range/date-range.component';
@NgModule({
declarations: [
TreoDateRangeComponent
],
imports : [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDatepickerModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatMomentDateModule
],
exports : [
TreoDateRangeComponent
]
})
export class TreoDateRangeModule
{
}
@@ -0,0 +1 @@
export * from '@treo/components/date-range/public-api';
@@ -0,0 +1,2 @@
export * from '@treo/components/date-range/date-range.component';
export * from '@treo/components/date-range/date-range.module';
@@ -0,0 +1,3 @@
<div class="treo-drawer-content">
<ng-content></ng-content>
</div>
@@ -0,0 +1,146 @@
@import 'treo';
$treo-drawer-width: 320;
treo-drawer {
position: relative;
display: flex;
flex-direction: column;
flex: 1 1 auto;
width: #{$treo-drawer-width}px;
min-width: #{$treo-drawer-width}px;
max-width: #{$treo-drawer-width}px;
z-index: 300;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, .35);
// Animations
&.treo-drawer-animations-enabled {
transition-duration: 400ms;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
transition-property: visibility, margin-left, margin-right, transform, width, max-width, min-width;
.treo-drawer-content {
transition-duration: 400ms;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
transition-property: width, max-width, min-width;
}
}
// Over mode
&.treo-drawer-mode-over {
position: absolute;
top: 0;
bottom: 0;
// Fixed mode
&.treo-drawer-fixed {
position: fixed;
}
}
// Left position
&.treo-drawer-position-left {
// Side mode
&.treo-drawer-mode-side {
margin-left: #{$treo-drawer-width}px;
&.treo-drawer-opened {
margin-left: 0;
}
}
// Over mode
&.treo-drawer-mode-over {
left: 0;
transform: translate3d(-100%, 0, 0);
&.treo-drawer-opened {
transform: translate3d(0, 0, 0);
}
}
// Content
.treo-drawer-content {
left: 0;
}
}
// Right position
&.treo-drawer-position-right {
// Side mode
&.treo-drawer-mode-side {
margin-right: -#{$treo-drawer-width}px;
&.treo-drawer-opened {
margin-right: 0;
}
}
// Over mode
&.treo-drawer-mode-over {
right: 0;
transform: translate3d(100%, 0, 0);
&.treo-drawer-opened {
transform: translate3d(0, 0, 0);
}
}
// Content
.treo-drawer-content {
right: 0;
}
}
// Content
.treo-drawer-content {
position: absolute;
display: flex;
flex: 1 1 auto;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
}
// Overlay
.treo-drawer-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 299;
opacity: 0;
background-color: rgba(0, 0, 0, 0.6);
// Fixed mode
&.treo-drawer-overlay-fixed {
position: fixed;
}
// Transparent overlay
&.treo-drawer-overlay-transparent {
background-color: transparent;
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
treo-drawer {
background: map-get($background, card);
.treo-drawer-content {
background: map-get($background, card);
}
}
}
@@ -0,0 +1,523 @@
import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnDestroy, OnInit, Output, Renderer2, ViewEncapsulation } from '@angular/core';
import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations';
import { TreoDrawerMode, TreoDrawerPosition } from '@treo/components/drawer/drawer.types';
import { TreoDrawerService } from '@treo/components/drawer/drawer.service';
@Component({
selector : 'treo-drawer',
templateUrl : './drawer.component.html',
styleUrls : ['./drawer.component.scss'],
encapsulation: ViewEncapsulation.None,
exportAs : 'treoDrawer'
})
export class TreoDrawerComponent implements OnInit, OnDestroy
{
// Name
@Input()
name: string;
// Private
private _fixed: boolean;
private _mode: TreoDrawerMode;
private _opened: boolean | '';
private _overlay: HTMLElement | null;
private _player: AnimationPlayer;
private _position: TreoDrawerPosition;
private _transparentOverlay: boolean | '';
// On fixed changed
@Output()
readonly fixedChanged: EventEmitter<boolean>;
// On mode changed
@Output()
readonly modeChanged: EventEmitter<TreoDrawerMode>;
// On opened changed
@Output()
readonly openedChanged: EventEmitter<boolean | ''>;
// On position changed
@Output()
readonly positionChanged: EventEmitter<TreoDrawerPosition>;
@HostBinding('class.treo-drawer-animations-enabled')
private _animationsEnabled: boolean;
/**
* Constructor
*
* @param {AnimationBuilder} _animationBuilder
* @param {TreoDrawerService} _treoDrawerService
* @param {ElementRef} _elementRef
* @param {Renderer2} _renderer2
*/
constructor(
private _animationBuilder: AnimationBuilder,
private _treoDrawerService: TreoDrawerService,
private _elementRef: ElementRef,
private _renderer2: Renderer2
)
{
// Set the private defaults
this._animationsEnabled = false;
this._overlay = null;
// Set the defaults
this.fixedChanged = new EventEmitter<boolean>();
this.modeChanged = new EventEmitter<TreoDrawerMode>();
this.openedChanged = new EventEmitter<boolean | ''>();
this.positionChanged = new EventEmitter<TreoDrawerPosition>();
this.fixed = false;
this.mode = 'side';
this.opened = false;
this.position = 'left';
this.transparentOverlay = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter & getter for fixed
*
* @param value
*/
@Input()
set fixed(value: boolean)
{
// If the value is the same, return...
if ( this._fixed === value )
{
return;
}
// Store the fixed value
this._fixed = value;
// Update the class
if ( this.fixed )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-fixed');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-fixed');
}
// Execute the observable
this.fixedChanged.next(this.fixed);
}
get fixed(): boolean
{
return this._fixed;
}
/**
* Setter & getter for mode
*
* @param value
*/
@Input()
set mode(value: TreoDrawerMode)
{
// If the value is the same, return...
if ( this._mode === value )
{
return;
}
// Disable the animations
this._disableAnimations();
// If the mode changes: 'over -> side'
if ( this.mode === 'over' && value === 'side' )
{
// Hide the overlay
this._hideOverlay();
}
// If the mode changes: 'side -> over'
if ( this.mode === 'side' && value === 'over' )
{
// If the drawer is opened
if ( this.opened )
{
// Show the overlay
this._showOverlay();
}
}
let modeClassName;
// Remove the previous mode class
modeClassName = 'treo-drawer-mode-' + this.mode;
this._renderer2.removeClass(this._elementRef.nativeElement, modeClassName);
// Store the mode
this._mode = value;
// Add the new mode class
modeClassName = 'treo-drawer-mode-' + this.mode;
this._renderer2.addClass(this._elementRef.nativeElement, modeClassName);
// Execute the observable
this.modeChanged.next(this.mode);
// Enable the animations after a delay
// The delay must be bigger than the current transition-duration
// to make sure nothing will be animated while the mode changing
setTimeout(() => {
this._enableAnimations();
}, 500);
}
get mode(): TreoDrawerMode
{
return this._mode;
}
/**
* Setter & getter for opened
*
* @param value
*/
@Input()
set opened(value: boolean | '')
{
// If the value is the same, return...
if ( this._opened === value )
{
return;
}
// If the provided value is an empty string,
// take that as a 'true'
if ( value === '' )
{
value = true;
}
// Set the opened value
this._opened = value;
// If the drawer opened, and the mode
// is 'over', show the overlay
if ( this.mode === 'over' )
{
if ( this._opened )
{
this._showOverlay();
}
else
{
this._hideOverlay();
}
}
// Update opened classes
if ( this.opened )
{
this._renderer2.setStyle(this._elementRef.nativeElement, 'visibility', 'visible');
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-opened');
}
else
{
this._renderer2.setStyle(this._elementRef.nativeElement, 'visibility', 'hidden');
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-opened');
}
// Execute the observable
this.openedChanged.next(this.opened);
}
get opened(): boolean | ''
{
return this._opened;
}
/**
* Setter & getter for position
*
* @param value
*/
@Input()
set position(value: TreoDrawerPosition)
{
// If the value is the same, return...
if ( this._position === value )
{
return;
}
let positionClassName;
// Remove the previous position class
positionClassName = 'treo-drawer-position-' + this.position;
this._renderer2.removeClass(this._elementRef.nativeElement, positionClassName);
// Store the position
this._position = value;
// Add the new position class
positionClassName = 'treo-drawer-position-' + this.position;
this._renderer2.addClass(this._elementRef.nativeElement, positionClassName);
// Execute the observable
this.positionChanged.next(this.position);
}
get position(): TreoDrawerPosition
{
return this._position;
}
/**
* Setter & getter for transparent overlay
*
* @param value
*/
@Input()
set transparentOverlay(value: boolean | '')
{
// If the value is the same, return...
if ( this._opened === value )
{
return;
}
// If the provided value is an empty string,
// take that as a 'true' and set the opened value
if ( value === '' )
{
// Set the opened value
this._transparentOverlay = true;
}
else
{
// Set the transparent overlay value
this._transparentOverlay = value;
}
}
get transparentOverlay(): boolean | ''
{
return this._transparentOverlay;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Register the drawer
this._treoDrawerService.registerComponent(this.name, this);
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Deregister the drawer from the registry
this._treoDrawerService.deregisterComponent(this.name);
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Enable the animations
*
* @private
*/
private _enableAnimations(): void
{
// If the animations are already enabled, return...
if ( this._animationsEnabled )
{
return;
}
// Enable the animations
this._animationsEnabled = true;
}
/**
* Disable the animations
*
* @private
*/
private _disableAnimations(): void
{
// If the animations are already disabled, return...
if ( !this._animationsEnabled )
{
return;
}
// Disable the animations
this._animationsEnabled = false;
}
/**
* Show the backdrop
*
* @private
*/
private _showOverlay(): void
{
// Create the backdrop element
this._overlay = this._renderer2.createElement('div');
// Add a class to the backdrop element
this._overlay.classList.add('treo-drawer-overlay');
// Add a class depending on the fixed option
if ( this.fixed )
{
this._overlay.classList.add('treo-drawer-overlay-fixed');
}
// Add a class depending on the transparentOverlay option
if ( this.transparentOverlay )
{
this._overlay.classList.add('treo-drawer-overlay-transparent');
}
// Append the backdrop to the parent of the drawer
this._renderer2.appendChild(this._elementRef.nativeElement.parentElement, this._overlay);
// Create the enter animation and attach it to the player
this._player =
this._animationBuilder
.build([
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 1}))
]).create(this._overlay);
// Play the animation
this._player.play();
// Add an event listener to the overlay
this._overlay.addEventListener('click', () => {
this.close();
});
}
/**
* Hide the backdrop
*
* @private
*/
private _hideOverlay(): void
{
if ( !this._overlay )
{
return;
}
// Create the leave animation and attach it to the player
this._player =
this._animationBuilder
.build([
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 0}))
]).create(this._overlay);
// Play the animation
this._player.play();
// Once the animation is done...
this._player.onDone(() => {
// If the backdrop still exists...
if ( this._overlay )
{
// Remove the backdrop
this._overlay.parentNode.removeChild(this._overlay);
this._overlay = null;
}
});
}
/**
* On mouseenter
*
* @private
*/
@HostListener('mouseenter')
private _onMouseenter(): void
{
// Enable the animations
this._enableAnimations();
// Add a class
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-hover');
}
/**
* On mouseleave
*
* @private
*/
@HostListener('mouseleave')
private _onMouseleave(): void
{
// Enable the animations
this._enableAnimations();
// Remove the class
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-hover');
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Open the drawer
*/
open(): void
{
// Enable the animations
this._enableAnimations();
// Open
this.opened = true;
}
/**
* Close the drawer
*/
close(): void
{
// Enable the animations
this._enableAnimations();
// Close
this.opened = false;
}
/**
* Toggle the opened status
*/
toggle(): void
{
// Toggle
if ( this.opened )
{
this.close();
}
else
{
this.open();
}
}
}
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TreoDrawerComponent } from '@treo/components/drawer/drawer.component';
@NgModule({
declarations: [
TreoDrawerComponent
],
imports : [
CommonModule
],
exports : [
TreoDrawerComponent
]
})
export class TreoDrawerModule
{
}
@@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import { TreoDrawerComponent } from '@treo/components/drawer/drawer.component';
@Injectable({
providedIn: 'root'
})
export class TreoDrawerService
{
// Private
private _componentRegistry: Map<string, TreoDrawerComponent>;
/**
* Constructor
*/
constructor()
{
// Set the defaults
this._componentRegistry = new Map<string, TreoDrawerComponent>();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Register drawer component
*
* @param name
* @param component
*/
registerComponent(name: string, component: TreoDrawerComponent): void
{
this._componentRegistry.set(name, component);
}
/**
* Deregister drawer component
*
* @param name
*/
deregisterComponent(name: string): void
{
this._componentRegistry.delete(name);
}
/**
* Get drawer component from the registry
*
* @param name
*/
getComponent(name: string): TreoDrawerComponent
{
return this._componentRegistry.get(name);
}
}
@@ -0,0 +1,2 @@
export type TreoDrawerMode = 'over' | 'side';
export type TreoDrawerPosition = 'left' | 'right';
@@ -0,0 +1 @@
export * from '@treo/components/drawer/public-api';
@@ -0,0 +1,4 @@
export * from '@treo/components/drawer/drawer.component';
export * from '@treo/components/drawer/drawer.module';
export * from '@treo/components/drawer/drawer.service';
export * from '@treo/components/drawer/drawer.types';
@@ -0,0 +1,9 @@
<ng-content></ng-content>
<!-- @formatter:off -->
<ng-template let-highlightedCode="highlightedCode" let-lang="lang">
<div class="treo-highlight treo-highlight-code-container">
<pre [ngClass]="'language-' + lang"><code [ngClass]="'language-' + lang" [innerHTML]="highlightedCode"></code></pre>
</div>
</ng-template>
<!-- @formatter:on -->
@@ -0,0 +1,3 @@
textarea[treo-highlight] {
display: none;
}
@@ -0,0 +1,175 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EmbeddedViewRef, Input, Renderer2, SecurityContext, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { TreoHighlightService } from '@treo/components/highlight/highlight.service';
@Component({
selector : 'textarea[treo-highlight]',
templateUrl : './highlight.component.html',
styleUrls : ['./highlight.component.scss'],
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs : 'treoHighlight'
})
export class TreoHighlightComponent implements AfterViewInit
{
highlightedCode: string;
viewRef: EmbeddedViewRef<any>;
@ViewChild(TemplateRef)
templateRef: TemplateRef<any>;
// Private
private _code: string;
private _lang: string;
/**
* Constructor
*
* @param {TreoHighlightService} _treoHighlightService
* @param {DomSanitizer} _domSanitizer
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {ElementRef} _elementRef
* @param {Renderer2} _renderer2
* @param {ViewContainerRef} _viewContainerRef
*/
constructor(
private _treoHighlightService: TreoHighlightService,
private _domSanitizer: DomSanitizer,
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef,
private _renderer2: Renderer2,
private _viewContainerRef: ViewContainerRef
)
{
// Set the private defaults
this._code = '';
this._lang = '';
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for the code
*/
@Input()
set code(value: string)
{
// If the value is the same, return...
if ( this._code === value )
{
return;
}
// Set the code
this._code = value;
// Highlight and insert the code if the
// viewContainerRef is available. This will
// ensure the highlightAndInsert method
// won't run before the AfterContentInit hook.
if ( this._viewContainerRef.length )
{
this._highlightAndInsert();
}
}
get code(): string
{
return this._code;
}
/**
* Setter and getter for the language
*/
@Input()
set lang(value: string)
{
// If the value is the same, return...
if ( this._lang === value )
{
return;
}
// Set the language
this._lang = value;
// Highlight and insert the code if the
// viewContainerRef is available. This will
// ensure the highlightAndInsert method
// won't run before the AfterContentInit hook.
if ( this._viewContainerRef.length )
{
this._highlightAndInsert();
}
}
get lang(): string
{
return this._lang;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* After view init
*/
ngAfterViewInit(): void
{
// Return, if there is no language set
if ( !this.lang )
{
return;
}
// If there is no code input, get the code from
// the textarea
if ( !this.code )
{
// Get the code
this.code = this._elementRef.nativeElement.value;
}
// Highlight and insert
this._highlightAndInsert();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Highlight and insert the highlighted code
*
* @private
*/
private _highlightAndInsert(): void
{
// Return, if the code or language is not defined
if ( !this.code || !this.lang )
{
return;
}
// Destroy the component if there is already one
if ( this.viewRef )
{
this.viewRef.destroy();
}
// Highlight and sanitize the code just in case
this.highlightedCode = this._domSanitizer.sanitize(SecurityContext.HTML, this._treoHighlightService.highlight(this.code, this.lang));
// Render and insert the template
this.viewRef = this._viewContainerRef.createEmbeddedView(this.templateRef, {
highlightedCode: this.highlightedCode,
lang : this.lang
});
// Detect the changes
this.viewRef.detectChanges();
}
}
@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TreoHighlightComponent } from '@treo/components/highlight/highlight.component';
@NgModule({
declarations : [
TreoHighlightComponent
],
imports : [
CommonModule
],
exports : [
TreoHighlightComponent
],
entryComponents: [
TreoHighlightComponent
]
})
export class TreoHighlightModule
{
}
@@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import * as hljs from 'highlight.js';
@Injectable({
providedIn: 'root'
})
export class TreoHighlightService
{
/**
* Constructor
*/
constructor()
{
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Remove the empty lines around the code block
* and re-align the indentation based on the first
* non-whitespace indented character
*
* @param code
* @private
*/
private _format(code: string): string
{
let firstCharIndentation: number | null = null;
// Split the code into lines and store the lines
const lines = code.split('\n');
// Trim the empty lines around the code block
while ( lines.length && lines[0].trim() === '' )
{
lines.shift();
}
while ( lines.length && lines[lines.length - 1].trim() === '' )
{
lines.pop();
}
// Iterate through the lines to figure out the first
// non-whitespace character indentation
lines.forEach((line) => {
// Skip the line if its length is zero
if ( line.length === 0 )
{
return;
}
// We look at all the lines to find the smallest indentation
// of the first non-whitespace char since the first ever line
// is not necessarily has to be the line with the smallest
// non-whitespace char indentation
firstCharIndentation = firstCharIndentation === null ?
line.search(/\S|$/) :
Math.min(line.search(/\S|$/), firstCharIndentation);
});
// Iterate through the lines one more time, remove the extra
// indentation, join them together and return it
return lines.map((line) => {
return line.substring(firstCharIndentation);
}).join('\n');
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Highlight
*/
highlight(code: string, language: string): string
{
// Format the code
code = this._format(code);
// Highlight and return the code
return hljs.highlight(language, code).value;
}
}
@@ -0,0 +1 @@
export * from '@treo/components/highlight/public-api';
@@ -0,0 +1,3 @@
export * from '@treo/components/highlight/highlight.component';
export * from '@treo/components/highlight/highlight.module';
export * from '@treo/components/highlight/highlight.service';
@@ -0,0 +1 @@
export * from '@treo/components/message/public-api';
@@ -0,0 +1,70 @@
<div class="treo-message-container"
*ngIf="!dismissed"
[@fadeIn]="dismissed === null ? false : !dismissed"
[@fadeOut]="dismissed === null ? false : !dismissed">
<!-- Icon -->
<div class="treo-message-icon"
*ngIf="showIcon">
<!-- Custom icon -->
<div class="treo-message-custom-icon">
<ng-content select="[treoMessageIcon]"></ng-content>
</div>
<!-- Default icons -->
<div class="treo-message-default-icon">
<mat-icon *ngIf="type === 'primary'"
[svgIcon]="'check_circle'"></mat-icon>
<mat-icon *ngIf="type === 'accent'"
[svgIcon]="'check_circle'"></mat-icon>
<mat-icon *ngIf="type === 'warn'"
[svgIcon]="'warning'"></mat-icon>
<mat-icon *ngIf="type === 'basic'"
[svgIcon]="'check_circle'"></mat-icon>
<mat-icon *ngIf="type === 'info'"
[svgIcon]="'info'"></mat-icon>
<mat-icon *ngIf="type === 'success'"
[svgIcon]="'check_circle'"></mat-icon>
<mat-icon *ngIf="type === 'warning'"
[svgIcon]="'warning'"></mat-icon>
<mat-icon *ngIf="type === 'error'"
[svgIcon]="'error'"></mat-icon>
</div>
</div>
<!-- Content -->
<div class="treo-message-content">
<div class="treo-message-title">
<p>
<ng-content select="[treoMessageTitle]"></ng-content>
</p>
</div>
<div class="treo-message-message">
<p>
<ng-content></ng-content>
</p>
</div>
</div>
<!-- Dismiss button -->
<button class="treo-message-dismiss-button"
mat-icon-button
(click)="dismiss()">
<mat-icon [svgIcon]="'close'"></mat-icon>
</button>
</div>
@@ -0,0 +1,592 @@
@import 'treo';
treo-message {
display: block;
// Show icon
&.treo-message-show-icon {
.treo-message-container {
padding-left: 56px;
}
}
// Dismissible
&.treo-message-dismissible {
.treo-message-container {
padding-right: 56px;
}
}
// Common
.treo-message-container {
position: relative;
display: flex;
min-height: 64px;
padding: 16px 24px;
font-size: 14px;
line-height: 1;
// Icon
.treo-message-icon {
position: absolute;
top: 20px;
left: 17px;
.treo-message-custom-icon,
.treo-message-default-icon {
display: none;
align-items: center;
justify-content: center;
border-radius: 50%;
&:not(:empty) {
display: flex;
}
}
.treo-message-custom-icon {
display: none;
&:not(:empty) {
display: flex;
+ .treo-message-default-icon {
display: none;
}
}
}
}
// Content
.treo-message-content {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1;
// Title
.treo-message-title {
display: none;
font-size: 15px;
font-weight: 600;
line-height: 1.2;
&:not(:empty) {
display: block;
}
p {
line-height: 1.625;
}
}
// Message
.treo-message-message {
display: none;
&:not(:empty) {
display: block;
}
p {
line-height: 1.625;
}
}
}
// Dismiss button
.treo-message-dismiss-button {
position: absolute;
top: 12px;
right: 12px;
width: 32px !important;
min-width: 32px !important;
height: 32px !important;
min-height: 32px !important;
line-height: 32px !important;
margin-left: auto;
.mat-icon {
@include treo-icon-size(20);
}
}
}
// Dismissible
&:not(.treo-message-dismissible) {
.treo-message-container {
.treo-message-dismiss-button {
display: none !important;
}
}
}
// Border
&.treo-message-appearance-border {
.treo-message-container {
overflow: hidden;
border-left-width: 4px;
border-radius: 4px;
@include treo-elevation('xl');
}
}
// Fill
&.treo-message-appearance-fill {
.treo-message-container {
border-radius: 4px;
}
}
// Outline
&.treo-message-appearance-outline {
.treo-message-container {
overflow: hidden;
border-radius: 4px;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 6px;
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$is-dark: map-get($theme, is-dark);
treo-message {
.treo-message-container {
// Icon
.mat-icon {
color: currentColor;
}
}
// Border
&.treo-message-appearance-border {
.treo-message-container {
background: map-get($background, card);
.treo-message-message {
color: map-get($foreground, secondary-text);
}
}
// Primary
&.treo-message-type-primary {
.treo-message-container {
border-left-color: map-get($primary, default);
.treo-message-title,
.treo-message-icon {
color: map-get($primary, default);
}
}
}
// Accent
&.treo-message-type-accent {
.treo-message-container {
border-left-color: map-get($accent, default);
.treo-message-title,
.treo-message-icon {
color: map-get($accent, default);
}
}
}
// Warn
&.treo-message-type-warn {
.treo-message-container {
border-left-color: map-get($warn, default);
.treo-message-title,
.treo-message-icon {
color: map-get($warn, default);
}
}
}
// Basic
&.treo-message-type-basic {
.treo-message-container {
border-left-color: treo-color('cool-gray', 600);
.treo-message-title,
.treo-message-icon {
color: treo-color('cool-gray', 600);
}
}
}
// Info
&.treo-message-type-info {
.treo-message-container {
border-left-color: treo-color('blue', 600);
.treo-message-title,
.treo-message-icon {
color: treo-color('blue', 700);
}
}
}
// Success
&.treo-message-type-success {
.treo-message-container {
border-left-color: treo-color('green', 500);
.treo-message-title,
.treo-message-icon {
color: treo-color('green', 500);
}
}
}
// Warning
&.treo-message-type-warning {
.treo-message-container {
border-left-color: treo-color('yellow', 400);
.treo-message-title,
.treo-message-icon {
color: treo-color('yellow', 400);
}
}
}
// Error
&.treo-message-type-error {
.treo-message-container {
border-left-color: treo-color('red', 600);
.treo-message-title,
.treo-message-icon {
color: treo-color('red', 700);
}
}
}
}
// Fill
&.treo-message-appearance-fill {
// Primary
&.treo-message-type-primary {
.treo-message-container {
background: map-get($primary, default);
color: map-get($primary, default-contrast);
code {
background: map-get($primary, 600);
color: map-get($primary, '600-contrast');
}
}
}
// Accent
&.treo-message-type-accent {
.treo-message-container {
background: map-get($accent, default);
color: map-get($accent, default-contrast);
code {
background: map-get($accent, 600);
color: map-get($accent, '600-contrast');
}
}
}
// Warn
&.treo-message-type-warn {
.treo-message-container {
background: map-get($warn, default);
color: map-get($warn, default-contrast);
code {
background: map-get($warn, 800);
color: map-get($warn, '800-contrast');
}
}
}
// Basic
&.treo-message-type-basic {
.treo-message-container {
background: treo-color('cool-gray', 500);
color: treo-color('cool-gray', 50);
code {
background: treo-color('cool-gray', 600);
color: treo-color('cool-gray', 50);
}
}
}
// Info
&.treo-message-type-info {
.treo-message-container {
background: treo-color('blue', 600);
color: treo-color('blue', 50);
code {
background: treo-color('blue', 800);
color: treo-color('blue', 50);
}
}
}
// Success
&.treo-message-type-success {
.treo-message-container {
background: treo-color('green', 500);
color: treo-color('green', 50);
code {
background: treo-color('green', 600);
color: treo-color('green', 50);
}
}
}
// Warning
&.treo-message-type-warning {
.treo-message-container {
background: treo-color('yellow', 400);
color: treo-color('yellow', 50);
code {
background: treo-color('yellow', 600);
color: treo-color('yellow', 50);
}
}
}
// Error
&.treo-message-type-error {
.treo-message-container {
background: treo-color('red', 600);
color: treo-color('red', 50);
code {
background: treo-color('red', 800);
color: treo-color('red', 50);
}
}
}
}
// Outline
&.treo-message-appearance-outline {
// Primary
&.treo-message-type-primary {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: map-get($primary, 300);
box-shadow: inset 0 0 0 1px map-get($primary, 300);
} @else {
background: map-get($primary, 50);
color: map-get($primary, 800);
box-shadow: inset 0 0 0 1px map-get($primary, 400);
}
code {
background: map-get($primary, 200);
color: map-get($primary, 800);
}
}
}
// Accent
&.treo-message-type-accent {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: map-get($accent, 300);
box-shadow: inset 0 0 0 1px map-get($accent, 300);
} @else {
background: map-get($accent, 50);
color: map-get($accent, 800);
box-shadow: inset 0 0 0 1px map-get($accent, 400);
}
code {
background: map-get($accent, 200);
color: map-get($accent, 800);
}
}
}
// Warn
&.treo-message-type-warn {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: map-get($warn, 300);
box-shadow: inset 0 0 0 1px map-get($warn, 300);
} @else {
background: map-get($warn, 50);
color: map-get($warn, 800);
box-shadow: inset 0 0 0 1px map-get($warn, 400);
}
code {
background: map-get($warn, 200);
color: map-get($warn, 800);
}
}
}
// Basic
&.treo-message-type-basic {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('cool-gray', 300);
box-shadow: inset 0 0 0 1px treo-color('cool-gray', 300);
} @else {
background: treo-color('cool-gray', 50);
color: treo-color('cool-gray', 800);
box-shadow: inset 0 0 0 1px treo-color('cool-gray', 400);
}
code {
background: treo-color('cool-gray', 200);
color: treo-color('cool-gray', 800);
}
}
}
// Info
&.treo-message-type-info {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('blue', 300);
box-shadow: inset 0 0 0 1px treo-color('blue', 300);
} @else {
background: treo-color('blue', 50);
color: treo-color('blue', 800);
box-shadow: inset 0 0 0 1px treo-color('blue', 400);
}
code {
background: treo-color('blue', 200);
color: treo-color('blue', 800);
}
}
}
// Success
&.treo-message-type-success {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('green', 300);
box-shadow: inset 0 0 0 1px treo-color('green', 300);
} @else {
background: treo-color('green', 50);
color: treo-color('green', 800);
box-shadow: inset 0 0 0 1px treo-color('green', 400);
}
code {
background: treo-color('green', 200);
color: treo-color('green', 800);
}
}
}
// Warning
&.treo-message-type-warning {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('yellow', 300);
box-shadow: inset 0 0 0 1px treo-color('yellow', 300);
} @else {
background: treo-color('yellow', 50);
color: treo-color('yellow', 800);
box-shadow: inset 0 0 0 1px treo-color('yellow', 400);
}
code {
background: treo-color('yellow', 200);
color: treo-color('yellow', 800);
}
}
}
// Error
&.treo-message-type-error {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('red', 500);
box-shadow: inset 0 0 0 1px treo-color('red', 500);
} @else {
background: treo-color('red', 50);
color: treo-color('red', 800);
box-shadow: inset 0 0 0 1px treo-color('red', 400);
}
code {
background: treo-color('red', 200);
color: treo-color('red', 800);
}
}
}
}
}
}
@@ -0,0 +1,287 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, Renderer2, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { TreoAnimations } from '@treo/animations';
import { TreoMessageAppearance, TreoMessageType } from '@treo/components/message/message.types';
import { TreoMessageService } from '@treo/components/message/message.service';
@Component({
selector : 'treo-message',
templateUrl : './message.component.html',
styleUrls : ['./message.component.scss'],
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
animations : TreoAnimations,
exportAs : 'treoMessage'
})
export class TreoMessageComponent implements OnInit, OnDestroy
{
// Name
@Input()
name: string;
@Output()
readonly afterDismissed: EventEmitter<boolean>;
@Output()
readonly afterShown: EventEmitter<boolean>;
// Private
private _appearance: TreoMessageAppearance;
private _dismissed: null | boolean;
private _showIcon: boolean;
private _type: TreoMessageType;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoMessageService} _treoMessageService
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {ElementRef} _elementRef
* @param {Renderer2} _renderer2
*/
constructor(
private _treoMessageService: TreoMessageService,
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef,
private _renderer2: Renderer2
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.afterDismissed = new EventEmitter<boolean>();
this.afterShown = new EventEmitter<boolean>();
this.appearance = 'fill';
this.dismissed = null;
this.showIcon = true;
this.type = 'primary';
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for appearance
*
* @param value
*/
@Input()
set appearance(value: TreoMessageAppearance)
{
// If the value is the same, return...
if ( this._appearance === value )
{
return;
}
// Update the class name
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-appearance-' + this.appearance);
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-appearance-' + value);
// Store the value
this._appearance = value;
}
get appearance(): TreoMessageAppearance
{
return this._appearance;
}
/**
* Setter and getter for dismissed
*
* @param value
*/
@Input()
set dismissed(value: null | boolean)
{
// If the value is the same, return...
if ( this._dismissed === value )
{
return;
}
// Update the class name
if ( value === null )
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-dismissible');
}
else if ( value === false )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissible');
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissed');
}
else
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissible');
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-dismissed');
}
// Store the value
this._dismissed = value;
}
get dismissed(): null | boolean
{
return this._dismissed;
}
/**
* Setter and getter for show icon
*
* @param value
*/
@Input()
set showIcon(value: boolean)
{
// If the value is the same, return...
if ( this._showIcon === value )
{
return;
}
// Update the class name
if ( value )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-show-icon');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-show-icon');
}
// Store the value
this._showIcon = value;
}
get showIcon(): boolean
{
return this._showIcon;
}
/**
* Setter and getter for type
*
* @param value
*/
@Input()
set type(value: TreoMessageType)
{
// If the value is the same, return...
if ( this._type === value )
{
return;
}
// Update the class name
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-type-' + this.type);
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-type-' + value);
// Store the value
this._type = value;
}
get type(): TreoMessageType
{
return this._type;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Subscribe to the service calls if only
// a name provided for the message box
if ( this.name )
{
// Subscribe to the dismiss calls
this._treoMessageService.onDismiss
.pipe(
filter((name) => this.name === name),
takeUntil(this._unsubscribeAll)
)
.subscribe(() => {
// Dismiss the message box
this.dismiss();
});
// Subscribe to the show calls
this._treoMessageService.onShow
.pipe(
filter((name) => this.name === name),
takeUntil(this._unsubscribeAll)
)
.subscribe(() => {
// Show the message box
this.show();
});
}
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Dismiss the message box
*/
dismiss(): void
{
// Return, if already dismissed
if ( this.dismissed )
{
return;
}
// Dismiss
this.dismissed = true;
// Execute the observable
this.afterDismissed.next(true);
// Notify the change detector
this._changeDetectorRef.markForCheck();
}
/**
* Show the dismissed message box
*/
show(): void
{
// Return, if not dismissed
if ( !this.dismissed )
{
return;
}
// Show
this.dismissed = false;
// Execute the observable
this.afterShown.next(true);
// Notify the change detector
this._changeDetectorRef.markForCheck();
}
}
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { TreoMessageComponent } from '@treo/components/message/message.component';
@NgModule({
declarations: [
TreoMessageComponent
],
imports : [
CommonModule,
MatButtonModule,
MatIconModule
],
exports : [
TreoMessageComponent
]
})
export class TreoMessageModule
{
}
@@ -0,0 +1,81 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class TreoMessageService
{
// Private
private _onDismiss: BehaviorSubject<any>;
private _onShow: BehaviorSubject<any>;
/**
* Constructor
*/
constructor()
{
// Set the private defaults
this._onDismiss = new BehaviorSubject(null);
this._onShow = new BehaviorSubject(null);
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Getter for onDismiss
*/
get onDismiss(): Observable<any>
{
return this._onDismiss.asObservable();
}
/**
* Getter for onShow
*/
get onShow(): Observable<any>
{
return this._onShow.asObservable();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Dismiss the message box
*
* @param name
*/
dismiss(name: string): void
{
// Return, if the name is not provided
if ( !name )
{
return;
}
// Execute the observable
this._onDismiss.next(name);
}
/**
* Show the dismissed message box
*
* @param name
*/
show(name: string): void
{
// Return, if the name is not provided
if ( !name )
{
return;
}
// Execute the observable
this._onShow.next(name);
}
}
@@ -0,0 +1,2 @@
export type TreoMessageAppearance = 'border' | 'fill' | 'outline';
export type TreoMessageType = 'primary' | 'accent' | 'warn' | 'basic' | 'info' | 'success' | 'warning' | 'error';
@@ -0,0 +1,4 @@
export * from '@treo/components/message/message.component';
export * from '@treo/components/message/message.module';
export * from '@treo/components/message/message.service';
export * from '@treo/components/message/message.types';
@@ -0,0 +1,92 @@
<!-- Item wrapper -->
<div class="treo-horizontal-navigation-item-wrapper"
[class.treo-horizontal-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<!-- Item with an internal link -->
<div class="treo-horizontal-navigation-item"
*ngIf="item.link && !item.externalLink && !item.function && !item.disabled"
[routerLink]="[item.link]"
[routerLinkActive]="'treo-horizontal-navigation-item-active'"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item with an external link -->
<a class="treo-horizontal-navigation-item"
*ngIf="item.link && item.externalLink && !item.function && !item.disabled"
[href]="item.link">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with a function -->
<div class="treo-horizontal-navigation-item"
*ngIf="!item.link && item.function && !item.disabled"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item with an internal link and function -->
<div class="treo-horizontal-navigation-item"
*ngIf="item.link && !item.externalLink && item.function && !item.disabled"
[routerLink]="[item.link]"
[routerLinkActive]="'treo-horizontal-navigation-item-active'"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item with an external link and function -->
<a class="treo-horizontal-navigation-item"
*ngIf="item.link && item.externalLink && item.function && !item.disabled"
[href]="item.link"
(click)="item.function(item)"
mat-menu-item>
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with a no link and no function -->
<div class="treo-horizontal-navigation-item"
*ngIf="!item.link && !item.function && !item.disabled">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item is disabled -->
<div class="treo-horizontal-navigation-item treo-horizontal-navigation-item-disabled"
*ngIf="item.disabled">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
</div>
<!-- Item template -->
<ng-template #itemTemplate>
<!-- Icon -->
<mat-icon class="treo-horizontal-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-horizontal-navigation-item-title-wrapper">
<div class="treo-horizontal-navigation-item-title">{{item.title}}</div>
<div class="treo-horizontal-navigation-item-subtitle text-hint"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-horizontal-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-horizontal-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-horizontal-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</ng-template>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-horizontal-navigation-basic-item',
templateUrl : './basic.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoHorizontalNavigationBasicItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,93 @@
<div *ngIf="!child"
[ngClass]="{'treo-horizontal-navigation-menu-active': trigger.menuOpen}"
[matMenuTriggerFor]="matMenu"
(onMenuOpen)="triggerChangeDetection()"
(onMenuClose)="triggerChangeDetection()"
#trigger="matMenuTrigger">
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: item}"></ng-container>
</div>
<mat-menu class="treo-horizontal-navigation-menu-panel"
[overlapTrigger]="false"
#matMenu="matMenu">
<ng-container *ngFor="let item of item.children">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<div class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'basic'"
mat-menu-item>
<treo-horizontal-navigation-basic-item [item]="item"
[name]="name"></treo-horizontal-navigation-basic-item>
</div>
<!-- Branch: aside, collapsable, group -->
<div class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'aside' || item.type === 'collapsable' || item.type === 'group'"
[matMenuTriggerFor]="branch.matMenu"
mat-menu-item>
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: item}"></ng-container>
<treo-horizontal-navigation-branch-item [child]="true"
[item]="item"
[name]="name"
#branch></treo-horizontal-navigation-branch-item>
</div>
<!-- Divider -->
<div class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'divider'"
mat-menu-item>
<treo-horizontal-navigation-divider-item [item]="item"
[name]="name"></treo-horizontal-navigation-divider-item>
</div>
</ng-container>
</ng-container>
</mat-menu>
<!-- Item template -->
<ng-template let-item
#itemTemplate>
<div class="treo-horizontal-navigation-item-wrapper"
[class.treo-horizontal-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<div class="treo-horizontal-navigation-item"
[ngClass]="{'treo-horizontal-navigation-item-disabled': item.disabled}">
<!-- Icon -->
<mat-icon class="treo-horizontal-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-horizontal-navigation-item-title-wrapper">
<div class="treo-horizontal-navigation-item-title">{{item.title}}</div>
<div class="treo-horizontal-navigation-item-subtitle text-hint"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-horizontal-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-horizontal-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-horizontal-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</div>
</div>
</ng-template>
@@ -0,0 +1,99 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatMenu } from '@angular/material/menu';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-horizontal-navigation-branch-item',
templateUrl : './branch.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoHorizontalNavigationBranchItemComponent implements OnInit, OnDestroy
{
// Child
@Input()
child: boolean;
// Item
@Input()
item: TreoNavigationItem;
// Mat menu
@ViewChild('matMenu', {static: true})
matMenu: MatMenu;
// Name
@Input()
name: string;
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.child = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Trigger the change detection
*/
triggerChangeDetection(): void
{
// Mark for check
this._changeDetectorRef.markForCheck();
}
}
@@ -0,0 +1,2 @@
<!-- Divider -->
<div class="treo-horizontal-navigation-item-wrapper divider"></div>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-horizontal-navigation-divider-item',
templateUrl : './divider.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoHorizontalNavigationDividerItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,2 @@
<!-- Spacer -->
<div class="treo-horizontal-navigation-item-wrapper"></div>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-horizontal-navigation-spacer-item',
templateUrl : './spacer.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoHorizontalNavigationSpacerItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,30 @@
<div class="treo-horizontal-navigation-wrapper">
<ng-container *ngFor="let item of navigation">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<treo-horizontal-navigation-basic-item class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-horizontal-navigation-basic-item>
<!-- Branch: aside, collapsable, group -->
<treo-horizontal-navigation-branch-item class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'aside' || item.type === 'collapsable' || item.type === 'group'"
[item]="item"
[name]="name"></treo-horizontal-navigation-branch-item>
<!-- Spacer -->
<treo-horizontal-navigation-spacer-item class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-horizontal-navigation-spacer-item>
</ng-container>
</ng-container>
</div>
@@ -0,0 +1,234 @@
@import 'treo';
// Root navigation specific
treo-horizontal-navigation {
.treo-horizontal-navigation-wrapper {
display: flex;
align-items: center;
// Basic, Branch
treo-horizontal-navigation-basic-item,
treo-horizontal-navigation-branch-item {
.treo-horizontal-navigation-item-wrapper {
border-radius: 4px;
overflow: hidden;
.treo-horizontal-navigation-item {
padding: 0 16px;
cursor: pointer;
user-select: none;
.treo-horizontal-navigation-item-icon {
margin-right: 12px;
}
}
}
}
// Spacer
treo-horizontal-navigation-spacer-item {
margin: 12px 0;
}
}
}
// Menu panel specific
.treo-horizontal-navigation-menu-panel {
.treo-horizontal-navigation-menu-item {
height: auto;
min-height: 0;
line-height: normal;
white-space: normal;
// Basic, Branch
treo-horizontal-navigation-basic-item,
treo-horizontal-navigation-branch-item,
treo-horizontal-navigation-divider-item {
display: flex;
flex: 1 1 auto;
}
// Divider
treo-horizontal-navigation-divider-item {
margin: 8px -16px;
.treo-horizontal-navigation-item-wrapper {
height: 1px;
box-shadow: 0 1px 0 0;
}
}
}
}
// Navigation menu item common
.treo-horizontal-navigation-menu-item {
.treo-horizontal-navigation-item-wrapper {
width: 100%;
&.treo-horizontal-navigation-item-has-subtitle {
.treo-horizontal-navigation-item {
min-height: 56px;
}
}
.treo-horizontal-navigation-item {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
min-height: 48px;
width: 100%;
font-size: 13px;
font-weight: 500;
text-decoration: none;
.treo-horizontal-navigation-item-title-wrapper {
.treo-horizontal-navigation-item-subtitle {
font-size: 12px;
}
}
.treo-horizontal-navigation-item-badge {
margin-left: auto;
.treo-horizontal-navigation-item-badge-content {
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
width: 20px;
height: 20px;
border-radius: 50%;
// Rectangle
&.treo-horizontal-navigation-item-badge-style-rectangle {
width: auto;
min-width: 24px;
height: 20px;
line-height: normal;
padding: 0 6px;
border-radius: 4px;
}
// Rounded
&.treo-horizontal-navigation-item-badge-style-rounded {
width: auto;
min-width: 24px;
height: 20px;
line-height: normal;
padding: 0 10px;
border-radius: 12px;
}
// Simple
&.treo-horizontal-navigation-item-badge-style-simple {
width: auto;
font-size: 11px;
background-color: transparent !important;
}
}
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$primary: map-get($theme, primary);
$is-dark: map-get($theme, is-dark);
// Root navigation specific
treo-horizontal-navigation {
.treo-horizontal-navigation-wrapper {
// Basic, Branch
treo-horizontal-navigation-basic-item,
treo-horizontal-navigation-branch-item {
@include treo-breakpoint('gt-xs') {
&:hover {
.treo-horizontal-navigation-item-wrapper {
background: map-get($background, hover);
}
}
}
}
// Basic - When item active (current link)
treo-horizontal-navigation-basic-item {
.treo-horizontal-navigation-item-active {
.treo-horizontal-navigation-item-title {
color: map-get($primary, default) !important;
}
.treo-horizontal-navigation-item-subtitle {
@if ($is-dark) {
color: map-get($primary, 600) !important;
} @else {
color: map-get($primary, 400) !important;
}
}
.treo-horizontal-navigation-item-icon {
color: map-get($primary, default) !important;
}
}
}
// Branch - When menu open
treo-horizontal-navigation-branch-item {
.treo-horizontal-navigation-menu-active {
.treo-horizontal-navigation-item-wrapper {
background: map-get($background, hover);
}
}
}
}
}
// Navigation menu item common
.treo-horizontal-navigation-menu-item {
// Basic - When item active (current link)
treo-horizontal-navigation-basic-item {
.treo-horizontal-navigation-item-active {
.treo-horizontal-navigation-item-title {
color: map-get($primary, default) !important;
}
.treo-horizontal-navigation-item-subtitle {
@if ($is-dark) {
color: map-get($primary, 600) !important;
} @else {
color: map-get($primary, 400) !important;
}
}
.treo-horizontal-navigation-item-icon {
color: map-get($primary, default) !important;
}
}
}
}
}
@@ -0,0 +1,120 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { TreoAnimations } from '@treo/animations';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
@Component({
selector : 'treo-horizontal-navigation',
templateUrl : './horizontal.component.html',
styleUrls : ['./horizontal.component.scss'],
animations : TreoAnimations,
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs : 'treoHorizontalNavigation'
})
export class TreoHorizontalNavigationComponent implements OnInit, OnDestroy
{
onRefreshed: BehaviorSubject<boolean | null>;
// Name
@Input()
name: string;
// Private
private _navigation: TreoNavigationItem[];
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.onRefreshed = new BehaviorSubject(null);
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter & getter for data
*/
@Input()
set navigation(value: TreoNavigationItem[])
{
// Store the data
this._navigation = value;
// Mark for check
this._changeDetectorRef.markForCheck();
}
get navigation(): TreoNavigationItem[]
{
return this._navigation;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Register the navigation component
this._treoNavigationService.registerComponent(this.name, this);
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Deregister the navigation component from the registry
this._treoNavigationService.deregisterComponent(this.name);
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Refresh the component to apply the changes
*/
refresh(): void
{
// Mark for check
this._changeDetectorRef.markForCheck();
// Execute the observable
this.onRefreshed.next(true);
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}
@@ -0,0 +1 @@
export * from '@treo/components/navigation/public-api';
@@ -0,0 +1,55 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
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 { MatTooltipModule } from '@angular/material/tooltip';
import { TreoScrollbarModule } from '@treo/directives/scrollbar/public-api';
import { TreoHorizontalNavigationBasicItemComponent } from '@treo/components/navigation/horizontal/components/basic/basic.component';
import { TreoHorizontalNavigationBranchItemComponent } from '@treo/components/navigation/horizontal/components/branch/branch.component';
import { TreoHorizontalNavigationDividerItemComponent } from '@treo/components/navigation/horizontal/components/divider/divider.component';
import { TreoHorizontalNavigationSpacerItemComponent } from '@treo/components/navigation/horizontal/components/spacer/spacer.component';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoVerticalNavigationAsideItemComponent } from '@treo/components/navigation/vertical/components/aside/aside.component';
import { TreoVerticalNavigationBasicItemComponent } from '@treo/components/navigation/vertical/components/basic/basic.component';
import { TreoVerticalNavigationCollapsableItemComponent } from '@treo/components/navigation/vertical/components/collapsable/collapsable.component';
import { TreoVerticalNavigationDividerItemComponent } from '@treo/components/navigation/vertical/components/divider/divider.component';
import { TreoVerticalNavigationGroupItemComponent } from '@treo/components/navigation/vertical/components/group/group.component';
import { TreoVerticalNavigationSpacerItemComponent } from '@treo/components/navigation/vertical/components/spacer/spacer.component';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
@NgModule({
declarations: [
TreoHorizontalNavigationBasicItemComponent,
TreoHorizontalNavigationBranchItemComponent,
TreoHorizontalNavigationDividerItemComponent,
TreoHorizontalNavigationSpacerItemComponent,
TreoHorizontalNavigationComponent,
TreoVerticalNavigationAsideItemComponent,
TreoVerticalNavigationBasicItemComponent,
TreoVerticalNavigationCollapsableItemComponent,
TreoVerticalNavigationDividerItemComponent,
TreoVerticalNavigationGroupItemComponent,
TreoVerticalNavigationSpacerItemComponent,
TreoVerticalNavigationComponent
],
imports : [
CommonModule,
RouterModule,
MatButtonModule,
MatDividerModule,
MatIconModule,
MatMenuModule,
MatTooltipModule,
TreoScrollbarModule
],
exports : [
TreoHorizontalNavigationComponent,
TreoVerticalNavigationComponent
]
})
export class TreoNavigationModule
{
}
@@ -0,0 +1,192 @@
import { Injectable } from '@angular/core';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Injectable({
providedIn: 'root'
})
export class TreoNavigationService
{
// Private
private _componentRegistry: Map<string, any>;
private _navigationStore: Map<string, TreoNavigationItem[]>;
/**
* Constructor
*/
constructor()
{
// Set the private defaults
this._componentRegistry = new Map<string, any>();
this._navigationStore = new Map<string, any>();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Register navigation component
*
* @param name
* @param component
*/
registerComponent(name: string, component: any): void
{
this._componentRegistry.set(name, component);
}
/**
* Deregister navigation component
*
* @param name
*/
deregisterComponent(name: string): void
{
this._componentRegistry.delete(name);
}
/**
* Get navigation component from the registry
*
* @param name
*/
getComponent(name: string): any
{
return this._componentRegistry.get(name);
}
/**
* Store the given navigation with the given key
*
* @param key
* @param navigation
*/
storeNavigation(key: string, navigation: TreoNavigationItem[]): void
{
// Add to the store
this._navigationStore.set(key, navigation);
}
/**
* Get navigation from storage by key
*
* @param key
* @returns {any}
*/
getNavigation(key: string): TreoNavigationItem[]
{
return this._navigationStore.get(key);
}
/**
* Delete the navigation from the storage
*
* @param key
*/
deleteNavigation(key: string): void
{
// Check if the navigation exists
if ( !this._navigationStore.has(key) )
{
console.warn(`Navigation with the key '${key}' does not exist in the store.`);
}
// Delete from the storage
this._navigationStore.delete(key);
}
/**
* Utility function that returns a flattened
* version of the given navigation array
*
* @param navigation
* @param flatNavigation
* @returns {TreoNavigationItem[]}
*/
getFlatNavigation(navigation: TreoNavigationItem[], flatNavigation: TreoNavigationItem[] = []): TreoNavigationItem[]
{
for ( const item of navigation )
{
if ( item.type === 'basic' )
{
flatNavigation.push(item);
continue;
}
if ( item.type === 'aside' || item.type === 'collapsable' || item.type === 'group' )
{
if ( item.children )
{
this.getFlatNavigation(item.children, flatNavigation);
}
}
}
return flatNavigation;
}
/**
* Utility function that returns the item
* with the given id from given navigation
*
* @param id
* @param navigation
*/
getItem(id: string, navigation: TreoNavigationItem[]): TreoNavigationItem | null
{
for ( const item of navigation )
{
if ( item.id === id )
{
return item;
}
if ( item.children )
{
const childItem = this.getItem(id, item.children);
if ( childItem )
{
return childItem;
}
}
}
return null;
}
/**
* Utility function that returns the item's parent
* with the given id from given navigation
*
* @param id
* @param navigation
* @param parent
*/
getItemParent(
id: string,
navigation: TreoNavigationItem[],
parent: TreoNavigationItem[] | TreoNavigationItem
): TreoNavigationItem[] | TreoNavigationItem | null
{
for ( const item of navigation )
{
if ( item.id === id )
{
return parent;
}
if ( item.children )
{
const childItem = this.getItemParent(id, item.children, item);
if ( childItem )
{
return childItem;
}
}
}
return null;
}
}
@@ -0,0 +1,28 @@
export interface TreoNavigationItem
{
id?: string;
title?: string;
subtitle?: string;
type: 'aside' | 'basic' | 'collapsable' | 'divider' | 'group' | 'spacer';
hidden?: (item: TreoNavigationItem) => boolean;
disabled?: boolean;
link?: string;
externalLink?: boolean;
exactMatch?: boolean;
function?: (item: TreoNavigationItem) => void;
classes?: string;
icon?: string;
iconClasses?: string;
badge?: {
title?: string;
style?: 'rectangle' | 'rounded' | 'simple',
background?: string;
color?: string;
};
children?: TreoNavigationItem[];
meta?: any;
}
export type TreoVerticalNavigationAppearance = string;
export type TreoVerticalNavigationMode = 'over' | 'side';
export type TreoVerticalNavigationPosition = 'left' | 'right';
@@ -0,0 +1,5 @@
export * from '@treo/components/navigation/horizontal/horizontal.component';
export * from '@treo/components/navigation/vertical/vertical.component';
export * from '@treo/components/navigation/navigation.module';
export * from '@treo/components/navigation/navigation.service';
export * from '@treo/components/navigation/navigation.types';
@@ -0,0 +1,83 @@
<div class="treo-vertical-navigation-item-wrapper"
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<div class="treo-vertical-navigation-item"
[ngClass]="{'treo-vertical-navigation-item-active': active, 'treo-vertical-navigation-item-disabled': item.disabled}">
<!-- Icon -->
<mat-icon class="treo-vertical-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-vertical-navigation-item-title-wrapper">
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
<div class="treo-vertical-navigation-item-subtitle"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-vertical-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-vertical-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</div>
</div>
<ng-container *ngIf="!skipChildren">
<div class="treo-vertical-navigation-item-children">
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-vertical-navigation-basic-item>
<!-- Collapsable -->
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
<!-- Divider -->
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
[item]="item"
[name]="name"></treo-vertical-navigation-divider-item>
<!-- Group -->
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
[item]="item"
[name]="name"></treo-vertical-navigation-group-item>
<!-- Spacer -->
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-vertical-navigation-spacer-item>
</ng-container>
</ng-container>
</div>
</ng-container>
@@ -0,0 +1,104 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-aside-item',
templateUrl : './aside.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationAsideItemComponent implements OnInit, OnDestroy
{
// Active
@Input()
active: boolean;
// Auto collapse
@Input()
autoCollapse: boolean;
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Skip children
@Input()
skipChildren: boolean;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.skipChildren = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}
@@ -0,0 +1,91 @@
<!-- Item wrapper -->
<div class="treo-vertical-navigation-item-wrapper"
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<!-- Item with an internal link -->
<a class="treo-vertical-navigation-item"
*ngIf="item.link && !item.externalLink && !item.function && !item.disabled"
[routerLink]="[item.link]"
[routerLinkActive]="'treo-vertical-navigation-item-active'"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with an external link -->
<a class="treo-vertical-navigation-item"
*ngIf="item.link && item.externalLink && !item.function && !item.disabled"
[href]="item.link">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with a function -->
<div class="treo-vertical-navigation-item"
*ngIf="!item.link && item.function && !item.disabled"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item with an internal link and function -->
<a class="treo-vertical-navigation-item"
*ngIf="item.link && !item.externalLink && item.function && !item.disabled"
[routerLink]="[item.link]"
[routerLinkActive]="'treo-vertical-navigation-item-active'"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with an external link and function -->
<a class="treo-vertical-navigation-item"
*ngIf="item.link && item.externalLink && item.function && !item.disabled"
[href]="item.link"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with a no link and no function -->
<div class="treo-vertical-navigation-item"
*ngIf="!item.link && !item.function && !item.disabled">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item is disabled -->
<div class="treo-vertical-navigation-item treo-vertical-navigation-item-disabled"
*ngIf="item.disabled">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
</div>
<!-- Item template -->
<ng-template #itemTemplate>
<!-- Icon -->
<mat-icon class="treo-vertical-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-vertical-navigation-item-title-wrapper">
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
<div class="treo-vertical-navigation-item-subtitle"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-vertical-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-vertical-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</ng-template>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-basic-item',
templateUrl : './basic.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationBasicItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,85 @@
<div class="treo-vertical-navigation-item-wrapper"
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<div class="treo-vertical-navigation-item"
[ngClass]="{'treo-vertical-navigation-item-disabled': item.disabled}"
(click)="toggleCollapsable()">
<!-- Icon -->
<mat-icon class="treo-vertical-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-vertical-navigation-item-title-wrapper">
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
<div class="treo-vertical-navigation-item-subtitle"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-vertical-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-vertical-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
<!-- Arrow -->
<mat-icon class="treo-vertical-navigation-item-arrow icon-size-16"
[svgIcon]="'heroicons_solid:cheveron-right'"></mat-icon>
</div>
</div>
<div class="treo-vertical-navigation-item-children"
*ngIf="!isCollapsed"
@expandCollapse>
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-vertical-navigation-basic-item>
<!-- Collapsable -->
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
<!-- Divider -->
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
[item]="item"
[name]="name"></treo-vertical-navigation-divider-item>
<!-- Group -->
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
[item]="item"
[name]="name"></treo-vertical-navigation-group-item>
<!-- Spacer -->
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-vertical-navigation-spacer-item>
</ng-container>
</ng-container>
</div>
@@ -0,0 +1,363 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { TreoAnimations } from '@treo/animations';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-collapsable-item',
templateUrl : './collapsable.component.html',
styles : [],
animations : TreoAnimations,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationCollapsableItemComponent implements OnInit, OnDestroy
{
// Auto collapse
@Input()
autoCollapse: boolean;
// Item
@Input()
item: TreoNavigationItem;
// Collapsed
@HostBinding('class.treo-vertical-navigation-item-collapsed')
isCollapsed: boolean;
// Expanded
@HostBinding('class.treo-vertical-navigation-item-expanded')
isExpanded: boolean;
// Name
@Input()
name: string;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {Router} _router
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef,
private _router: Router
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.isCollapsed = true;
this.isExpanded = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// If the item has a children that has a matching url with the current url, expand...
if ( this._hasCurrentUrlInChildren(this.item, this._router.url) )
{
this.expand();
}
// Otherwise...
else
{
// If the autoCollapse is on, collapse...
if ( this.autoCollapse )
{
this.collapse();
}
}
// Listen for the onCollapsableItemCollapsed from the service
this._treoVerticalNavigationComponent.onCollapsableItemCollapsed
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((collapsedItem) => {
// Check if the collapsed item is null
if ( collapsedItem === null )
{
return;
}
// Collapse if this is a children of the collapsed item
if ( this._isChildrenOf(collapsedItem, this.item) )
{
this.collapse();
}
});
// Listen for the onCollapsableItemExpanded from the service if the autoCollapse is on
if ( this.autoCollapse )
{
this._treoVerticalNavigationComponent.onCollapsableItemExpanded
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((expandedItem) => {
// Check if the expanded item is null
if ( expandedItem === null )
{
return;
}
// Check if this is a parent of the expanded item
if ( this._isChildrenOf(this.item, expandedItem) )
{
return;
}
// Check if this has a children with a matching url with the current active url
if ( this._hasCurrentUrlInChildren(this.item, this._router.url) )
{
return;
}
// Check if this is the expanded item
if ( this.item === expandedItem )
{
return;
}
// If none of the above conditions are matched, collapse this item
this.collapse();
});
}
// Attach a listener to the NavigationEnd event
this._router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this._unsubscribeAll)
)
.subscribe((event: NavigationEnd) => {
// If the item has a children that has a matching url with the current url, expand...
if ( this._hasCurrentUrlInChildren(this.item, event.urlAfterRedirects) )
{
this.expand();
}
// Otherwise...
else
{
// If the autoCollapse is on, collapse...
if ( this.autoCollapse )
{
this.collapse();
}
}
});
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Check if the given item has the given url
* in one of its children
*
* @param item
* @param url
* @private
*/
private _hasCurrentUrlInChildren(item, url): boolean
{
const children = item.children;
if ( !children )
{
return false;
}
for ( const child of children )
{
if ( child.children )
{
if ( this._hasCurrentUrlInChildren(child, url) )
{
return true;
}
}
// Check if the item's link is the exact same of the
// current url
if ( child.link === url )
{
return true;
}
// If exactMatch is not set for the item, also check
// if the current url starts with the item's link and
// continues with a question mark, a pound sign or a
// slash
if ( !child.exactMatch && (child.link === url || url.startsWith(child.link + '?') || url.startsWith(child.link + '#') || url.startsWith(child.link + '/')) )
{
return true;
}
}
return false;
}
/**
* Check if this is a children
* of the given item
*
* @param parent
* @param item
* @return {boolean}
* @private
*/
private _isChildrenOf(parent, item): boolean
{
const children = parent.children;
if ( !children )
{
return false;
}
if ( children.indexOf(item) > -1 )
{
return true;
}
for ( const child of children )
{
if ( child.children )
{
if ( this._isChildrenOf(child, item) )
{
return true;
}
}
}
return false;
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Collapse
*/
collapse(): void
{
// Return if the item is disabled
if ( this.item.disabled )
{
return;
}
// Return if the item is already collapsed
if ( this.isCollapsed )
{
return;
}
// Collapse it
this.isCollapsed = true;
this.isExpanded = false;
// Mark for check
this._changeDetectorRef.markForCheck();
// Execute the observable
this._treoVerticalNavigationComponent.onCollapsableItemCollapsed.next(this.item);
}
/**
* Expand
*/
expand(): void
{
// Return if the item is disabled
if ( this.item.disabled )
{
return;
}
// Return if the item is already expanded
if ( !this.isCollapsed )
{
return;
}
// Expand it
this.isCollapsed = false;
this.isExpanded = true;
// Mark for check
this._changeDetectorRef.markForCheck();
// Execute the observable
this._treoVerticalNavigationComponent.onCollapsableItemExpanded.next(this.item);
}
/**
* Toggle collapsable
*/
toggleCollapsable(): void
{
// Toggle collapse/expand
if ( this.isCollapsed )
{
this.expand();
}
else
{
this.collapse();
}
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}
@@ -0,0 +1,2 @@
<!-- Divider -->
<div class="treo-vertical-navigation-item-wrapper divider"></div>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-divider-item',
templateUrl : './divider.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationDividerItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,74 @@
<!-- Item wrapper -->
<div class="treo-vertical-navigation-item-wrapper"
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<div class="treo-vertical-navigation-item">
<!-- Icon -->
<mat-icon class="treo-vertical-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-vertical-navigation-item-title-wrapper">
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
<div class="treo-vertical-navigation-item-subtitle"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-vertical-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-vertical-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</div>
</div>
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-vertical-navigation-basic-item>
<!-- Collapsable -->
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
<!-- Divider -->
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
[item]="item"
[name]="name"></treo-vertical-navigation-divider-item>
<!-- Group -->
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
[item]="item"
[name]="name"></treo-vertical-navigation-group-item>
<!-- Spacer -->
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-vertical-navigation-spacer-item>
</ng-container>
</ng-container>
@@ -0,0 +1,93 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-group-item',
templateUrl : './group.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationGroupItemComponent implements OnInit, OnDestroy
{
// Auto collapse
@Input()
autoCollapse: boolean;
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}
@@ -0,0 +1,2 @@
<!-- Spacer -->
<div class="treo-vertical-navigation-item-wrapper"></div>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-spacer-item',
templateUrl : './spacer.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationSpacerItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,105 @@
<div class="treo-vertical-navigation-wrapper">
<!-- Header -->
<div class="treo-vertical-navigation-header">
<ng-content select="[treoVerticalNavigationHeader]"></ng-content>
</div>
<!-- Content -->
<div class="treo-vertical-navigation-content"
treoScrollbar
[treoScrollbarOptions]="{wheelPropagation: inner, suppressScrollX: true}"
#navigationContent>
<!-- Content header -->
<div class="treo-vertical-navigation-content-header">
<ng-content select="[treoVerticalNavigationContentHeader]"></ng-content>
</div>
<!-- Items -->
<ng-container *ngFor="let item of navigation; trackBy: trackByFn">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Aside -->
<treo-vertical-navigation-aside-item *ngIf="item.type === 'aside'"
[item]="item"
[name]="name"
[skipChildren]="true"
[autoCollapse]="autoCollapse"
[active]="item.id === activeAsideItemId"
(click)="toggleAside(item)"></treo-vertical-navigation-aside-item>
<!-- Basic -->
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-vertical-navigation-basic-item>
<!-- Collapsable -->
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
<!-- Divider -->
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
[item]="item"
[name]="name"></treo-vertical-navigation-divider-item>
<!-- Group -->
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-group-item>
<!-- Spacer -->
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-vertical-navigation-spacer-item>
</ng-container>
</ng-container>
<!-- Content footer -->
<div class="treo-vertical-navigation-content-footer">
<ng-content select="[treoVerticalNavigationContentFooter]"></ng-content>
</div>
</div>
<!-- Footer -->
<div class="treo-vertical-navigation-footer">
<ng-content select="[treoVerticalNavigationFooter]"></ng-content>
</div>
</div>
<!-- Aside -->
<div class="treo-vertical-navigation-aside-wrapper"
*ngIf="activeAsideItemId"
treoScrollbar
[treoScrollbarOptions]="{wheelPropagation: false, suppressScrollX: true}"
[@fadeInLeft]="position === 'left'"
[@fadeInRight]="position === 'right'"
[@fadeOutLeft]="position === 'left'"
[@fadeOutRight]="position === 'right'">
<!-- Items -->
<ng-container *ngFor="let item of navigation; trackBy: trackByFn">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Aside -->
<treo-vertical-navigation-aside-item *ngIf="item.type === 'aside' && item.id === activeAsideItemId"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-aside-item>
</ng-container>
</ng-container>
</div>
@@ -0,0 +1,793 @@
@import 'treo';
$treo-vertical-navigation-width: 280;
treo-vertical-navigation {
position: sticky;
display: flex;
flex-direction: column;
flex: 1 0 auto;
top: 0;
width: #{$treo-vertical-navigation-width}px;
min-width: #{$treo-vertical-navigation-width}px;
max-width: #{$treo-vertical-navigation-width}px;
height: 100vh;
min-height: 100vh;
max-height: 100vh;
z-index: 200;
// -----------------------------------------------------------------------------------------------------
// @ Navigation Drawer
// -----------------------------------------------------------------------------------------------------
// Animations
&.treo-vertical-navigation-animations-enabled {
transition-duration: 400ms;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
transition-property: visibility, margin-left, margin-right, transform, width, max-width, min-width;
// Wrapper
.treo-vertical-navigation-wrapper {
transition-duration: 400ms;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
transition-property: width, max-width, min-width;
}
}
// Over mode
&.treo-vertical-navigation-mode-over {
position: fixed;
top: 0;
bottom: 0;
}
// Left position
&.treo-vertical-navigation-position-left {
// Side mode
&.treo-vertical-navigation-mode-side {
margin-left: -#{$treo-vertical-navigation-width}px;
&.treo-vertical-navigation-opened {
margin-left: 0;
}
}
// Over mode
&.treo-vertical-navigation-mode-over {
left: 0;
transform: translate3d(-100%, 0, 0);
&.treo-vertical-navigation-opened {
transform: translate3d(0, 0, 0);
}
}
// Wrapper
.treo-vertical-navigation-wrapper {
left: 0;
}
}
// Right position
&.treo-vertical-navigation-position-right {
// Side mode
&.treo-vertical-navigation-mode-side {
margin-right: -#{$treo-vertical-navigation-width}px;
&.treo-vertical-navigation-opened {
margin-right: 0;
}
}
// Over mode
&.treo-vertical-navigation-mode-over {
right: 0;
transform: translate3d(100%, 0, 0);
&.treo-vertical-navigation-opened {
transform: translate3d(0, 0, 0);
}
}
// Wrapper
.treo-vertical-navigation-wrapper {
right: 0;
}
}
// Wrapper
.treo-vertical-navigation-wrapper {
position: absolute;
display: flex;
flex: 1 1 auto;
flex-direction: column;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
border-right-width: 1px;
overflow: hidden;
z-index: 10;
// Header
.treo-vertical-navigation-header {
}
// Content
.treo-vertical-navigation-content {
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
// Divider
> treo-vertical-navigation-divider-item {
margin: 24px 0;
}
// Group
> treo-vertical-navigation-group-item {
margin-top: 24px;
}
}
// Footer
.treo-vertical-navigation-footer {
}
}
// Aside wrapper
.treo-vertical-navigation-aside-wrapper {
position: absolute;
display: flex;
flex: 1 1 auto;
flex-direction: column;
top: 0;
bottom: 0;
left: #{$treo-vertical-navigation-width}px;
width: #{$treo-vertical-navigation-width}px;
height: 100%;
z-index: 5;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
transition-duration: 400ms;
transition-property: left, right;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
> treo-vertical-navigation-aside-item {
padding: 24px 0;
// First item of the aside
> .treo-vertical-navigation-item-wrapper {
display: none !important;
}
}
}
&.treo-vertical-navigation-position-right {
.treo-vertical-navigation-aside-wrapper {
left: auto;
right: #{$treo-vertical-navigation-width}px;
}
}
// -----------------------------------------------------------------------------------------------------
// @ Navigation Items
// -----------------------------------------------------------------------------------------------------
// Navigation items common
treo-vertical-navigation-aside-item,
treo-vertical-navigation-basic-item,
treo-vertical-navigation-collapsable-item,
treo-vertical-navigation-divider-item,
treo-vertical-navigation-group-item,
treo-vertical-navigation-spacer-item {
display: flex;
flex-direction: column;
flex: 1 0 auto;
user-select: none;
.treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 12px 24px;
font-size: 13px;
font-weight: 500;
line-height: 20px;
text-decoration: none;
transition: background-color 375ms cubic-bezier(0.25, 0.8, 0.25, 1);
.treo-vertical-navigation-item-icon {
margin-right: 16px;
transition: color 375ms cubic-bezier(0.25, 0.8, 0.25, 1);
}
.treo-vertical-navigation-item-title-wrapper {
.treo-vertical-navigation-item-title {
transition: color 375ms cubic-bezier(0.25, 0.8, 0.25, 1);
}
.treo-vertical-navigation-item-subtitle {
font-size: 11px;
line-height: 1.5;
transition: color 375ms cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
.treo-vertical-navigation-item-badge {
margin-left: auto;
.treo-vertical-navigation-item-badge-content {
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
width: 20px;
height: 20px;
border-radius: 50%;
// Rectangle
&.treo-vertical-navigation-item-badge-style-rectangle {
width: auto;
min-width: 24px;
height: 20px;
line-height: normal;
padding: 0 6px;
border-radius: 4px;
}
// Rounded
&.treo-vertical-navigation-item-badge-style-rounded {
width: auto;
min-width: 24px;
height: 20px;
line-height: normal;
padding: 0 10px;
border-radius: 12px;
}
// Simple
&.treo-vertical-navigation-item-badge-style-simple {
width: auto;
font-size: 11px;
background-color: transparent !important;
}
}
}
}
}
}
treo-vertical-navigation-aside-item,
treo-vertical-navigation-basic-item,
treo-vertical-navigation-collapsable-item {
.treo-vertical-navigation-item {
cursor: pointer;
}
}
// Aside
treo-vertical-navigation-aside-item {
}
// Basic
treo-vertical-navigation-basic-item {
}
// Collapsable
treo-vertical-navigation-collapsable-item {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
.treo-vertical-navigation-item-badge {
+ .treo-vertical-navigation-item-arrow {
margin-left: 8px;
}
}
.treo-vertical-navigation-item-arrow {
height: 20px;
line-height: 20px;
margin-left: auto;
transition: transform 300ms cubic-bezier(0.25, 0.8, 0.25, 1),
color 375ms cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
}
&.treo-vertical-navigation-item-expanded {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
.treo-vertical-navigation-item-arrow {
transform: rotate(90deg);
}
}
}
}
> .treo-vertical-navigation-item-children {
> *:last-child {
padding-bottom: 6px;
> .treo-vertical-navigation-item-children {
> *:last-child {
padding-bottom: 0;
}
}
}
.treo-vertical-navigation-item {
padding: 10px 24px;
}
}
// 1st level
.treo-vertical-navigation-item-children {
overflow: hidden;
.treo-vertical-navigation-item {
padding-left: 64px;
}
// 2nd level
.treo-vertical-navigation-item-children {
.treo-vertical-navigation-item {
padding-left: 80px;
}
// 3rd level
.treo-vertical-navigation-item-children {
.treo-vertical-navigation-item {
padding-left: 96px;
}
// 4th level
.treo-vertical-navigation-item-children {
.treo-vertical-navigation-item {
padding-left: 112px;
}
}
}
}
}
}
// Divider
treo-vertical-navigation-divider-item {
margin: 12px 0;
.treo-vertical-navigation-item-wrapper {
height: 1px;
box-shadow: 0 1px 0 0;
}
}
// Group
treo-vertical-navigation-group-item {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
.treo-vertical-navigation-item-badge,
.treo-vertical-navigation-item-icon {
display: none !important;
}
.treo-vertical-navigation-item-title {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
}
}
}
}
// Spacer
treo-vertical-navigation-spacer-item {
margin: 6px 0;
}
// -----------------------------------------------------------------------------------------------------
// @ [inner]
// -----------------------------------------------------------------------------------------------------
&.treo-vertical-navigation-inner {
position: relative;
width: auto;
min-width: 0;
max-width: none;
height: auto;
min-height: 0;
max-height: none;
box-shadow: none;
.treo-vertical-navigation-wrapper {
position: relative;
overflow: visible;
height: auto;
.treo-vertical-navigation-content {
overflow: visible !important;
}
}
}
}
// Overlay
.treo-vertical-navigation-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 170;
opacity: 0;
background-color: rgba(0, 0, 0, 0.6);
+ .treo-vertical-navigation-aside-overlay {
background-color: transparent;
}
}
// Aside overlay
.treo-vertical-navigation-aside-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 169;
opacity: 0;
background-color: rgba(0, 0, 0, 0.3);
}
// -----------------------------------------------------------------------------------------------------
// @ 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);
treo-vertical-navigation {
// Wrapper
.treo-vertical-navigation-wrapper {
background: inherit;
}
// Aside wrapper
.treo-vertical-navigation-aside-wrapper {
background: inherit;
}
// Navigation items common
.treo-vertical-navigation-item {
color: currentColor;
// Normal state
.treo-vertical-navigation-item-icon {
color: treo-color('cool-gray', 400);
}
.treo-vertical-navigation-item-title {
@if ($is-dark) {
color: treo-color('cool-gray', 300);
} @else {
color: treo-color('cool-gray', 600);
}
}
.treo-vertical-navigation-item-subtitle {
@if ($is-dark) {
color: treo-color('cool-gray', 400);
} @else {
color: treo-color('cool-gray', 500);
}
}
// Active state
&.treo-vertical-navigation-item-active:not(.treo-vertical-navigation-item-disabled) {
@if ($is-dark) {
background-color: rgba(0, 0, 0, 0.25);
} @else {
background-color: treo-color('cool-gray', 100);
}
.treo-vertical-navigation-item-icon {
@if ($is-dark) {
color: treo-color('cool-gray', 100);
} @else {
color: treo-color('cool-gray', 500);
}
}
.treo-vertical-navigation-item-title {
@if ($is-dark) {
color: treo-color('cool-gray', 50);
} @else {
color: treo-color('cool-gray', 900);
}
}
.treo-vertical-navigation-item-subtitle {
@if ($is-dark) {
color: treo-color('cool-gray', 300);
} @else {
color: treo-color('cool-gray', 700);
}
}
}
// Disabled state
&.treo-vertical-navigation-item-disabled {
cursor: default;
.treo-vertical-navigation-item-icon,
.treo-vertical-navigation-item-title,
.treo-vertical-navigation-item-subtitle,
.treo-vertical-navigation-item-arrow {
@if ($is-dark) {
color: treo-color('cool-gray', 600);
} @else {
color: treo-color('cool-gray', 300);
}
}
}
}
// Aside, Basic, Collapsable
treo-vertical-navigation-aside-item,
treo-vertical-navigation-basic-item,
treo-vertical-navigation-collapsable-item {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
// Hover state
&:hover:not(.treo-vertical-navigation-item-active):not(.treo-vertical-navigation-item-disabled) {
@if ($is-dark) {
background-color: rgba(0, 0, 0, 0.25);
} @else {
background-color: treo-color('gray', 50);
}
.treo-vertical-navigation-item-icon {
@if ($is-dark) {
color: treo-color('cool-gray', 100);
} @else {
color: treo-color('cool-gray', 500);
}
}
.treo-vertical-navigation-item-title,
.treo-vertical-navigation-item-arrow {
@if ($is-dark) {
color: treo-color('cool-gray', 50);
} @else {
color: treo-color('cool-gray', 900);
}
}
.treo-vertical-navigation-item-subtitle {
@if ($is-dark) {
color: treo-color('cool-gray', 300);
} @else {
color: treo-color('cool-gray', 700);
}
}
}
}
}
}
// Collapsable - Expanded state
treo-vertical-navigation-collapsable-item {
&.treo-vertical-navigation-item-expanded {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
.treo-vertical-navigation-item-icon {
@if ($is-dark) {
color: treo-color('cool-gray', 100);
} @else {
color: treo-color('cool-gray', 500);
}
}
.treo-vertical-navigation-item-title,
.treo-vertical-navigation-item-arrow {
@if ($is-dark) {
color: treo-color('cool-gray', 50);
} @else {
color: treo-color('cool-gray', 900);
}
}
.treo-vertical-navigation-item-subtitle {
@if ($is-dark) {
color: treo-color('cool-gray', 300);
} @else {
color: treo-color('cool-gray', 700);
}
}
}
}
}
}
// Group - Normal state
treo-vertical-navigation-group-item {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
.treo-vertical-navigation-item-icon {
color: treo-color('cool-gray', 400);
}
.treo-vertical-navigation-item-title {
@if ($is-dark) {
color: map-get($primary, 400);
} @else {
color: map-get($primary, 600);
}
}
.treo-vertical-navigation-item-subtitle {
color: treo-color('cool-gray', 500);
}
}
}
}
// DARK THEME
&.theme-dark {
// Navigation items common
.treo-vertical-navigation-item {
.treo-vertical-navigation-item-title {
color: treo-color('cool-gray', 300);
}
.treo-vertical-navigation-item-subtitle {
color: treo-color('cool-gray', 400);
}
// Active state
&.treo-vertical-navigation-item-active:not(.treo-vertical-navigation-item-disabled) {
background-color: rgba(0, 0, 0, 0.25);
.treo-vertical-navigation-item-icon {
color: treo-color('cool-gray', 100);
}
.treo-vertical-navigation-item-title {
color: treo-color('cool-gray', 50);
}
.treo-vertical-navigation-item-subtitle {
color: treo-color('cool-gray', 300);
}
}
// Disabled state
&.treo-vertical-navigation-item-disabled {
cursor: default;
.treo-vertical-navigation-item-icon,
.treo-vertical-navigation-item-title,
.treo-vertical-navigation-item-subtitle,
.treo-vertical-navigation-item-arrow {
color: treo-color('cool-gray', 600);
}
}
}
// Aside, Basic, Collapsable
treo-vertical-navigation-aside-item,
treo-vertical-navigation-basic-item,
treo-vertical-navigation-collapsable-item {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
// Hover state
&:hover:not(.treo-vertical-navigation-item-active):not(.treo-vertical-navigation-item-disabled) {
background-color: rgba(0, 0, 0, 0.25);
.treo-vertical-navigation-item-icon {
color: treo-color('cool-gray', 100);
}
.treo-vertical-navigation-item-title,
.treo-vertical-navigation-item-arrow {
color: treo-color('cool-gray', 50);
}
.treo-vertical-navigation-item-subtitle {
color: treo-color('cool-gray', 300);
}
}
}
}
}
// Collapsable - Expanded state
treo-vertical-navigation-collapsable-item {
&.treo-vertical-navigation-item-expanded {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
.treo-vertical-navigation-item-icon {
color: treo-color('cool-gray', 100);
}
.treo-vertical-navigation-item-title,
.treo-vertical-navigation-item-arrow {
color: treo-color('cool-gray', 50);
}
.treo-vertical-navigation-item-subtitle {
color: treo-color('cool-gray', 300);
}
}
}
}
}
// Group - Normal state
treo-vertical-navigation-group-item {
> .treo-vertical-navigation-item-wrapper {
.treo-vertical-navigation-item {
.treo-vertical-navigation-item-title {
color: map-get($primary, 400);
}
}
}
}
}
}
}
@@ -0,0 +1,890 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnDestroy, OnInit, Output, QueryList, Renderer2, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core';
import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations';
import { NavigationEnd, Router } from '@angular/router';
import { ScrollStrategy, ScrollStrategyOptions } from '@angular/cdk/overlay';
import { BehaviorSubject, merge, Subject, Subscription } from 'rxjs';
import { delay, filter, takeUntil } from 'rxjs/operators';
import { TreoAnimations } from '@treo/animations';
import { TreoVerticalNavigationAppearance, TreoNavigationItem, TreoVerticalNavigationMode, TreoVerticalNavigationPosition } from '@treo/components/navigation/navigation.types';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoScrollbarDirective } from '@treo/directives/scrollbar/scrollbar.directive';
@Component({
selector : 'treo-vertical-navigation',
templateUrl : './vertical.component.html',
styleUrls : ['./vertical.component.scss'],
animations : TreoAnimations,
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs : 'treoVerticalNavigation'
})
export class TreoVerticalNavigationComponent implements OnInit, AfterViewInit, OnDestroy
{
activeAsideItemId: null | string;
onCollapsableItemCollapsed: BehaviorSubject<TreoNavigationItem | null>;
onCollapsableItemExpanded: BehaviorSubject<TreoNavigationItem | null>;
onRefreshed: BehaviorSubject<boolean | null>;
// Auto collapse
@Input()
autoCollapse: boolean;
// Name
@Input()
name: string;
// On appearance changed
@Output()
readonly appearanceChanged: EventEmitter<TreoVerticalNavigationAppearance>;
// On mode changed
@Output()
readonly modeChanged: EventEmitter<TreoVerticalNavigationMode>;
// On opened changed
@Output()
readonly openedChanged: EventEmitter<boolean | ''>;
// On position changed
@Output()
readonly positionChanged: EventEmitter<TreoVerticalNavigationPosition>;
// Private
private _appearance: TreoVerticalNavigationAppearance;
private _asideOverlay: HTMLElement | null;
private _treoScrollbarDirectives: QueryList<TreoScrollbarDirective>;
private _treoScrollbarDirectivesSubscription: Subscription;
private _handleAsideOverlayClick: any;
private _handleOverlayClick: any;
private _inner: boolean;
private _mode: TreoVerticalNavigationMode;
private _navigation: TreoNavigationItem[];
private _opened: boolean | '';
private _overlay: HTMLElement | null;
private _player: AnimationPlayer;
private _position: TreoVerticalNavigationPosition;
private _scrollStrategy: ScrollStrategy;
private _transparentOverlay: boolean | '';
private _unsubscribeAll: Subject<any>;
@HostBinding('class.treo-vertical-navigation-animations-enabled')
private _animationsEnabled: boolean;
@ViewChild('navigationContent')
private _navigationContentEl: ElementRef;
/**
* Constructor
*
* @param {AnimationBuilder} _animationBuilder
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {ElementRef} _elementRef
* @param {Renderer2} _renderer2
* @param {Router} _router
* @param {ScrollStrategyOptions} _scrollStrategyOptions
*/
constructor(
private _animationBuilder: AnimationBuilder,
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef,
private _renderer2: Renderer2,
private _router: Router,
private _scrollStrategyOptions: ScrollStrategyOptions
)
{
// Set the private defaults
this._animationsEnabled = false;
this._asideOverlay = null;
this._handleAsideOverlayClick = () => {
this.closeAside();
};
this._handleOverlayClick = () => {
this.close();
};
this._overlay = null;
this._scrollStrategy = this._scrollStrategyOptions.block();
this._unsubscribeAll = new Subject();
// Set the defaults
this.appearanceChanged = new EventEmitter<TreoVerticalNavigationAppearance>();
this.modeChanged = new EventEmitter<TreoVerticalNavigationMode>();
this.openedChanged = new EventEmitter<boolean | ''>();
this.positionChanged = new EventEmitter<TreoVerticalNavigationPosition>();
this.onCollapsableItemCollapsed = new BehaviorSubject(null);
this.onCollapsableItemExpanded = new BehaviorSubject(null);
this.onRefreshed = new BehaviorSubject(null);
this.activeAsideItemId = null;
this.appearance = 'classic';
this.autoCollapse = true;
this.inner = false;
this.mode = 'side';
this.opened = false;
this.position = 'left';
this.transparentOverlay = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter & getter for appearance
*
* @param value
*/
@Input()
set appearance(value: TreoVerticalNavigationAppearance)
{
// If the value is the same, return...
if ( this._appearance === value )
{
return;
}
let appearanceClassName;
// Remove the previous appearance class
appearanceClassName = 'treo-vertical-navigation-appearance-' + this.appearance;
this._renderer2.removeClass(this._elementRef.nativeElement, appearanceClassName);
// Store the appearance
this._appearance = value;
// Add the new appearance class
appearanceClassName = 'treo-vertical-navigation-appearance-' + this.appearance;
this._renderer2.addClass(this._elementRef.nativeElement, appearanceClassName);
// Execute the observable
this.appearanceChanged.next(this.appearance);
}
get appearance(): TreoVerticalNavigationAppearance
{
return this._appearance;
}
/**
* Setter for treoScrollbarDirectives
*/
@ViewChildren(TreoScrollbarDirective)
set treoScrollbarDirectives(treoScrollbarDirectives: QueryList<TreoScrollbarDirective>)
{
// Store the directives
this._treoScrollbarDirectives = treoScrollbarDirectives;
// Return, if there are no directives
if ( treoScrollbarDirectives.length === 0 )
{
return;
}
// Unsubscribe the previous subscriptions
if ( this._treoScrollbarDirectivesSubscription )
{
this._treoScrollbarDirectivesSubscription.unsubscribe();
}
// Update the scrollbars on collapsable items' collapse/expand
this._treoScrollbarDirectivesSubscription =
merge(
this.onCollapsableItemCollapsed,
this.onCollapsableItemExpanded
)
.pipe(
takeUntil(this._unsubscribeAll),
delay(250)
)
.subscribe(() => {
// Loop through the scrollbars and update them
treoScrollbarDirectives.forEach((treoScrollbarDirective) => {
treoScrollbarDirective.update();
});
});
}
/**
* Setter & getter for data
*/
@Input()
set navigation(value: TreoNavigationItem[])
{
// Store the data
this._navigation = value;
// Mark for check
this._changeDetectorRef.markForCheck();
}
get navigation(): TreoNavigationItem[]
{
return this._navigation;
}
/**
* Setter & getter for inner
*
* @param value
*/
@Input()
set inner(value: boolean)
{
// If the value is the same, return...
if ( this._inner === value )
{
return;
}
// Set the naked value
this._inner = value;
// Update the class
if ( this.inner )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-vertical-navigation-inner');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-vertical-navigation-inner');
}
}
get inner(): boolean
{
return this._inner;
}
/**
* Setter & getter for mode
*
* @param value
*/
@Input()
set mode(value: TreoVerticalNavigationMode)
{
// If the value is the same, return...
if ( this._mode === value )
{
return;
}
// Disable the animations
this._disableAnimations();
// If the mode changes: 'over -> side'
if ( this.mode === 'over' && value === 'side' )
{
// Hide the overlay
this._hideOverlay();
}
// If the mode changes: 'side -> over'
if ( this.mode === 'side' && value === 'over' )
{
// Close the aside
this.closeAside();
// If the navigation is opened
if ( this.opened )
{
// Show the overlay
this._showOverlay();
}
}
let modeClassName;
// Remove the previous mode class
modeClassName = 'treo-vertical-navigation-mode-' + this.mode;
this._renderer2.removeClass(this._elementRef.nativeElement, modeClassName);
// Store the mode
this._mode = value;
// Add the new mode class
modeClassName = 'treo-vertical-navigation-mode-' + this.mode;
this._renderer2.addClass(this._elementRef.nativeElement, modeClassName);
// Execute the observable
this.modeChanged.next(this.mode);
// Enable the animations after a delay
// The delay must be bigger than the current transition-duration
// to make sure nothing will be animated while the mode changing
setTimeout(() => {
this._enableAnimations();
}, 500);
}
get mode(): TreoVerticalNavigationMode
{
return this._mode;
}
/**
* Setter & getter for opened
*
* @param value
*/
@Input()
set opened(value: boolean | '')
{
// If the value is the same, return...
if ( this._opened === value )
{
return;
}
// If the provided value is an empty string,
// take that as a 'true'
if ( value === '' )
{
value = true;
}
// Set the opened value
this._opened = value;
// If the navigation opened, and the mode
// is 'over', show the overlay
if ( this.mode === 'over' )
{
if ( this._opened )
{
this._showOverlay();
}
else
{
this._hideOverlay();
}
}
if ( this.opened )
{
// Update styles and classes
this._renderer2.setStyle(this._elementRef.nativeElement, 'visibility', 'visible');
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-vertical-navigation-opened');
}
else
{
// Update styles and classes
this._renderer2.setStyle(this._elementRef.nativeElement, 'visibility', 'hidden');
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-vertical-navigation-opened');
}
// Execute the observable
this.openedChanged.next(this.opened);
}
get opened(): boolean | ''
{
return this._opened;
}
/**
* Setter & getter for position
*
* @param value
*/
@Input()
set position(value: TreoVerticalNavigationPosition)
{
// If the value is the same, return...
if ( this._position === value )
{
return;
}
let positionClassName;
// Remove the previous position class
positionClassName = 'treo-vertical-navigation-position-' + this.position;
this._renderer2.removeClass(this._elementRef.nativeElement, positionClassName);
// Store the position
this._position = value;
// Add the new position class
positionClassName = 'treo-vertical-navigation-position-' + this.position;
this._renderer2.addClass(this._elementRef.nativeElement, positionClassName);
// Execute the observable
this.positionChanged.next(this.position);
}
get position(): TreoVerticalNavigationPosition
{
return this._position;
}
/**
* Setter & getter for transparent overlay
*
* @param value
*/
@Input()
set transparentOverlay(value: boolean | '')
{
// If the value is the same, return...
if ( this._opened === value )
{
return;
}
// If the provided value is an empty string,
// take that as a 'true' and set the opened value
if ( value === '' )
{
// Set the opened value
this._transparentOverlay = true;
}
else
{
// Set the transparent overlay value
this._transparentOverlay = value;
}
}
get transparentOverlay(): boolean | ''
{
return this._transparentOverlay;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Register the navigation component
this._treoNavigationService.registerComponent(this.name, this);
// Subscribe to the 'NavigationEnd' event
this._router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this._unsubscribeAll)
)
.subscribe(() => {
if ( this.mode === 'over' && this.opened )
{
// Close the navigation
this.close();
}
});
}
/**
* After view init
*/
ngAfterViewInit(): void
{
setTimeout(() => {
// If 'navigation content' element doesn't have
// perfect scrollbar activated on it...
if ( !this._navigationContentEl.nativeElement.classList.contains('ps') )
{
// Find the active item
const activeItem = this._navigationContentEl.nativeElement.querySelector('.treo-vertical-navigation-item-active');
// If the active item exists, scroll it into view
if ( activeItem )
{
activeItem.scrollIntoView();
}
}
// Otherwise
else
{
// Go through all the scrollbar directives
this._treoScrollbarDirectives.forEach((treoScrollbarDirective) => {
// Skip if not enabled
if ( !treoScrollbarDirective.enabled )
{
return;
}
// Scroll to the active element
treoScrollbarDirective.scrollToElement('.treo-vertical-navigation-item-active', -120, true);
});
}
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Deregister the navigation component from the registry
this._treoNavigationService.deregisterComponent(this.name);
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Enable the animations
*
* @private
*/
private _enableAnimations(): void
{
// If the animations are already enabled, return...
if ( this._animationsEnabled )
{
return;
}
// Enable the animations
this._animationsEnabled = true;
}
/**
* Disable the animations
*
* @private
*/
private _disableAnimations(): void
{
// If the animations are already disabled, return...
if ( !this._animationsEnabled )
{
return;
}
// Disable the animations
this._animationsEnabled = false;
}
/**
* Show the overlay
*
* @private
*/
private _showOverlay(): void
{
// If there is already an overlay, return...
if ( this._asideOverlay )
{
return;
}
// Create the overlay element
this._overlay = this._renderer2.createElement('div');
// Add a class to the overlay element
this._overlay.classList.add('treo-vertical-navigation-overlay');
// Add a class depending on the transparentOverlay option
if ( this.transparentOverlay )
{
this._overlay.classList.add('treo-vertical-navigation-overlay-transparent');
}
// Append the overlay to the parent of the navigation
this._renderer2.appendChild(this._elementRef.nativeElement.parentElement, this._overlay);
// Enable block scroll strategy
this._scrollStrategy.enable();
// Create the enter animation and attach it to the player
this._player =
this._animationBuilder
.build([
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 1}))
]).create(this._overlay);
// Play the animation
this._player.play();
// Add an event listener to the overlay
this._overlay.addEventListener('click', this._handleOverlayClick);
}
/**
* Hide the overlay
*
* @private
*/
private _hideOverlay(): void
{
if ( !this._overlay )
{
return;
}
// Create the leave animation and attach it to the player
this._player =
this._animationBuilder
.build([
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 0}))
]).create(this._overlay);
// Play the animation
this._player.play();
// Once the animation is done...
this._player.onDone(() => {
// If the overlay still exists...
if ( this._overlay )
{
// Remove the event listener
this._overlay.removeEventListener('click', this._handleOverlayClick);
// Remove the overlay
this._overlay.parentNode.removeChild(this._overlay);
this._overlay = null;
}
// Disable block scroll strategy
this._scrollStrategy.disable();
});
}
/**
* Show the aside overlay
*
* @private
*/
private _showAsideOverlay(): void
{
// If there is already an overlay, return...
if ( this._asideOverlay )
{
return;
}
// Create the aside overlay element
this._asideOverlay = this._renderer2.createElement('div');
// Add a class to the aside overlay element
this._asideOverlay.classList.add('treo-vertical-navigation-aside-overlay');
// Append the aside overlay to the parent of the navigation
this._renderer2.appendChild(this._elementRef.nativeElement.parentElement, this._asideOverlay);
// Create the enter animation and attach it to the player
this._player =
this._animationBuilder
.build([
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 1}))
]).create(this._asideOverlay);
// Play the animation
this._player.play();
// Add an event listener to the aside overlay
this._asideOverlay.addEventListener('click', this._handleAsideOverlayClick);
}
/**
* Hide the aside overlay
*
* @private
*/
private _hideAsideOverlay(): void
{
if ( !this._asideOverlay )
{
return;
}
// Create the leave animation and attach it to the player
this._player =
this._animationBuilder
.build([
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 0}))
]).create(this._asideOverlay);
// Play the animation
this._player.play();
// Once the animation is done...
this._player.onDone(() => {
// If the aside overlay still exists...
if ( this._asideOverlay )
{
// Remove the event listener
this._asideOverlay.removeEventListener('click', this._handleAsideOverlayClick);
// Remove the aside overlay
this._asideOverlay.parentNode.removeChild(this._asideOverlay);
this._asideOverlay = null;
}
});
}
/**
* On mouseenter
*
* @private
*/
@HostListener('mouseenter')
private _onMouseenter(): void
{
// Enable the animations
this._enableAnimations();
// Add a class
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-vertical-navigation-hover');
}
/**
* On mouseleave
*
* @private
*/
@HostListener('mouseleave')
private _onMouseleave(): void
{
// Enable the animations
this._enableAnimations();
// Remove the class
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-vertical-navigation-hover');
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Refresh the component to apply the changes
*/
refresh(): void
{
// Mark for check
this._changeDetectorRef.markForCheck();
// Execute the observable
this.onRefreshed.next(true);
}
/**
* Open the navigation
*/
open(): void
{
// Enable the animations
this._enableAnimations();
// Open
this.opened = true;
}
/**
* Close the navigation
*/
close(): void
{
// Enable the animations
this._enableAnimations();
// Close the aside
this.closeAside();
// Close
this.opened = false;
}
/**
* Toggle the opened status
*/
toggle(): void
{
// Toggle
if ( this.opened )
{
this.close();
}
else
{
this.open();
}
}
/**
* Open the aside
*
* @param item
*/
openAside(item: TreoNavigationItem): void
{
// Return if the item is disabled
if ( item.disabled )
{
return;
}
// Open
this.activeAsideItemId = item.id;
// Show the aside overlay
this._showAsideOverlay();
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Close the aside
*/
closeAside(): void
{
// Close
this.activeAsideItemId = null;
// Hide the aside overlay
this._hideAsideOverlay();
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Toggle the aside
*
* @param item
*/
toggleAside(item: TreoNavigationItem): void
{
// Toggle
if ( this.activeAsideItemId === item.id )
{
this.closeAside();
}
else
{
this.openAside(item);
}
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}
@@ -0,0 +1,106 @@
import { Directive, ElementRef, HostBinding, HostListener, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { Subject } from 'rxjs';
@Directive({
selector: 'textarea[treoAutogrow]',
exportAs: 'treoAutogrow'
})
export class TreoAutogrowDirective implements OnInit, OnDestroy
{
@HostBinding('rows')
rows: number;
// Private
private _padding: number;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {ElementRef} _elementRef
* @param {Renderer2} _renderer2
*/
constructor(
private _elementRef: ElementRef,
private _renderer2: Renderer2
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.padding = 8;
this.rows = 1;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for padding
*
* @param value
*/
@Input('treoAutogrowVerticalPadding')
set padding(value)
{
// Store the value
this._padding = value;
}
get padding(): number
{
return this._padding;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Set base styles
this._renderer2.setStyle(this._elementRef.nativeElement, 'resize', 'none');
this._renderer2.setStyle(this._elementRef.nativeElement, 'overflow', 'hidden');
// Set the height for the first time
setTimeout(() => {
this._resize();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Resize on 'input' and 'ngModelChange' events
*
* @private
*/
@HostListener('input')
@HostListener('ngModelChange')
private _resize(): void
{
// Set the height to 'auto' so we can correctly read the scrollHeight
this._renderer2.setStyle(this._elementRef.nativeElement, 'height', 'auto');
// Get the scrollHeight and subtract the vertical padding
const height = this._elementRef.nativeElement.scrollHeight - this.padding + 'px';
this._renderer2.setStyle(this._elementRef.nativeElement, 'height', height);
}
}
@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { TreoAutogrowDirective } from '@treo/directives/autogrow/autogrow.directive';
@NgModule({
declarations: [
TreoAutogrowDirective
],
exports : [
TreoAutogrowDirective
]
})
export class TreoAutogrowModule
{
}
@@ -0,0 +1 @@
export * from '@treo/directives/autogrow/public-api';
@@ -0,0 +1,2 @@
export * from '@treo/directives/autogrow/autogrow.directive';
export * from '@treo/directives/autogrow/autogrow.module';
@@ -0,0 +1 @@
export * from '@treo/directives/scrollbar/public-api';
@@ -0,0 +1,2 @@
export * from '@treo/directives/scrollbar/scrollbar.directive';
export * from '@treo/directives/scrollbar/scrollbar.module';
@@ -0,0 +1,489 @@
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Platform } from '@angular/cdk/platform';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import PerfectScrollbar from 'perfect-scrollbar';
import * as _ from 'lodash';
import { ScrollbarGeometry, ScrollbarPosition } from '@treo/directives/scrollbar/scrollbar.interfaces';
// -----------------------------------------------------------------------------------------------------
// Wrapper directive for the Perfect Scrollbar: https://github.com/mdbootstrap/perfect-scrollbar
// Based on https://github.com/zefoy/ngx-perfect-scrollbar
// -----------------------------------------------------------------------------------------------------
@Directive({
selector: '[treoScrollbar]',
exportAs: 'treoScrollbar'
})
export class TreoScrollbarDirective implements OnInit, OnDestroy
{
isMobile: boolean;
ps: PerfectScrollbar | any;
// Private
private _animation: number | null;
private _enabled: boolean;
private _options: any;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {ElementRef} _elementRef
* @param {Platform} _platform
* @param {Router} _router
*/
constructor(
private _elementRef: ElementRef,
private _platform: Platform,
private _router: Router
)
{
// Set the private defaults
this._animation = null;
this._options = {};
this._unsubscribeAll = new Subject();
// Set the defaults
this.enabled = true;
this.isMobile = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Scrollbar options
*
* @param value
*/
@Input()
set treoScrollbarOptions(value: any)
{
// Merge the options
this._options = _.merge({}, this._options, value);
// Destroy and re-init the PerfectScrollbar to update its options
setTimeout(() => {
this._destroy();
});
setTimeout(() => {
this._init();
});
}
get treoScrollbarOptions(): any
{
// Return the options
return this._options;
}
/**
* Is enabled
*
* @param value
*/
@Input('treoScrollbar')
set enabled(value: boolean | '')
{
// If the value is an empty string, interpret it as 'true'
if ( value === '' )
{
value = true;
}
// If the value is the same, return...
if ( this._enabled === value )
{
return;
}
// Store the value
this._enabled = value;
// If enabled...
if ( this.enabled )
{
// Init the directive
this._init();
}
else
{
// Otherwise destroy it
this._destroy();
}
}
get enabled(): boolean | ''
{
// Return the enabled status
return this._enabled;
}
/**
* Getter for _elementRef
*/
get elementRef(): ElementRef
{
return this._elementRef;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Subscribe to window resize event
fromEvent(window, 'resize')
.pipe(
takeUntil(this._unsubscribeAll),
debounceTime(150)
)
.subscribe(() => {
// Update the PerfectScrollbar
this.update();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
this._destroy();
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Initialize
*
* @private
*/
private _init(): void
{
// Return, if already initialized
if ( this.ps )
{
return;
}
// Check if is mobile
if ( this._platform.ANDROID || this._platform.IOS )
{
this.isMobile = true;
}
// Return if it's mobile or the platform is not a browser
if ( this.isMobile || !this._platform.isBrowser )
{
// Silently set the enabled to false
this._enabled = false;
return;
}
// Initialize the PerfectScrollbar
this.ps = new PerfectScrollbar(this._elementRef.nativeElement, {...this.treoScrollbarOptions});
}
/**
* Destroy
*
* @private
*/
private _destroy(): void
{
if ( !this.ps )
{
return;
}
// Destroy the PerfectScrollbar
this.ps.destroy();
// Clean up
this.ps = null;
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Update the scrollbar
*/
update(): void
{
if ( !this.ps )
{
return;
}
// Update the PerfectScrollbar
this.ps.update();
}
/**
* Destroy the scrollbar
*/
destroy(): void
{
this.ngOnDestroy();
}
/**
* Returns the geometry of the scrollable element
*
* @param prefix
*/
geometry(prefix: string = 'scroll'): ScrollbarGeometry
{
const scrollbarGeometry = new ScrollbarGeometry(
this._elementRef.nativeElement[prefix + 'Left'],
this._elementRef.nativeElement[prefix + 'Top'],
this._elementRef.nativeElement[prefix + 'Width'],
this._elementRef.nativeElement[prefix + 'Height']);
return scrollbarGeometry;
}
/**
* Returns the position of the scrollable element
*
* @param absolute
*/
position(absolute: boolean = false): ScrollbarPosition
{
let scrollbarPosition;
if ( !absolute && this.ps )
{
scrollbarPosition = new ScrollbarPosition(
this.ps.reach.x || 0,
this.ps.reach.y || 0
);
}
else
{
scrollbarPosition = new ScrollbarPosition(
this._elementRef.nativeElement.scrollLeft,
this._elementRef.nativeElement.scrollTop
);
}
return scrollbarPosition;
}
/**
* Scroll to
*
* @param x
* @param y
* @param speed
*/
scrollTo(x: number, y?: number, speed?: number): void
{
if ( y == null && speed == null )
{
this.animateScrolling('scrollTop', x, speed);
}
else
{
if ( x != null )
{
this.animateScrolling('scrollLeft', x, speed);
}
if ( y != null )
{
this.animateScrolling('scrollTop', y, speed);
}
}
}
/**
* Scroll to X
*
* @param {number} x
* @param {number} speed
*/
scrollToX(x: number, speed?: number): void
{
this.animateScrolling('scrollLeft', x, speed);
}
/**
* Scroll to Y
*
* @param {number} y
* @param {number} speed
*/
scrollToY(y: number, speed?: number): void
{
this.animateScrolling('scrollTop', y, speed);
}
/**
* Scroll to top
*
* @param {number} offset
* @param {number} speed
*/
scrollToTop(offset: number = 0, speed?: number): void
{
this.animateScrolling('scrollTop', offset, speed);
}
/**
* Scroll to bottom
*
* @param {number} offset
* @param {number} speed
*/
scrollToBottom(offset: number = 0, speed?: number): void
{
const top = this._elementRef.nativeElement.scrollHeight - this._elementRef.nativeElement.clientHeight;
this.animateScrolling('scrollTop', top - offset, speed);
}
/**
* Scroll to left
*
* @param {number} offset
* @param {number} speed
*/
scrollToLeft(offset: number = 0, speed?: number): void
{
this.animateScrolling('scrollLeft', offset, speed);
}
/**
* Scroll to right
*
* @param {number} offset
* @param {number} speed
*/
scrollToRight(offset: number = 0, speed?: number): void
{
const left = this._elementRef.nativeElement.scrollWidth - this._elementRef.nativeElement.clientWidth;
this.animateScrolling('scrollLeft', left - offset, speed);
}
/**
* Scroll to element
*
* @param {string} qs
* @param {number} offset
* @param {boolean} ignoreVisible If true, scrollToElement won't happen if element is already inside the current viewport
* @param {number} speed
*/
scrollToElement(qs: string, offset: number = 0, ignoreVisible: boolean = false, speed?: number): void
{
const element = this._elementRef.nativeElement.querySelector(qs);
if ( !element )
{
return;
}
const elementPos = element.getBoundingClientRect();
const scrollerPos = this._elementRef.nativeElement.getBoundingClientRect();
if ( this._elementRef.nativeElement.classList.contains('ps--active-x') )
{
if ( ignoreVisible && elementPos.right <= (scrollerPos.right - Math.abs(offset)) )
{
return;
}
const currentPos = this._elementRef.nativeElement['scrollLeft'];
const position = elementPos.left - scrollerPos.left + currentPos;
this.animateScrolling('scrollLeft', position + offset, speed);
}
if ( this._elementRef.nativeElement.classList.contains('ps--active-y') )
{
if ( ignoreVisible && elementPos.bottom <= (scrollerPos.bottom - Math.abs(offset)) )
{
return;
}
const currentPos = this._elementRef.nativeElement['scrollTop'];
const position = elementPos.top - scrollerPos.top + currentPos;
this.animateScrolling('scrollTop', position + offset, speed);
}
}
/**
* Animate scrolling
*
* @param target
* @param value
* @param speed
*/
animateScrolling(target: string, value: number, speed?: number): void
{
if ( this._animation )
{
window.cancelAnimationFrame(this._animation);
this._animation = null;
}
if ( !speed || typeof window === 'undefined' )
{
this._elementRef.nativeElement[target] = value;
}
else if ( value !== this._elementRef.nativeElement[target] )
{
let newValue = 0;
let scrollCount = 0;
let oldTimestamp = performance.now();
let oldValue = this._elementRef.nativeElement[target];
const cosParameter = (oldValue - value) / 2;
const step = (newTimestamp: number) => {
scrollCount += Math.PI / (speed / (newTimestamp - oldTimestamp));
newValue = Math.round(value + cosParameter + cosParameter * Math.cos(scrollCount));
// Only continue animation if scroll position has not changed
if ( this._elementRef.nativeElement[target] === oldValue )
{
if ( scrollCount >= Math.PI )
{
this.animateScrolling(target, value, 0);
}
else
{
this._elementRef.nativeElement[target] = newValue;
// On a zoomed out page the resulting offset may differ
oldValue = this._elementRef.nativeElement[target];
oldTimestamp = newTimestamp;
this._animation = window.requestAnimationFrame(step);
}
}
};
window.requestAnimationFrame(step);
}
}
}
@@ -0,0 +1,28 @@
export class ScrollbarGeometry
{
public x: number;
public y: number;
public w: number;
public h: number;
constructor(x: number, y: number, w: number, h: number)
{
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
}
export class ScrollbarPosition
{
public x: number | 'start' | 'end';
public y: number | 'start' | 'end';
constructor(x: number | 'start' | 'end', y: number | 'start' | 'end')
{
this.x = x;
this.y = y;
}
}
@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { TreoScrollbarDirective } from '@treo/directives/scrollbar/scrollbar.directive';
@NgModule({
declarations: [
TreoScrollbarDirective
],
exports : [
TreoScrollbarDirective
]
})
export class TreoScrollbarModule
{
}
+1
View File
@@ -0,0 +1 @@
export * from './treo.module';
@@ -0,0 +1 @@
export * from '@treo/lib/mock-api/mock-api.module';
@@ -0,0 +1,92 @@
import { Injectable } from '@angular/core';
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, switchMap } from 'rxjs/operators';
import { TreoMockApiRequestHandler } from '@treo/lib/mock-api/mock-api.request-handler';
import { TreoMockApiService } from '@treo/lib/mock-api/mock-api.service';
@Injectable({
providedIn: 'root'
})
export class TreoMockApiInterceptor implements HttpInterceptor
{
/**
* Constructor
*
* @param {TreoMockApiService} _treoMockApiService
*/
constructor(
private _treoMockApiService: TreoMockApiService
)
{
}
/**
* Intercept
*
* @param request
* @param next
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
{
// Try to get the request handler
const requestHandler: TreoMockApiRequestHandler = this._treoMockApiService.requestHandlers[request.method.toLowerCase()].get(request.url);
// If the request handler exists..
if ( requestHandler )
{
// Set the intercepted request on the requestHandler
requestHandler.interceptedRequest = request;
// Subscribe to the reply function observable
return requestHandler.replyCallback.pipe(
delay(requestHandler.delay),
switchMap((response) => {
// Throw a not found response, if there is no response data
if ( !response )
{
response = new HttpErrorResponse({
error : 'NOT FOUND',
status : 404,
statusText: 'NOT FOUND'
});
return throwError(response);
}
// Parse the response data
const data = {
status: response[0],
body : response[1]
};
// If the status is in between 200 and 300,
// it's a success response
if ( data.status >= 200 && data.status < 300 )
{
response = new HttpResponse({
body : data.body,
status : data.status,
statusText: 'OK'
});
return of(response);
}
// Error response
response = new HttpErrorResponse({
error : data.body.error,
status : data.status,
statusText: 'ERROR'
});
return throwError(response);
}));
}
// Pass through if the request handler does not exists
return next.handle(request);
}
}
@@ -0,0 +1,4 @@
export interface TreoMockApi
{
register(): void;
}
@@ -0,0 +1,37 @@
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TreoMockApiInterceptor } from '@treo/lib/mock-api/mock-api.interceptor';
import { TreoMockApiService } from '@treo/lib/mock-api/mock-api.service';
@NgModule({
providers: [
TreoMockApiService,
{
provide : HTTP_INTERCEPTORS,
useClass: TreoMockApiInterceptor,
multi : true
}
]
})
export class TreoMockApiModule
{
/**
* forRoot method for setting user configuration
*
* @param mockDataServices
*/
static forRoot(mockDataServices: any[]): ModuleWithProviders
{
return {
ngModule : TreoMockApiModule,
providers: [
{
provide : APP_INITIALIZER,
deps : mockDataServices,
useFactory: () => () => null,
multi : true
},
]
};
}
}
@@ -0,0 +1,160 @@
import { Injectable } from '@angular/core';
import { HttpRequest } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { take } from 'rxjs/operators';
@Injectable()
export class TreoMockApiRequestHandler
{
// Private
private _delay: number;
private _executionCount: number;
private _executionLimit: number;
private _interceptedRequest: HttpRequest<any>;
private _replyCallback: any;
private _url: string;
/**
* Constructor
*/
constructor()
{
// Set the private defaults
this._executionCount = 0;
this._executionLimit = 0;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for delay
*
* @param value
*/
set delay(value: number)
{
// Return, if the value is the same
if ( this._delay === value )
{
return;
}
// Store the delay
this._delay = value;
}
get delay(): number
{
return this._delay;
}
/**
* Setter and getter for url
*
* @param value
*/
set url(value: string)
{
// Return, if the value is the same
if ( this._url === value )
{
return;
}
// Store the url
this._url = value;
}
get url(): string
{
return this._url;
}
/**
* Setter and getter for intercepted request
*
* @param value
*/
set interceptedRequest(value: HttpRequest<any>)
{
// Return, if the value is the same
if ( this._interceptedRequest === value )
{
return;
}
// Store the intercepted request
this._interceptedRequest = value;
}
get interceptedRequest(): HttpRequest<any>
{
return this._interceptedRequest;
}
/**
* Getter for reply callback
*/
get replyCallback(): Observable<any>
{
// Throw an error, if the execution limit has been reached
if ( this._executionLimit > 0 && this._executionCount === this._executionLimit )
{
return throwError('Execution limit reached');
}
// Throw an error, if the intercepted request has not been set
if ( !this.interceptedRequest )
{
return throwError('Intercepted request does not exist!');
}
// Increase the execution count
this._executionCount++;
// Execute the reply callback
const replyCallbackResult = this._replyCallback(this.interceptedRequest);
// If the result of the reply function is an observable...
if ( replyCallbackResult instanceof Observable )
{
// Return the result as it is
return replyCallbackResult.pipe(take(1));
}
// Otherwise, return the result as an observable
return of(replyCallbackResult).pipe(take(1));
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Reply
*
* @param callback
*/
reply(callback: (req: HttpRequest<any>) => ([number, any | string] | Observable<any>)): void
{
// Store the reply callback
this._replyCallback = callback;
}
/**
* Reply once
*
* @param callback
*/
replyOnce(callback: (req: HttpRequest<any>) => ([number, any | string] | Observable<any>)): void
{
// Set the execute limit to 1
this._executionLimit = 1;
// Call reply as normal
this.reply(callback);
}
}
@@ -0,0 +1,114 @@
import { Injectable } from '@angular/core';
import { TreoMockApiRequestHandler } from '@treo/lib/mock-api/mock-api.request-handler';
@Injectable({
providedIn: 'root'
})
export class TreoMockApiService
{
requestHandlers: any;
/**
* Constructor
*/
constructor()
{
// Set the defaults
this.requestHandlers = {
delete: new Map<string, TreoMockApiRequestHandler>(),
get : new Map<string, TreoMockApiRequestHandler>(),
patch : new Map<string, TreoMockApiRequestHandler>(),
post : new Map<string, TreoMockApiRequestHandler>(),
put : new Map<string, TreoMockApiRequestHandler>()
};
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Register 'delete' request handler
*
* @param url
* @param delay
*/
onDelete(url: string, delay: number = 0): TreoMockApiRequestHandler
{
return this._registerRequestHandler('delete', url, delay);
}
/**
* Register 'get' request handler
*
* @param url
* @param delay
*/
onGet(url: string, delay: number = 0): TreoMockApiRequestHandler
{
return this._registerRequestHandler('get', url, delay);
}
/**
* Register 'patch' request handler
*
* @param url
* @param delay
*/
onPatch(url: string, delay: number = 0): TreoMockApiRequestHandler
{
return this._registerRequestHandler('patch', url, delay);
}
/**
* Register 'post' request handler
*
* @param url
* @param delay
*/
onPost(url: string, delay: number = 0): TreoMockApiRequestHandler
{
return this._registerRequestHandler('post', url, delay);
}
/**
* Register 'put' request handler
*
* @param url
* @param delay
*/
onPut(url: string, delay: number = 0): TreoMockApiRequestHandler
{
return this._registerRequestHandler('put', url, delay);
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Register a request handler
*
* @param requestType
* @param url
* @param delay
* @private
*/
private _registerRequestHandler(requestType, url, delay): TreoMockApiRequestHandler
{
// Create a new instance of TreoMockApiRequestHandler
const treoMockHttp = new TreoMockApiRequestHandler();
// Store the url
treoMockHttp.url = url;
// Store the delay
treoMockHttp.delay = delay;
// Store the request handler to access them from the interceptor
this.requestHandlers[requestType].set(url, treoMockHttp);
// Return the instance
return treoMockHttp;
}
}
@@ -0,0 +1,38 @@
export class TreoMockApiUtils
{
/**
* Constructor
*/
constructor()
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Generate a globally unique id
*/
static guid(): string
{
/* tslint:disable */
let d = new Date().getTime();
// Use high-precision timer if available
if ( typeof performance !== 'undefined' && typeof performance.now === 'function' )
{
d += performance.now();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
/* tslint:enable */
}
}
@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { TreoFindByKeyPipe } from '@treo/pipes/find-by-key/find-by-key.pipe';
@NgModule({
declarations: [
TreoFindByKeyPipe
],
exports : [
TreoFindByKeyPipe
]
})
export class TreoFindByKeyPipeModule
{
}
@@ -0,0 +1,39 @@
import { Pipe, PipeTransform } from '@angular/core';
/**
* Finds an object from given source using the given key - value pairs
*/
@Pipe({
name: 'treoFindByKey',
pure: false
})
export class TreoFindByKeyPipe implements PipeTransform
{
/**
* Constructor
*/
constructor()
{
}
/**
* Transform
*
* @param value A string or an array of strings to find from source
* @param key Key of the object property to look for
* @param source Array of objects to find from
*/
transform(value: string | string[], key: string, source: any[]): any
{
// If the given value is an array of strings...
if ( Array.isArray(value) )
{
return value.map((item) => {
return source.find((sourceItem) => sourceItem[key] === item);
});
}
// If the value is a string...
return source.find(sourceItem => sourceItem[key] === value);
}
}
@@ -0,0 +1 @@
export * from '@treo/pipes/find-by-key/public-api';
@@ -0,0 +1,2 @@
export * from '@treo/pipes/find-by-key/find-by-key.pipe';
export * from '@treo/pipes/find-by-key/find-by-key.module';
@@ -0,0 +1,3 @@
import { InjectionToken } from '@angular/core';
export const TREO_APP_CONFIG = new InjectionToken<any>('Default configuration for the app');
@@ -0,0 +1,36 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { TreoConfigService } from '@treo/services/config/config.service';
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
@NgModule()
export class TreoConfigModule
{
/**
* Constructor
*
* @param {TreoConfigService} _treoConfigService
*/
constructor(
private _treoConfigService: TreoConfigService
)
{
}
/**
* forRoot method for setting user configuration
*
* @param config
*/
static forRoot(config: any): ModuleWithProviders
{
return {
ngModule : TreoConfigModule,
providers: [
{
provide : TREO_APP_CONFIG,
useValue: config
}
]
};
}
}
@@ -0,0 +1,56 @@
import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import * as _ from 'lodash';
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
@Injectable({
providedIn: 'root'
})
export class TreoConfigService
{
// Private
private _config: BehaviorSubject<any>;
/**
* Constructor
*/
constructor(@Inject(TREO_APP_CONFIG) config: any)
{
// Set the private defaults
this._config = new BehaviorSubject(config);
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for config
*/
set config(value: any)
{
// Merge the new config over to the current config
const config = _.merge({}, this._config.getValue(), value);
// Execute the observable
this._config.next(config);
}
get config$(): Observable<any>
{
return this._config.asObservable();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resets the config to the default
*/
reset(): void
{
// Set the config
this._config.next(this.config);
}
}
@@ -0,0 +1 @@
export * from '@treo/services/config/public-api';
@@ -0,0 +1,2 @@
export * from '@treo/services/config/config.module';
export * from '@treo/services/config/config.service';

Some files were not shown because too many files have changed in this diff Show More