init
This commit is contained in:
@@ -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';
|
||||
+92
@@ -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>
|
||||
+74
@@ -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();
|
||||
}
|
||||
}
|
||||
+93
@@ -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>
|
||||
+99
@@ -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();
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<!-- Divider -->
|
||||
<div class="treo-horizontal-navigation-item-wrapper divider"></div>
|
||||
+74
@@ -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();
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<!-- Spacer -->
|
||||
<div class="treo-horizontal-navigation-item-wrapper"></div>
|
||||
+74
@@ -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';
|
||||
+83
@@ -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>
|
||||
+104
@@ -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;
|
||||
}
|
||||
}
|
||||
+91
@@ -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>
|
||||
+74
@@ -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();
|
||||
}
|
||||
}
|
||||
+85
@@ -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>
|
||||
+363
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<!-- Divider -->
|
||||
<div class="treo-vertical-navigation-item-wrapper divider"></div>
|
||||
+74
@@ -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();
|
||||
}
|
||||
}
|
||||
+74
@@ -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>
|
||||
+93
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<!-- Spacer -->
|
||||
<div class="treo-vertical-navigation-item-wrapper"></div>
|
||||
+74
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user