Replace submodules with full folder contents
This commit is contained in:
12
fuse-starter-v20.0.0/src/@fuse/animations/defaults.ts
Normal file
12
fuse-starter-v20.0.0/src/@fuse/animations/defaults.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export class FuseAnimationCurves {
|
||||
static standard = 'cubic-bezier(0.4, 0.0, 0.2, 1)';
|
||||
static deceleration = 'cubic-bezier(0.0, 0.0, 0.2, 1)';
|
||||
static acceleration = 'cubic-bezier(0.4, 0.0, 1, 1)';
|
||||
static sharp = 'cubic-bezier(0.4, 0.0, 0.6, 1)';
|
||||
}
|
||||
|
||||
export class FuseAnimationDurations {
|
||||
static complex = '375ms';
|
||||
static entering = '225ms';
|
||||
static exiting = '195ms';
|
||||
}
|
||||
37
fuse-starter-v20.0.0/src/@fuse/animations/expand-collapse.ts
Normal file
37
fuse-starter-v20.0.0/src/@fuse/animations/expand-collapse.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
FuseAnimationCurves,
|
||||
FuseAnimationDurations,
|
||||
} from '@fuse/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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
export { expandCollapse };
|
||||
330
fuse-starter-v20.0.0/src/@fuse/animations/fade.ts
Normal file
330
fuse-starter-v20.0.0/src/@fuse/animations/fade.ts
Normal file
@ -0,0 +1,330 @@
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
FuseAnimationCurves,
|
||||
FuseAnimationDurations,
|
||||
} from '@fuse/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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
export {
|
||||
fadeIn,
|
||||
fadeInBottom,
|
||||
fadeInLeft,
|
||||
fadeInRight,
|
||||
fadeInTop,
|
||||
fadeOut,
|
||||
fadeOutBottom,
|
||||
fadeOutLeft,
|
||||
fadeOutRight,
|
||||
fadeOutTop,
|
||||
};
|
||||
1
fuse-starter-v20.0.0/src/@fuse/animations/index.ts
Normal file
1
fuse-starter-v20.0.0/src/@fuse/animations/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@fuse/animations/public-api';
|
||||
50
fuse-starter-v20.0.0/src/@fuse/animations/public-api.ts
Normal file
50
fuse-starter-v20.0.0/src/@fuse/animations/public-api.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { expandCollapse } from '@fuse/animations/expand-collapse';
|
||||
import {
|
||||
fadeIn,
|
||||
fadeInBottom,
|
||||
fadeInLeft,
|
||||
fadeInRight,
|
||||
fadeInTop,
|
||||
fadeOut,
|
||||
fadeOutBottom,
|
||||
fadeOutLeft,
|
||||
fadeOutRight,
|
||||
fadeOutTop,
|
||||
} from '@fuse/animations/fade';
|
||||
import { shake } from '@fuse/animations/shake';
|
||||
import {
|
||||
slideInBottom,
|
||||
slideInLeft,
|
||||
slideInRight,
|
||||
slideInTop,
|
||||
slideOutBottom,
|
||||
slideOutLeft,
|
||||
slideOutRight,
|
||||
slideOutTop,
|
||||
} from '@fuse/animations/slide';
|
||||
import { zoomIn, zoomOut } from '@fuse/animations/zoom';
|
||||
|
||||
export const fuseAnimations = [
|
||||
expandCollapse,
|
||||
fadeIn,
|
||||
fadeInTop,
|
||||
fadeInBottom,
|
||||
fadeInLeft,
|
||||
fadeInRight,
|
||||
fadeOut,
|
||||
fadeOutTop,
|
||||
fadeOutBottom,
|
||||
fadeOutLeft,
|
||||
fadeOutRight,
|
||||
shake,
|
||||
slideInTop,
|
||||
slideInBottom,
|
||||
slideInLeft,
|
||||
slideInRight,
|
||||
slideOutTop,
|
||||
slideOutBottom,
|
||||
slideOutLeft,
|
||||
slideOutRight,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
];
|
||||
78
fuse-starter-v20.0.0/src/@fuse/animations/shake.ts
Normal file
78
fuse-starter-v20.0.0/src/@fuse/animations/shake.ts
Normal file
@ -0,0 +1,78 @@
|
||||
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 };
|
||||
254
fuse-starter-v20.0.0/src/@fuse/animations/slide.ts
Normal file
254
fuse-starter-v20.0.0/src/@fuse/animations/slide.ts
Normal file
@ -0,0 +1,254 @@
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
FuseAnimationCurves,
|
||||
FuseAnimationDurations,
|
||||
} from '@fuse/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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
export {
|
||||
slideInBottom,
|
||||
slideInLeft,
|
||||
slideInRight,
|
||||
slideInTop,
|
||||
slideOutBottom,
|
||||
slideOutLeft,
|
||||
slideOutRight,
|
||||
slideOutTop,
|
||||
};
|
||||
75
fuse-starter-v20.0.0/src/@fuse/animations/zoom.ts
Normal file
75
fuse-starter-v20.0.0/src/@fuse/animations/zoom.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
FuseAnimationCurves,
|
||||
FuseAnimationDurations,
|
||||
} from '@fuse/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: `${FuseAnimationDurations.entering} ${FuseAnimationCurves.deceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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: `${FuseAnimationDurations.exiting} ${FuseAnimationCurves.acceleration}`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
export { zoomIn, zoomOut };
|
||||
@ -0,0 +1,93 @@
|
||||
@if (!dismissible || (dismissible && !dismissed)) {
|
||||
<div
|
||||
class="fuse-alert-container"
|
||||
[@fadeIn]="!dismissed"
|
||||
[@fadeOut]="!dismissed"
|
||||
>
|
||||
<!-- Border -->
|
||||
@if (appearance === 'border') {
|
||||
<div class="fuse-alert-border"></div>
|
||||
}
|
||||
|
||||
<!-- Icon -->
|
||||
@if (showIcon) {
|
||||
<div class="fuse-alert-icon">
|
||||
<!-- Custom icon -->
|
||||
<div class="fuse-alert-custom-icon">
|
||||
<ng-content select="[fuseAlertIcon]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Default icons -->
|
||||
<div class="fuse-alert-default-icon">
|
||||
@if (type === 'primary') {
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:check-circle'"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
@if (type === 'accent') {
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:check-circle'"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
@if (type === 'warn') {
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:x-circle'"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
@if (type === 'basic') {
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:check-circle'"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
@if (type === 'info') {
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:information-circle'"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
@if (type === 'success') {
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:check-circle'"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
@if (type === 'warning') {
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:exclamation-triangle'"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
@if (type === 'error') {
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:x-circle'"
|
||||
></mat-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Content -->
|
||||
<div class="fuse-alert-content">
|
||||
<div class="fuse-alert-title">
|
||||
<ng-content select="[fuseAlertTitle]"></ng-content>
|
||||
</div>
|
||||
|
||||
<div class="fuse-alert-message">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dismiss button -->
|
||||
<button
|
||||
class="fuse-alert-dismiss-button"
|
||||
mat-icon-button
|
||||
(click)="dismiss()"
|
||||
>
|
||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
1290
fuse-starter-v20.0.0/src/@fuse/components/alert/alert.component.scss
Normal file
1290
fuse-starter-v20.0.0/src/@fuse/components/alert/alert.component.scss
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,220 @@
|
||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { fuseAnimations } from '@fuse/animations';
|
||||
import { FuseAlertService } from '@fuse/components/alert/alert.service';
|
||||
import {
|
||||
FuseAlertAppearance,
|
||||
FuseAlertType,
|
||||
} from '@fuse/components/alert/alert.types';
|
||||
import { FuseUtilsService } from '@fuse/services/utils/utils.service';
|
||||
import { Subject, filter, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-alert',
|
||||
templateUrl: './alert.component.html',
|
||||
styleUrls: ['./alert.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: fuseAnimations,
|
||||
exportAs: 'fuseAlert',
|
||||
standalone: true,
|
||||
imports: [MatIconModule, MatButtonModule],
|
||||
})
|
||||
export class FuseAlertComponent implements OnChanges, OnInit, OnDestroy {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_dismissible: BooleanInput;
|
||||
static ngAcceptInputType_dismissed: BooleanInput;
|
||||
static ngAcceptInputType_showIcon: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseAlertService = inject(FuseAlertService);
|
||||
private _fuseUtilsService = inject(FuseUtilsService);
|
||||
|
||||
@Input() appearance: FuseAlertAppearance = 'soft';
|
||||
@Input() dismissed: boolean = false;
|
||||
@Input() dismissible: boolean = false;
|
||||
@Input() name: string = this._fuseUtilsService.randomId();
|
||||
@Input() showIcon: boolean = true;
|
||||
@Input() type: FuseAlertType = 'primary';
|
||||
@Output() readonly dismissedChanged: EventEmitter<boolean> =
|
||||
new EventEmitter<boolean>();
|
||||
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Host binding for component classes
|
||||
*/
|
||||
@HostBinding('class') get classList(): any {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
return {
|
||||
'fuse-alert-appearance-border': this.appearance === 'border',
|
||||
'fuse-alert-appearance-fill': this.appearance === 'fill',
|
||||
'fuse-alert-appearance-outline': this.appearance === 'outline',
|
||||
'fuse-alert-appearance-soft': this.appearance === 'soft',
|
||||
'fuse-alert-dismissed': this.dismissed,
|
||||
'fuse-alert-dismissible': this.dismissible,
|
||||
'fuse-alert-show-icon': this.showIcon,
|
||||
'fuse-alert-type-primary': this.type === 'primary',
|
||||
'fuse-alert-type-accent': this.type === 'accent',
|
||||
'fuse-alert-type-warn': this.type === 'warn',
|
||||
'fuse-alert-type-basic': this.type === 'basic',
|
||||
'fuse-alert-type-info': this.type === 'info',
|
||||
'fuse-alert-type-success': this.type === 'success',
|
||||
'fuse-alert-type-warning': this.type === 'warning',
|
||||
'fuse-alert-type-error': this.type === 'error',
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Dismissed
|
||||
if ('dismissed' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.dismissed = coerceBooleanProperty(
|
||||
changes.dismissed.currentValue
|
||||
);
|
||||
|
||||
// Dismiss/show the alert
|
||||
this._toggleDismiss(this.dismissed);
|
||||
}
|
||||
|
||||
// Dismissible
|
||||
if ('dismissible' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.dismissible = coerceBooleanProperty(
|
||||
changes.dismissible.currentValue
|
||||
);
|
||||
}
|
||||
|
||||
// Show icon
|
||||
if ('showIcon' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.showIcon = coerceBooleanProperty(
|
||||
changes.showIcon.currentValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Subscribe to the dismiss calls
|
||||
this._fuseAlertService.onDismiss
|
||||
.pipe(
|
||||
filter((name) => this.name === name),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe(() => {
|
||||
// Dismiss the alert
|
||||
this.dismiss();
|
||||
});
|
||||
|
||||
// Subscribe to the show calls
|
||||
this._fuseAlertService.onShow
|
||||
.pipe(
|
||||
filter((name) => this.name === name),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe(() => {
|
||||
// Show the alert
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Dismiss the alert
|
||||
*/
|
||||
dismiss(): void {
|
||||
// Return if the alert is already dismissed
|
||||
if (this.dismissed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dismiss the alert
|
||||
this._toggleDismiss(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the dismissed alert
|
||||
*/
|
||||
show(): void {
|
||||
// Return if the alert is already showing
|
||||
if (!this.dismissed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the alert
|
||||
this._toggleDismiss(false);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Dismiss/show the alert
|
||||
*
|
||||
* @param dismissed
|
||||
* @private
|
||||
*/
|
||||
private _toggleDismiss(dismissed: boolean): void {
|
||||
// Return if the alert is not dismissible
|
||||
if (!this.dismissible) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the dismissed
|
||||
this.dismissed = dismissed;
|
||||
|
||||
// Execute the observable
|
||||
this.dismissedChanged.next(this.dismissed);
|
||||
|
||||
// Notify the change detector
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FuseAlertService {
|
||||
private readonly _onDismiss: ReplaySubject<string> =
|
||||
new ReplaySubject<string>(1);
|
||||
private readonly _onShow: ReplaySubject<string> = new ReplaySubject<string>(
|
||||
1
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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 alert
|
||||
*
|
||||
* @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 alert
|
||||
*
|
||||
* @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,11 @@
|
||||
export type FuseAlertAppearance = 'border' | 'fill' | 'outline' | 'soft';
|
||||
|
||||
export type FuseAlertType =
|
||||
| 'primary'
|
||||
| 'accent'
|
||||
| 'warn'
|
||||
| 'basic'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'error';
|
||||
1
fuse-starter-v20.0.0/src/@fuse/components/alert/index.ts
Normal file
1
fuse-starter-v20.0.0/src/@fuse/components/alert/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/alert/public-api';
|
||||
@ -0,0 +1,3 @@
|
||||
export * from '@fuse/components/alert/alert.component';
|
||||
export * from '@fuse/components/alert/alert.service';
|
||||
export * from '@fuse/components/alert/alert.types';
|
||||
@ -0,0 +1,25 @@
|
||||
<!-- Flippable card -->
|
||||
@if (flippable) {
|
||||
<!-- Front -->
|
||||
<div class="fuse-card-front">
|
||||
<ng-content select="[fuseCardFront]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Back -->
|
||||
<div class="fuse-card-back">
|
||||
<ng-content select="[fuseCardBack]"></ng-content>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Normal card -->
|
||||
@if (!flippable) {
|
||||
<!-- Content -->
|
||||
<ng-content></ng-content>
|
||||
|
||||
<!-- Expansion -->
|
||||
@if (expanded) {
|
||||
<div class="fuse-card-expansion" [@expandCollapse]>
|
||||
<ng-content select="[fuseCardExpansion]"></ng-content>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
fuse-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
@apply bg-card rounded-2xl shadow;
|
||||
|
||||
/* Flippable */
|
||||
&.fuse-card-flippable {
|
||||
border-radius: 0;
|
||||
overflow: visible;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 1s;
|
||||
perspective: 600px;
|
||||
background: transparent;
|
||||
@apply shadow-none;
|
||||
|
||||
&.fuse-card-face-back {
|
||||
.fuse-card-front {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.fuse-card-back {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: rotateY(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-card-front,
|
||||
.fuse-card-back {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
z-index: 10;
|
||||
transition:
|
||||
transform 0.5s ease-out 0s,
|
||||
visibility 0s ease-in 0.2s,
|
||||
opacity 0s ease-in 0.2s;
|
||||
backface-visibility: hidden;
|
||||
@apply bg-card rounded-2xl shadow;
|
||||
}
|
||||
|
||||
.fuse-card-front {
|
||||
position: relative;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: rotateY(0deg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fuse-card-back {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: rotateY(180deg);
|
||||
overflow: hidden auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
|
||||
import {
|
||||
Component,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { fuseAnimations } from '@fuse/animations';
|
||||
import { FuseCardFace } from '@fuse/components/card/card.types';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-card',
|
||||
templateUrl: './card.component.html',
|
||||
styleUrls: ['./card.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
animations: fuseAnimations,
|
||||
exportAs: 'fuseCard',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
})
|
||||
export class FuseCardComponent implements OnChanges {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_expanded: BooleanInput;
|
||||
static ngAcceptInputType_flippable: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
@Input() expanded: boolean = false;
|
||||
@Input() face: FuseCardFace = 'front';
|
||||
@Input() flippable: boolean = false;
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Host binding for component classes
|
||||
*/
|
||||
@HostBinding('class') get classList(): any {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
return {
|
||||
'fuse-card-expanded': this.expanded,
|
||||
'fuse-card-face-back': this.flippable && this.face === 'back',
|
||||
'fuse-card-face-front': this.flippable && this.face === 'front',
|
||||
'fuse-card-flippable': this.flippable,
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Expanded
|
||||
if ('expanded' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.expanded = coerceBooleanProperty(
|
||||
changes.expanded.currentValue
|
||||
);
|
||||
}
|
||||
|
||||
// Flippable
|
||||
if ('flippable' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.flippable = coerceBooleanProperty(
|
||||
changes.flippable.currentValue
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export type FuseCardFace = 'front' | 'back';
|
||||
1
fuse-starter-v20.0.0/src/@fuse/components/card/index.ts
Normal file
1
fuse-starter-v20.0.0/src/@fuse/components/card/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/card/public-api';
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/card/card.component';
|
||||
@ -0,0 +1,3 @@
|
||||
<div class="fuse-drawer-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
@ -0,0 +1,132 @@
|
||||
/* Variables */
|
||||
:root {
|
||||
--fuse-drawer-width: 320px;
|
||||
}
|
||||
|
||||
fuse-drawer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
width: var(--fuse-drawer-width);
|
||||
min-width: var(--fuse-drawer-width);
|
||||
max-width: var(--fuse-drawer-width);
|
||||
z-index: 300;
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.35);
|
||||
@apply bg-card;
|
||||
|
||||
/* Animations */
|
||||
&.fuse-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;
|
||||
|
||||
.fuse-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 */
|
||||
&.fuse-drawer-mode-over {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
/* Fixed mode */
|
||||
&.fuse-drawer-fixed {
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Left position */
|
||||
&.fuse-drawer-position-left {
|
||||
/* Side mode */
|
||||
&.fuse-drawer-mode-side {
|
||||
margin-left: calc(var(--fuse-drawer-width) * -1);
|
||||
|
||||
&.fuse-drawer-opened {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Over mode */
|
||||
&.fuse-drawer-mode-over {
|
||||
left: 0;
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
|
||||
&.fuse-drawer-opened {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.fuse-drawer-content {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right position */
|
||||
&.fuse-drawer-position-right {
|
||||
/* Side mode */
|
||||
&.fuse-drawer-mode-side {
|
||||
margin-right: calc(var(--fuse-drawer-width) * -1);
|
||||
|
||||
&.fuse-drawer-opened {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Over mode */
|
||||
&.fuse-drawer-mode-over {
|
||||
right: 0;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
|
||||
&.fuse-drawer-opened {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.fuse-drawer-content {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.fuse-drawer-content {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
@apply bg-card;
|
||||
}
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.fuse-drawer-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 299;
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* Fixed mode */
|
||||
&.fuse-drawer-overlay-fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
/* Transparent overlay */
|
||||
&.fuse-drawer-overlay-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,422 @@
|
||||
import {
|
||||
animate,
|
||||
AnimationBuilder,
|
||||
AnimationPlayer,
|
||||
style,
|
||||
} from '@angular/animations';
|
||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
Renderer2,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { FuseDrawerService } from '@fuse/components/drawer/drawer.service';
|
||||
import {
|
||||
FuseDrawerMode,
|
||||
FuseDrawerPosition,
|
||||
} from '@fuse/components/drawer/drawer.types';
|
||||
import { FuseUtilsService } from '@fuse/services/utils/utils.service';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-drawer',
|
||||
templateUrl: './drawer.component.html',
|
||||
styleUrls: ['./drawer.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
exportAs: 'fuseDrawer',
|
||||
standalone: true,
|
||||
})
|
||||
export class FuseDrawerComponent implements OnChanges, OnInit, OnDestroy {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_fixed: BooleanInput;
|
||||
static ngAcceptInputType_opened: BooleanInput;
|
||||
static ngAcceptInputType_transparentOverlay: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
private _animationBuilder = inject(AnimationBuilder);
|
||||
private _elementRef = inject(ElementRef);
|
||||
private _renderer2 = inject(Renderer2);
|
||||
private _fuseDrawerService = inject(FuseDrawerService);
|
||||
private _fuseUtilsService = inject(FuseUtilsService);
|
||||
|
||||
@Input() fixed: boolean = false;
|
||||
@Input() mode: FuseDrawerMode = 'side';
|
||||
@Input() name: string = this._fuseUtilsService.randomId();
|
||||
@Input() opened: boolean = false;
|
||||
@Input() position: FuseDrawerPosition = 'left';
|
||||
@Input() transparentOverlay: boolean = false;
|
||||
@Output() readonly fixedChanged: EventEmitter<boolean> =
|
||||
new EventEmitter<boolean>();
|
||||
@Output() readonly modeChanged: EventEmitter<FuseDrawerMode> =
|
||||
new EventEmitter<FuseDrawerMode>();
|
||||
@Output() readonly openedChanged: EventEmitter<boolean> =
|
||||
new EventEmitter<boolean>();
|
||||
@Output() readonly positionChanged: EventEmitter<FuseDrawerPosition> =
|
||||
new EventEmitter<FuseDrawerPosition>();
|
||||
|
||||
private _animationsEnabled: boolean = false;
|
||||
private readonly _handleOverlayClick = (): void => this.close();
|
||||
private _hovered: boolean = false;
|
||||
private _overlay: HTMLElement;
|
||||
private _player: AnimationPlayer;
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Host binding for component classes
|
||||
*/
|
||||
@HostBinding('class') get classList(): any {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
return {
|
||||
'fuse-drawer-animations-enabled': this._animationsEnabled,
|
||||
'fuse-drawer-fixed': this.fixed,
|
||||
'fuse-drawer-hover': this._hovered,
|
||||
[`fuse-drawer-mode-${this.mode}`]: true,
|
||||
'fuse-drawer-opened': this.opened,
|
||||
[`fuse-drawer-position-${this.position}`]: true,
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}
|
||||
|
||||
/**
|
||||
* Host binding for component inline styles
|
||||
*/
|
||||
@HostBinding('style') get styleList(): any {
|
||||
return {
|
||||
visibility: this.opened ? 'visible' : 'hidden',
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Decorated methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On mouseenter
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
@HostListener('mouseenter')
|
||||
private _onMouseenter(): void {
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// Set the hovered
|
||||
this._hovered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* On mouseleave
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
@HostListener('mouseleave')
|
||||
private _onMouseleave(): void {
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// Set the hovered
|
||||
this._hovered = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Fixed
|
||||
if ('fixed' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.fixed = coerceBooleanProperty(changes.fixed.currentValue);
|
||||
|
||||
// Execute the observable
|
||||
this.fixedChanged.next(this.fixed);
|
||||
}
|
||||
|
||||
// Mode
|
||||
if ('mode' in changes) {
|
||||
// Get the previous and current values
|
||||
const previousMode = changes.mode.previousValue;
|
||||
const currentMode = changes.mode.currentValue;
|
||||
|
||||
// Disable the animations
|
||||
this._disableAnimations();
|
||||
|
||||
// If the mode changes: 'over -> side'
|
||||
if (previousMode === 'over' && currentMode === 'side') {
|
||||
// Hide the overlay
|
||||
this._hideOverlay();
|
||||
}
|
||||
|
||||
// If the mode changes: 'side -> over'
|
||||
if (previousMode === 'side' && currentMode === 'over') {
|
||||
// If the drawer is opened
|
||||
if (this.opened) {
|
||||
// Show the overlay
|
||||
this._showOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the observable
|
||||
this.modeChanged.next(currentMode);
|
||||
|
||||
// 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 is changing
|
||||
setTimeout(() => {
|
||||
this._enableAnimations();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Opened
|
||||
if ('opened' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
const open = coerceBooleanProperty(changes.opened.currentValue);
|
||||
|
||||
// Open/close the drawer
|
||||
this._toggleOpened(open);
|
||||
}
|
||||
|
||||
// Position
|
||||
if ('position' in changes) {
|
||||
// Execute the observable
|
||||
this.positionChanged.next(this.position);
|
||||
}
|
||||
|
||||
// Transparent overlay
|
||||
if ('transparentOverlay' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.transparentOverlay = coerceBooleanProperty(
|
||||
changes.transparentOverlay.currentValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Register the drawer
|
||||
this._fuseDrawerService.registerComponent(this.name, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Finish the animation
|
||||
if (this._player) {
|
||||
this._player.finish();
|
||||
}
|
||||
|
||||
// Deregister the drawer from the registry
|
||||
this._fuseDrawerService.deregisterComponent(this.name);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Open the drawer
|
||||
*/
|
||||
open(): void {
|
||||
// Return if the drawer has already opened
|
||||
if (this.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the drawer
|
||||
this._toggleOpened(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the drawer
|
||||
*/
|
||||
close(): void {
|
||||
// Return if the drawer has already closed
|
||||
if (!this.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the drawer
|
||||
this._toggleOpened(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the drawer
|
||||
*/
|
||||
toggle(): void {
|
||||
if (this.opened) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Enable the animations
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _enableAnimations(): void {
|
||||
// Return if the animations are already enabled
|
||||
if (this._animationsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable the animations
|
||||
this._animationsEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the animations
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _disableAnimations(): void {
|
||||
// Return if the animations are already disabled
|
||||
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('fuse-drawer-overlay');
|
||||
|
||||
// Add a class depending on the fixed option
|
||||
if (this.fixed) {
|
||||
this._overlay.classList.add('fuse-drawer-overlay-fixed');
|
||||
}
|
||||
|
||||
// Add a class depending on the transparentOverlay option
|
||||
if (this.transparentOverlay) {
|
||||
this._overlay.classList.add('fuse-drawer-overlay-transparent');
|
||||
}
|
||||
|
||||
// Append the backdrop to the parent of the drawer
|
||||
this._renderer2.appendChild(
|
||||
this._elementRef.nativeElement.parentElement,
|
||||
this._overlay
|
||||
);
|
||||
|
||||
// Create enter animation and attach it to the player
|
||||
this._player = this._animationBuilder
|
||||
.build([
|
||||
style({ opacity: 0 }),
|
||||
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 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 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open/close the drawer
|
||||
*
|
||||
* @param open
|
||||
* @private
|
||||
*/
|
||||
private _toggleOpened(open: boolean): void {
|
||||
// Set the opened
|
||||
this.opened = open;
|
||||
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// If the mode is 'over'
|
||||
if (this.mode === 'over') {
|
||||
// If the drawer opens, show the overlay
|
||||
if (open) {
|
||||
this._showOverlay();
|
||||
}
|
||||
// Otherwise, close the overlay
|
||||
else {
|
||||
this._hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the observable
|
||||
this.openedChanged.next(open);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FuseDrawerComponent } from '@fuse/components/drawer/drawer.component';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FuseDrawerService {
|
||||
private _componentRegistry: Map<string, FuseDrawerComponent> = new Map<
|
||||
string,
|
||||
FuseDrawerComponent
|
||||
>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register drawer component
|
||||
*
|
||||
* @param name
|
||||
* @param component
|
||||
*/
|
||||
registerComponent(name: string, component: FuseDrawerComponent): 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): FuseDrawerComponent | undefined {
|
||||
return this._componentRegistry.get(name);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export type FuseDrawerMode = 'over' | 'side';
|
||||
|
||||
export type FuseDrawerPosition = 'left' | 'right';
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/drawer/public-api';
|
||||
@ -0,0 +1,3 @@
|
||||
export * from '@fuse/components/drawer/drawer.component';
|
||||
export * from '@fuse/components/drawer/drawer.service';
|
||||
export * from '@fuse/components/drawer/drawer.types';
|
||||
@ -0,0 +1,13 @@
|
||||
<!-- Button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="tooltip || 'Toggle Fullscreen'"
|
||||
(click)="toggleFullscreen()"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="iconTpl || defaultIconTpl"></ng-container>
|
||||
</button>
|
||||
|
||||
<!-- Default icon -->
|
||||
<ng-template #defaultIconTpl>
|
||||
<mat-icon [svgIcon]="'heroicons_outline:arrows-pointing-out'"></mat-icon>
|
||||
</ng-template>
|
||||
@ -0,0 +1,59 @@
|
||||
import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
TemplateRef,
|
||||
ViewEncapsulation,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-fullscreen',
|
||||
templateUrl: './fullscreen.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs: 'fuseFullscreen',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatTooltipModule,
|
||||
NgTemplateOutlet,
|
||||
MatIconModule,
|
||||
],
|
||||
})
|
||||
export class FuseFullscreenComponent {
|
||||
private _document = inject(DOCUMENT);
|
||||
|
||||
@Input() iconTpl: TemplateRef<any>;
|
||||
@Input() tooltip: string;
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toggle the fullscreen mode
|
||||
*/
|
||||
toggleFullscreen(): void {
|
||||
if (!this._document.fullscreenEnabled) {
|
||||
console.log('Fullscreen is not available in this browser.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the fullscreen is already open
|
||||
const fullScreen = this._document.fullscreenElement;
|
||||
|
||||
// Toggle the fullscreen
|
||||
if (fullScreen) {
|
||||
this._document.exitFullscreen();
|
||||
} else {
|
||||
this._document.documentElement.requestFullscreen().catch(() => {
|
||||
console.error('Entering fullscreen mode failed.');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/fullscreen/public-api';
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/fullscreen/fullscreen.component';
|
||||
@ -0,0 +1,8 @@
|
||||
<ng-content></ng-content>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<ng-template let-highlightedCode="highlightedCode" let-lang="lang">
|
||||
<div class="fuse-highlight fuse-highlight-code-container">
|
||||
<pre [ngClass]="'language-' + lang"><code [ngClass]="'language-' + lang" [innerHTML]="highlightedCode"></code></pre>
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -0,0 +1,3 @@
|
||||
textarea[fuse-highlight] {
|
||||
display: none;
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
EmbeddedViewRef,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
SecurityContext,
|
||||
SimpleChanges,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { FuseHighlightService } from '@fuse/components/highlight/highlight.service';
|
||||
|
||||
@Component({
|
||||
selector: 'textarea[fuse-highlight]',
|
||||
templateUrl: './highlight.component.html',
|
||||
styleUrls: ['./highlight.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs: 'fuseHighlight',
|
||||
standalone: true,
|
||||
imports: [NgClass],
|
||||
})
|
||||
export class FuseHighlightComponent implements OnChanges, AfterViewInit {
|
||||
private _domSanitizer = inject(DomSanitizer);
|
||||
private _elementRef = inject(ElementRef);
|
||||
private _fuseHighlightService = inject(FuseHighlightService);
|
||||
private _viewContainerRef = inject(ViewContainerRef);
|
||||
|
||||
@Input() code: string;
|
||||
@Input() lang: string;
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
|
||||
highlightedCode: string;
|
||||
private _viewRef: EmbeddedViewRef<any>;
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Code & Lang
|
||||
if ('code' in changes || 'lang' in changes) {
|
||||
// Return if the viewContainerRef is not available
|
||||
if (!this._viewContainerRef.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight and insert the code
|
||||
this._highlightAndInsert();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 template reference is not available
|
||||
if (!this.templateRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
this._viewRef = null;
|
||||
}
|
||||
|
||||
// Highlight and sanitize the code just in case
|
||||
this.highlightedCode = this._domSanitizer.sanitize(
|
||||
SecurityContext.HTML,
|
||||
this._fuseHighlightService.highlight(this.code, this.lang)
|
||||
);
|
||||
|
||||
// Return if the highlighted code is null
|
||||
if (this.highlightedCode === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,67 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FuseHighlightService {
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Highlight
|
||||
*/
|
||||
highlight(code: string, language: string): string {
|
||||
// Format the code
|
||||
code = this._format(code);
|
||||
|
||||
// Highlight and return the code
|
||||
return hljs.highlight(code, { language }).value;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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 indentation = 0;
|
||||
|
||||
// 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
|
||||
lines
|
||||
.filter((line) => line.length)
|
||||
.forEach((line, index) => {
|
||||
// Always get the indentation of the first line so we can
|
||||
// have something to compare with
|
||||
if (index === 0) {
|
||||
indentation = line.search(/\S|$/);
|
||||
return;
|
||||
}
|
||||
|
||||
// Look at all the remaining lines to figure out the smallest indentation.
|
||||
indentation = Math.min(line.search(/\S|$/), indentation);
|
||||
});
|
||||
|
||||
// Iterate through the lines one more time, remove the extra
|
||||
// indentation, join them together and return it
|
||||
return lines.map((line) => line.substring(indentation)).join('\n');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/highlight/public-api';
|
||||
@ -0,0 +1,2 @@
|
||||
export * from '@fuse/components/highlight/highlight.component';
|
||||
export * from '@fuse/components/highlight/highlight.service';
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/loading-bar/public-api';
|
||||
@ -0,0 +1,3 @@
|
||||
@if (show) {
|
||||
<mat-progress-bar [mode]="mode" [value]="progress"></mat-progress-bar>
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fuse-loading-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { FuseLoadingService } from '@fuse/services/loading';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-loading-bar',
|
||||
templateUrl: './loading-bar.component.html',
|
||||
styleUrls: ['./loading-bar.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
exportAs: 'fuseLoadingBar',
|
||||
standalone: true,
|
||||
imports: [MatProgressBarModule],
|
||||
})
|
||||
export class FuseLoadingBarComponent implements OnChanges, OnInit, OnDestroy {
|
||||
private _fuseLoadingService = inject(FuseLoadingService);
|
||||
|
||||
@Input() autoMode: boolean = true;
|
||||
mode: 'determinate' | 'indeterminate';
|
||||
progress: number = 0;
|
||||
show: boolean = false;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Auto mode
|
||||
if ('autoMode' in changes) {
|
||||
// Set the auto mode in the service
|
||||
this._fuseLoadingService.setAutoMode(
|
||||
coerceBooleanProperty(changes.autoMode.currentValue)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Subscribe to the service
|
||||
this._fuseLoadingService.mode$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((value) => {
|
||||
this.mode = value;
|
||||
});
|
||||
|
||||
|
||||
this._fuseLoadingService.progress$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((value) => {
|
||||
this.progress = value;
|
||||
});
|
||||
|
||||
this._fuseLoadingService.show$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((value) => {
|
||||
this.show = value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/loading-bar/loading-bar.component';
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/masonry/public-api';
|
||||
@ -0,0 +1,8 @@
|
||||
<div class="flex">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
columnsTemplate;
|
||||
context: { $implicit: distributedColumns }
|
||||
"
|
||||
></ng-container>
|
||||
</div>
|
||||
@ -0,0 +1,83 @@
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
TemplateRef,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { fuseAnimations } from '@fuse/animations';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-masonry',
|
||||
templateUrl: './masonry.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
animations: fuseAnimations,
|
||||
exportAs: 'fuseMasonry',
|
||||
standalone: true,
|
||||
imports: [NgTemplateOutlet],
|
||||
})
|
||||
export class FuseMasonryComponent implements OnChanges, AfterViewInit {
|
||||
@Input() columnsTemplate: TemplateRef<any>;
|
||||
@Input() columns: number;
|
||||
@Input() items: any[] = [];
|
||||
distributedColumns: any[] = [];
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Columns
|
||||
if ('columns' in changes) {
|
||||
// Distribute the items
|
||||
this._distributeItems();
|
||||
}
|
||||
|
||||
// Items
|
||||
if ('items' in changes) {
|
||||
// Distribute the items
|
||||
this._distributeItems();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After view init
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
// Distribute the items for the first time
|
||||
this._distributeItems();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Distribute items into columns
|
||||
*/
|
||||
private _distributeItems(): void {
|
||||
// Return an empty array if there are no items
|
||||
if (this.items.length === 0) {
|
||||
this.distributedColumns = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the distributed columns array
|
||||
this.distributedColumns = Array.from(Array(this.columns), (item) => ({
|
||||
items: [],
|
||||
}));
|
||||
|
||||
// Distribute the items to columns
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
this.distributedColumns[i % this.columns].items.push(this.items[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/components/masonry/masonry.component';
|
||||
@ -0,0 +1,149 @@
|
||||
<!-- Item wrapper -->
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item-wrapper"
|
||||
[class.fuse-horizontal-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
>
|
||||
<!-- Item with an internal link -->
|
||||
@if (item.link && !item.externalLink && !item.function && !item.disabled) {
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-horizontal-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[routerLink]="[item.link]"
|
||||
[fragment]="item.fragment ?? null"
|
||||
[preserveFragment]="item.preserveFragment ?? false"
|
||||
[queryParams]="item.queryParams ?? null"
|
||||
[queryParamsHandling]="item.queryParamsHandling ?? null"
|
||||
[routerLinkActive]="'fuse-horizontal-navigation-item-active'"
|
||||
[routerLinkActiveOptions]="isActiveMatchOptions"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Item with an external link -->
|
||||
@if (item.link && item.externalLink && !item.function && !item.disabled) {
|
||||
<a
|
||||
class="fuse-horizontal-navigation-item"
|
||||
[href]="item.link"
|
||||
[target]="item.target || '_self'"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- Item with a function -->
|
||||
@if (!item.link && item.function && !item.disabled) {
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-horizontal-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
(click)="item.function(item)"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Item with an internal link and function -->
|
||||
@if (item.link && !item.externalLink && item.function && !item.disabled) {
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-horizontal-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[routerLink]="[item.link]"
|
||||
[fragment]="item.fragment ?? null"
|
||||
[preserveFragment]="item.preserveFragment ?? false"
|
||||
[queryParams]="item.queryParams ?? null"
|
||||
[queryParamsHandling]="item.queryParamsHandling ?? null"
|
||||
[routerLinkActive]="'fuse-horizontal-navigation-item-active'"
|
||||
[routerLinkActiveOptions]="isActiveMatchOptions"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
(click)="item.function(item)"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Item with an external link and function -->
|
||||
@if (item.link && item.externalLink && item.function && !item.disabled) {
|
||||
<a
|
||||
class="fuse-horizontal-navigation-item"
|
||||
[href]="item.link"
|
||||
[target]="item.target || '_self'"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
(click)="item.function(item)"
|
||||
mat-menu-item
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- Item with a no link and no function -->
|
||||
@if (!item.link && !item.function && !item.disabled) {
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-horizontal-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Item is disabled -->
|
||||
@if (item.disabled) {
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item fuse-horizontal-navigation-item-disabled"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Item template -->
|
||||
<ng-template #itemTemplate>
|
||||
<!-- Icon -->
|
||||
@if (item.icon) {
|
||||
<mat-icon
|
||||
class="fuse-horizontal-navigation-item-icon"
|
||||
[ngClass]="item.classes?.icon"
|
||||
[svgIcon]="item.icon"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="fuse-horizontal-navigation-item-title-wrapper">
|
||||
<div class="fuse-horizontal-navigation-item-title">
|
||||
<span [ngClass]="item.classes?.title">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
@if (item.subtitle) {
|
||||
<div class="fuse-horizontal-navigation-item-subtitle text-hint">
|
||||
<span [ngClass]="item.classes?.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
@if (item.badge) {
|
||||
<div class="fuse-horizontal-navigation-item-badge">
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item-badge-content"
|
||||
[ngClass]="item.badge.classes"
|
||||
>
|
||||
{{ item.badge.title }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
@ -0,0 +1,100 @@
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import {
|
||||
IsActiveMatchOptions,
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
} from '@angular/router';
|
||||
import { FuseHorizontalNavigationComponent } from '@fuse/components/navigation/horizontal/horizontal.component';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseUtilsService } from '@fuse/services/utils/utils.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-horizontal-navigation-basic-item',
|
||||
templateUrl: './basic.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgClass,
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
MatTooltipModule,
|
||||
NgTemplateOutlet,
|
||||
MatMenuModule,
|
||||
MatIconModule,
|
||||
],
|
||||
})
|
||||
export class FuseHorizontalNavigationBasicItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
private _fuseUtilsService = inject(FuseUtilsService);
|
||||
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
|
||||
// Set the equivalent of {exact: false} as default for active match options.
|
||||
// We are not assigning the item.isActiveMatchOptions directly to the
|
||||
// [routerLinkActiveOptions] because if it's "undefined" initially, the router
|
||||
// will throw an error and stop working.
|
||||
isActiveMatchOptions: IsActiveMatchOptions =
|
||||
this._fuseUtilsService.subsetMatchOptions;
|
||||
|
||||
private _fuseHorizontalNavigationComponent: FuseHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Set the "isActiveMatchOptions" either from item's
|
||||
// "isActiveMatchOptions" or the equivalent form of
|
||||
// item's "exactMatch" option
|
||||
this.isActiveMatchOptions =
|
||||
this.item.isActiveMatchOptions ?? this.item.exactMatch
|
||||
? this._fuseUtilsService.exactMatchOptions
|
||||
: this._fuseUtilsService.subsetMatchOptions;
|
||||
|
||||
// Get the parent navigation component
|
||||
this._fuseHorizontalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseHorizontalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
@if (!child) {
|
||||
<div
|
||||
[ngClass]="{
|
||||
'fuse-horizontal-navigation-menu-active': trigger.menuOpen,
|
||||
'fuse-horizontal-navigation-menu-active-forced': item.active,
|
||||
}"
|
||||
[matMenuTriggerFor]="matMenu"
|
||||
(onMenuOpen)="triggerChangeDetection()"
|
||||
(onMenuClose)="triggerChangeDetection()"
|
||||
#trigger="matMenuTrigger"
|
||||
>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="itemTemplate; context: { $implicit: item }"
|
||||
></ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-menu
|
||||
class="fuse-horizontal-navigation-menu-panel"
|
||||
[overlapTrigger]="false"
|
||||
#matMenu="matMenu"
|
||||
>
|
||||
@for (item of item.children; track trackByFn($index, item)) {
|
||||
<!-- Skip the hidden items -->
|
||||
@if ((item.hidden && !item.hidden(item)) || !item.hidden) {
|
||||
<!-- Basic -->
|
||||
@if (item.type === 'basic') {
|
||||
<div
|
||||
class="fuse-horizontal-navigation-menu-item"
|
||||
[disabled]="item.disabled"
|
||||
mat-menu-item
|
||||
>
|
||||
<fuse-horizontal-navigation-basic-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-horizontal-navigation-basic-item>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Branch: aside, collapsable, group -->
|
||||
@if (
|
||||
item.type === 'aside' ||
|
||||
item.type === 'collapsable' ||
|
||||
item.type === 'group'
|
||||
) {
|
||||
<div
|
||||
class="fuse-horizontal-navigation-menu-item"
|
||||
[disabled]="item.disabled"
|
||||
[matMenuTriggerFor]="branch.matMenu"
|
||||
mat-menu-item
|
||||
>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
itemTemplate;
|
||||
context: { $implicit: item }
|
||||
"
|
||||
></ng-container>
|
||||
<fuse-horizontal-navigation-branch-item
|
||||
[child]="true"
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
#branch
|
||||
></fuse-horizontal-navigation-branch-item>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Divider -->
|
||||
@if (item.type === 'divider') {
|
||||
<div class="fuse-horizontal-navigation-menu-item" mat-menu-item>
|
||||
<fuse-horizontal-navigation-divider-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-horizontal-navigation-divider-item>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</mat-menu>
|
||||
|
||||
<!-- Item template -->
|
||||
<ng-template let-item #itemTemplate>
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item-wrapper"
|
||||
[class.fuse-horizontal-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
>
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-horizontal-navigation-item-disabled': item.disabled,
|
||||
'fuse-horizontal-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<!-- Icon -->
|
||||
@if (item.icon) {
|
||||
<mat-icon
|
||||
class="fuse-horizontal-navigation-item-icon"
|
||||
[ngClass]="item.classes?.icon"
|
||||
[svgIcon]="item.icon"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="fuse-horizontal-navigation-item-title-wrapper">
|
||||
<div class="fuse-horizontal-navigation-item-title">
|
||||
<span [ngClass]="item.classes?.title">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
@if (item.subtitle) {
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item-subtitle text-hint"
|
||||
>
|
||||
<span [ngClass]="item.classes?.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
@if (item.badge) {
|
||||
<div class="fuse-horizontal-navigation-item-badge">
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item-badge-content"
|
||||
[ngClass]="item.badge.classes"
|
||||
>
|
||||
{{ item.badge.title }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -0,0 +1,109 @@
|
||||
import { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
forwardRef,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenu, MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FuseHorizontalNavigationBasicItemComponent } from '@fuse/components/navigation/horizontal/components/basic/basic.component';
|
||||
import { FuseHorizontalNavigationDividerItemComponent } from '@fuse/components/navigation/horizontal/components/divider/divider.component';
|
||||
import { FuseHorizontalNavigationComponent } from '@fuse/components/navigation/horizontal/horizontal.component';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-horizontal-navigation-branch-item',
|
||||
templateUrl: './branch.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgClass,
|
||||
MatMenuModule,
|
||||
NgTemplateOutlet,
|
||||
FuseHorizontalNavigationBasicItemComponent,
|
||||
forwardRef(() => FuseHorizontalNavigationBranchItemComponent),
|
||||
FuseHorizontalNavigationDividerItemComponent,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
],
|
||||
})
|
||||
export class FuseHorizontalNavigationBranchItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_child: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
|
||||
@Input() child: boolean = false;
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
@ViewChild('matMenu', { static: true }) matMenu: MatMenu;
|
||||
|
||||
private _fuseHorizontalNavigationComponent: FuseHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Get the parent navigation component
|
||||
this._fuseHorizontalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseHorizontalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Trigger the change detection
|
||||
*/
|
||||
triggerChangeDetection(): void {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any {
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<!-- Divider -->
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item-wrapper divider"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
></div>
|
||||
@ -0,0 +1,64 @@
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { FuseHorizontalNavigationComponent } from '@fuse/components/navigation/horizontal/horizontal.component';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-horizontal-navigation-divider-item',
|
||||
templateUrl: './divider.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgClass],
|
||||
})
|
||||
export class FuseHorizontalNavigationDividerItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
|
||||
private _fuseHorizontalNavigationComponent: FuseHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Get the parent navigation component
|
||||
this._fuseHorizontalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseHorizontalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<!-- Spacer -->
|
||||
<div
|
||||
class="fuse-horizontal-navigation-item-wrapper"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
></div>
|
||||
@ -0,0 +1,64 @@
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { FuseHorizontalNavigationComponent } from '@fuse/components/navigation/horizontal/horizontal.component';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-horizontal-navigation-spacer-item',
|
||||
templateUrl: './spacer.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgClass],
|
||||
})
|
||||
export class FuseHorizontalNavigationSpacerItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
|
||||
private _fuseHorizontalNavigationComponent: FuseHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Get the parent navigation component
|
||||
this._fuseHorizontalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseHorizontalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
<div class="fuse-horizontal-navigation-wrapper">
|
||||
@for (item of navigation; track trackByFn($index, item)) {
|
||||
<!-- Skip the hidden items -->
|
||||
@if ((item.hidden && !item.hidden(item)) || !item.hidden) {
|
||||
<!-- Basic -->
|
||||
@if (item.type === 'basic') {
|
||||
<fuse-horizontal-navigation-basic-item
|
||||
class="fuse-horizontal-navigation-menu-item"
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-horizontal-navigation-basic-item>
|
||||
}
|
||||
|
||||
<!-- Branch: aside, collapsable, group -->
|
||||
@if (
|
||||
item.type === 'aside' ||
|
||||
item.type === 'collapsable' ||
|
||||
item.type === 'group'
|
||||
) {
|
||||
<fuse-horizontal-navigation-branch-item
|
||||
class="fuse-horizontal-navigation-menu-item"
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-horizontal-navigation-branch-item>
|
||||
}
|
||||
|
||||
<!-- Spacer -->
|
||||
@if (item.type === 'spacer') {
|
||||
<fuse-horizontal-navigation-spacer-item
|
||||
class="fuse-horizontal-navigation-menu-item"
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-horizontal-navigation-spacer-item>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@ -0,0 +1,166 @@
|
||||
/* Root navigation specific */
|
||||
fuse-horizontal-navigation {
|
||||
.fuse-horizontal-navigation-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
/* Basic, Branch */
|
||||
fuse-horizontal-navigation-basic-item,
|
||||
fuse-horizontal-navigation-branch-item {
|
||||
@screen sm {
|
||||
&:hover {
|
||||
.fuse-horizontal-navigation-item-wrapper {
|
||||
@apply bg-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-horizontal-navigation-item-wrapper {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.fuse-horizontal-navigation-item {
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
.fuse-horizontal-navigation-item-icon {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Basic - When item active (current link) */
|
||||
fuse-horizontal-navigation-basic-item {
|
||||
.fuse-horizontal-navigation-item-active,
|
||||
.fuse-horizontal-navigation-item-active-forced {
|
||||
.fuse-horizontal-navigation-item-title {
|
||||
@apply text-primary #{'!important'};
|
||||
}
|
||||
|
||||
.fuse-horizontal-navigation-item-subtitle {
|
||||
@apply text-primary-400 #{'!important'};
|
||||
|
||||
.dark & {
|
||||
@apply text-primary-600 #{'!important'};
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-horizontal-navigation-item-icon {
|
||||
@apply text-primary #{'!important'};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Branch - When menu open */
|
||||
fuse-horizontal-navigation-branch-item {
|
||||
.fuse-horizontal-navigation-menu-active,
|
||||
.fuse-horizontal-navigation-menu-active-forced {
|
||||
.fuse-horizontal-navigation-item-wrapper {
|
||||
@apply bg-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Spacer */
|
||||
fuse-horizontal-navigation-spacer-item {
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Menu panel specific */
|
||||
.fuse-horizontal-navigation-menu-panel {
|
||||
.fuse-horizontal-navigation-menu-item {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
|
||||
/* Basic, Branch */
|
||||
fuse-horizontal-navigation-basic-item,
|
||||
fuse-horizontal-navigation-branch-item,
|
||||
fuse-horizontal-navigation-divider-item {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
fuse-horizontal-navigation-divider-item {
|
||||
margin: 8px -16px;
|
||||
|
||||
.fuse-horizontal-navigation-item-wrapper {
|
||||
height: 1px;
|
||||
box-shadow: 0 1px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Navigation menu item common */
|
||||
.fuse-horizontal-navigation-menu-item {
|
||||
/* Basic - When item active (current link) */
|
||||
fuse-horizontal-navigation-basic-item {
|
||||
.fuse-horizontal-navigation-item-active,
|
||||
.fuse-horizontal-navigation-item-active-forced {
|
||||
.fuse-horizontal-navigation-item-title {
|
||||
@apply text-primary #{'!important'};
|
||||
}
|
||||
|
||||
.fuse-horizontal-navigation-item-subtitle {
|
||||
@apply text-primary-400 #{'!important'};
|
||||
|
||||
.dark & {
|
||||
@apply text-primary-600 #{'!important'};
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-horizontal-navigation-item-icon {
|
||||
@apply text-primary #{'!important'};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-horizontal-navigation-item-wrapper {
|
||||
width: 100%;
|
||||
|
||||
&.fuse-horizontal-navigation-item-has-subtitle {
|
||||
.fuse-horizontal-navigation-item {
|
||||
min-height: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-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;
|
||||
|
||||
.fuse-horizontal-navigation-item-title-wrapper {
|
||||
.fuse-horizontal-navigation-item-subtitle {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-horizontal-navigation-item-badge {
|
||||
margin-left: auto;
|
||||
|
||||
.fuse-horizontal-navigation-item-badge-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { fuseAnimations } from '@fuse/animations';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseUtilsService } from '@fuse/services/utils/utils.service';
|
||||
import { ReplaySubject, Subject } from 'rxjs';
|
||||
import { FuseHorizontalNavigationBasicItemComponent } from './components/basic/basic.component';
|
||||
import { FuseHorizontalNavigationBranchItemComponent } from './components/branch/branch.component';
|
||||
import { FuseHorizontalNavigationSpacerItemComponent } from './components/spacer/spacer.component';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-horizontal-navigation',
|
||||
templateUrl: './horizontal.component.html',
|
||||
styleUrls: ['./horizontal.component.scss'],
|
||||
animations: fuseAnimations,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs: 'fuseHorizontalNavigation',
|
||||
standalone: true,
|
||||
imports: [
|
||||
FuseHorizontalNavigationBasicItemComponent,
|
||||
FuseHorizontalNavigationBranchItemComponent,
|
||||
FuseHorizontalNavigationSpacerItemComponent,
|
||||
],
|
||||
})
|
||||
export class FuseHorizontalNavigationComponent
|
||||
implements OnChanges, OnInit, OnDestroy
|
||||
{
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
private _fuseUtilsService = inject(FuseUtilsService);
|
||||
|
||||
@Input() name: string = this._fuseUtilsService.randomId();
|
||||
@Input() navigation: FuseNavigationItem[];
|
||||
|
||||
onRefreshed: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Navigation
|
||||
if ('navigation' in changes) {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Make sure the name input is not an empty string
|
||||
if (this.name === '') {
|
||||
this.name = this._fuseUtilsService.randomId();
|
||||
}
|
||||
|
||||
// Register the navigation component
|
||||
this._fuseNavigationService.registerComponent(this.name, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Deregister the navigation component from the registry
|
||||
this._fuseNavigationService.deregisterComponent(this.name);
|
||||
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
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 '@fuse/components/navigation/public-api';
|
||||
@ -0,0 +1,169 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FuseNavigationService {
|
||||
private _componentRegistry: Map<string, any> = new Map<string, any>();
|
||||
private _navigationStore: Map<string, FuseNavigationItem[]> = 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<T>(name: string): T {
|
||||
return this._componentRegistry.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the given navigation with the given key
|
||||
*
|
||||
* @param key
|
||||
* @param navigation
|
||||
*/
|
||||
storeNavigation(key: string, navigation: FuseNavigationItem[]): void {
|
||||
// Add to the store
|
||||
this._navigationStore.set(key, navigation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation from storage by key
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
getNavigation(key: string): FuseNavigationItem[] {
|
||||
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
|
||||
*/
|
||||
getFlatNavigation(
|
||||
navigation: FuseNavigationItem[],
|
||||
flatNavigation: FuseNavigationItem[] = []
|
||||
): FuseNavigationItem[] {
|
||||
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: FuseNavigationItem[]
|
||||
): FuseNavigationItem | 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: FuseNavigationItem[],
|
||||
parent: FuseNavigationItem[] | FuseNavigationItem
|
||||
): FuseNavigationItem[] | FuseNavigationItem | 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,49 @@
|
||||
import {
|
||||
IsActiveMatchOptions,
|
||||
Params,
|
||||
QueryParamsHandling,
|
||||
} from '@angular/router';
|
||||
|
||||
export interface FuseNavigationItem {
|
||||
id?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
type: 'aside' | 'basic' | 'collapsable' | 'divider' | 'group' | 'spacer';
|
||||
hidden?: (item: FuseNavigationItem) => boolean;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
link?: string;
|
||||
fragment?: string;
|
||||
preserveFragment?: boolean;
|
||||
queryParams?: Params | null;
|
||||
queryParamsHandling?: QueryParamsHandling | null;
|
||||
externalLink?: boolean;
|
||||
target?: '_blank' | '_self' | '_parent' | '_top' | string;
|
||||
exactMatch?: boolean;
|
||||
isActiveMatchOptions?: IsActiveMatchOptions;
|
||||
function?: (item: FuseNavigationItem) => void;
|
||||
classes?: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
icon?: string;
|
||||
wrapper?: string;
|
||||
};
|
||||
icon?: string;
|
||||
badge?: {
|
||||
title?: string;
|
||||
classes?: string;
|
||||
};
|
||||
children?: FuseNavigationItem[];
|
||||
meta?: any;
|
||||
}
|
||||
|
||||
export type FuseVerticalNavigationAppearance =
|
||||
| 'default'
|
||||
| 'compact'
|
||||
| 'dense'
|
||||
| 'thin';
|
||||
|
||||
export type FuseVerticalNavigationMode = 'over' | 'side';
|
||||
|
||||
export type FuseVerticalNavigationPosition = 'left' | 'right';
|
||||
@ -0,0 +1,4 @@
|
||||
export * from '@fuse/components/navigation/horizontal/horizontal.component';
|
||||
export * from '@fuse/components/navigation/navigation.service';
|
||||
export * from '@fuse/components/navigation/navigation.types';
|
||||
export * from '@fuse/components/navigation/vertical/vertical.component';
|
||||
@ -0,0 +1,102 @@
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-wrapper"
|
||||
[class.fuse-vertical-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
>
|
||||
<div
|
||||
class="fuse-vertical-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-vertical-navigation-item-active': active,
|
||||
'fuse-vertical-navigation-item-disabled': item.disabled,
|
||||
'fuse-vertical-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<!-- Icon -->
|
||||
@if (item.icon) {
|
||||
<mat-icon
|
||||
class="fuse-vertical-navigation-item-icon"
|
||||
[ngClass]="item.classes?.icon"
|
||||
[svgIcon]="item.icon"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="fuse-vertical-navigation-item-title-wrapper">
|
||||
<div class="fuse-vertical-navigation-item-title">
|
||||
<span [ngClass]="item.classes?.title">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
@if (item.subtitle) {
|
||||
<div class="fuse-vertical-navigation-item-subtitle">
|
||||
<span [ngClass]="item.classes?.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
@if (item.badge) {
|
||||
<div class="fuse-vertical-navigation-item-badge">
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-badge-content"
|
||||
[ngClass]="item.badge.classes"
|
||||
>
|
||||
{{ item.badge.title }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!skipChildren) {
|
||||
<div class="fuse-vertical-navigation-item-children">
|
||||
@for (item of item.children; track trackByFn($index, item)) {
|
||||
<!-- Skip the hidden items -->
|
||||
@if ((item.hidden && !item.hidden(item)) || !item.hidden) {
|
||||
<!-- Basic -->
|
||||
@if (item.type === 'basic') {
|
||||
<fuse-vertical-navigation-basic-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-basic-item>
|
||||
}
|
||||
|
||||
<!-- Collapsable -->
|
||||
@if (item.type === 'collapsable') {
|
||||
<fuse-vertical-navigation-collapsable-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"
|
||||
></fuse-vertical-navigation-collapsable-item>
|
||||
}
|
||||
|
||||
<!-- Divider -->
|
||||
@if (item.type === 'divider') {
|
||||
<fuse-vertical-navigation-divider-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-divider-item>
|
||||
}
|
||||
|
||||
<!-- Group -->
|
||||
@if (item.type === 'group') {
|
||||
<fuse-vertical-navigation-group-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-group-item>
|
||||
}
|
||||
|
||||
<!-- Spacer -->
|
||||
@if (item.type === 'spacer') {
|
||||
<fuse-vertical-navigation-spacer-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-spacer-item>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@ -0,0 +1,203 @@
|
||||
import { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseVerticalNavigationBasicItemComponent } from '@fuse/components/navigation/vertical/components/basic/basic.component';
|
||||
import { FuseVerticalNavigationCollapsableItemComponent } from '@fuse/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { FuseVerticalNavigationDividerItemComponent } from '@fuse/components/navigation/vertical/components/divider/divider.component';
|
||||
import { FuseVerticalNavigationGroupItemComponent } from '@fuse/components/navigation/vertical/components/group/group.component';
|
||||
import { FuseVerticalNavigationSpacerItemComponent } from '@fuse/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component';
|
||||
import { Subject, filter, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-vertical-navigation-aside-item',
|
||||
templateUrl: './aside.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgClass,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
FuseVerticalNavigationBasicItemComponent,
|
||||
FuseVerticalNavigationCollapsableItemComponent,
|
||||
FuseVerticalNavigationDividerItemComponent,
|
||||
FuseVerticalNavigationGroupItemComponent,
|
||||
FuseVerticalNavigationSpacerItemComponent,
|
||||
],
|
||||
})
|
||||
export class FuseVerticalNavigationAsideItemComponent
|
||||
implements OnChanges, OnInit, OnDestroy
|
||||
{
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_autoCollapse: BooleanInput;
|
||||
static ngAcceptInputType_skipChildren: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _router = inject(Router);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
|
||||
@Input() activeItemId: string;
|
||||
@Input() autoCollapse: boolean;
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
@Input() skipChildren: boolean;
|
||||
|
||||
active: boolean = false;
|
||||
private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Active item id
|
||||
if ('activeItemId' in changes) {
|
||||
// Mark if active
|
||||
this._markIfActive(this._router.url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Mark if active
|
||||
this._markIfActive(this._router.url);
|
||||
|
||||
// Attach a listener to the NavigationEnd event
|
||||
this._router.events
|
||||
.pipe(
|
||||
filter(
|
||||
(event): event is NavigationEnd =>
|
||||
event instanceof NavigationEnd
|
||||
),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe((event: NavigationEnd) => {
|
||||
// Mark if active
|
||||
this._markIfActive(event.urlAfterRedirects);
|
||||
});
|
||||
|
||||
// Get the parent navigation component
|
||||
this._fuseVerticalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseVerticalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
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;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if the given item has the given url
|
||||
* in one of its children
|
||||
*
|
||||
* @param item
|
||||
* @param currentUrl
|
||||
* @private
|
||||
*/
|
||||
private _hasActiveChild(
|
||||
item: FuseNavigationItem,
|
||||
currentUrl: string
|
||||
): boolean {
|
||||
const children = item.children;
|
||||
|
||||
if (!children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
if (child.children) {
|
||||
if (this._hasActiveChild(child, currentUrl)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip items other than 'basic'
|
||||
if (child.type !== 'basic') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the child has a link and is active
|
||||
if (
|
||||
child.link &&
|
||||
this._router.isActive(child.link, child.exactMatch || false)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide and mark if the item is active
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _markIfActive(currentUrl: string): void {
|
||||
// Check if the activeItemId is equals to this item id
|
||||
this.active = this.activeItemId === this.item.id;
|
||||
|
||||
// If the aside has a children that is active,
|
||||
// always mark it as active
|
||||
if (this._hasActiveChild(this.item, currentUrl)) {
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
<!-- Item wrapper -->
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-wrapper"
|
||||
[class.fuse-vertical-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
>
|
||||
<!-- Item with an internal link -->
|
||||
@if (item.link && !item.externalLink && !item.function && !item.disabled) {
|
||||
<a
|
||||
class="fuse-vertical-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-vertical-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[routerLink]="[item.link]"
|
||||
[fragment]="item.fragment ?? null"
|
||||
[preserveFragment]="item.preserveFragment ?? false"
|
||||
[queryParams]="item.queryParams ?? null"
|
||||
[queryParamsHandling]="item.queryParamsHandling ?? null"
|
||||
[routerLinkActive]="'fuse-vertical-navigation-item-active'"
|
||||
[routerLinkActiveOptions]="isActiveMatchOptions"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- Item with an external link -->
|
||||
@if (item.link && item.externalLink && !item.function && !item.disabled) {
|
||||
<a
|
||||
class="fuse-vertical-navigation-item"
|
||||
[href]="item.link"
|
||||
[target]="item.target || '_self'"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- Item with a function -->
|
||||
@if (!item.link && item.function && !item.disabled) {
|
||||
<div
|
||||
class="fuse-vertical-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-vertical-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
(click)="item.function(item)"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Item with an internal link and function -->
|
||||
@if (item.link && !item.externalLink && item.function && !item.disabled) {
|
||||
<a
|
||||
class="fuse-vertical-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-vertical-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[routerLink]="[item.link]"
|
||||
[fragment]="item.fragment ?? null"
|
||||
[preserveFragment]="item.preserveFragment ?? false"
|
||||
[queryParams]="item.queryParams ?? null"
|
||||
[queryParamsHandling]="item.queryParamsHandling ?? null"
|
||||
[routerLinkActive]="'fuse-vertical-navigation-item-active'"
|
||||
[routerLinkActiveOptions]="isActiveMatchOptions"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
(click)="item.function(item)"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- Item with an external link and function -->
|
||||
@if (item.link && item.externalLink && item.function && !item.disabled) {
|
||||
<a
|
||||
class="fuse-vertical-navigation-item"
|
||||
[href]="item.link"
|
||||
[target]="item.target || '_self'"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
(click)="item.function(item)"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- Item with a no link and no function -->
|
||||
@if (!item.link && !item.function && !item.disabled) {
|
||||
<div
|
||||
class="fuse-vertical-navigation-item"
|
||||
[ngClass]="{
|
||||
'fuse-vertical-navigation-item-active-forced': item.active,
|
||||
}"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Item is disabled -->
|
||||
@if (item.disabled) {
|
||||
<div
|
||||
class="fuse-vertical-navigation-item fuse-vertical-navigation-item-disabled"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Item template -->
|
||||
<ng-template #itemTemplate>
|
||||
<!-- Icon -->
|
||||
@if (item.icon) {
|
||||
<mat-icon
|
||||
class="fuse-vertical-navigation-item-icon"
|
||||
[ngClass]="item.classes?.icon"
|
||||
[svgIcon]="item.icon"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="fuse-vertical-navigation-item-title-wrapper">
|
||||
<div class="fuse-vertical-navigation-item-title">
|
||||
<span [ngClass]="item.classes?.title">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
@if (item.subtitle) {
|
||||
<div class="fuse-vertical-navigation-item-subtitle">
|
||||
<span [ngClass]="item.classes?.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
@if (item.badge) {
|
||||
<div class="fuse-vertical-navigation-item-badge">
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-badge-content"
|
||||
[ngClass]="item.badge.classes"
|
||||
>
|
||||
{{ item.badge.title }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
@ -0,0 +1,98 @@
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import {
|
||||
IsActiveMatchOptions,
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
} from '@angular/router';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component';
|
||||
import { FuseUtilsService } from '@fuse/services/utils/utils.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-vertical-navigation-basic-item',
|
||||
templateUrl: './basic.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgClass,
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
MatTooltipModule,
|
||||
NgTemplateOutlet,
|
||||
MatIconModule,
|
||||
],
|
||||
})
|
||||
export class FuseVerticalNavigationBasicItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
private _fuseUtilsService = inject(FuseUtilsService);
|
||||
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
|
||||
// Set the equivalent of {exact: false} as default for active match options.
|
||||
// We are not assigning the item.isActiveMatchOptions directly to the
|
||||
// [routerLinkActiveOptions] because if it's "undefined" initially, the router
|
||||
// will throw an error and stop working.
|
||||
isActiveMatchOptions: IsActiveMatchOptions =
|
||||
this._fuseUtilsService.subsetMatchOptions;
|
||||
|
||||
private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Set the "isActiveMatchOptions" either from item's
|
||||
// "isActiveMatchOptions" or the equivalent form of
|
||||
// item's "exactMatch" option
|
||||
this.isActiveMatchOptions =
|
||||
this.item.isActiveMatchOptions ?? this.item.exactMatch
|
||||
? this._fuseUtilsService.exactMatchOptions
|
||||
: this._fuseUtilsService.subsetMatchOptions;
|
||||
|
||||
// Get the parent navigation component
|
||||
this._fuseVerticalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseVerticalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-wrapper"
|
||||
[class.fuse-vertical-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
>
|
||||
<div
|
||||
class="fuse-vertical-navigation-item"
|
||||
[ngClass]="{ 'fuse-vertical-navigation-item-disabled': item.disabled }"
|
||||
[matTooltip]="item.tooltip || ''"
|
||||
(click)="toggleCollapsable()"
|
||||
>
|
||||
<!-- Icon -->
|
||||
@if (item.icon) {
|
||||
<mat-icon
|
||||
class="fuse-vertical-navigation-item-icon"
|
||||
[ngClass]="item.classes?.icon"
|
||||
[svgIcon]="item.icon"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="fuse-vertical-navigation-item-title-wrapper">
|
||||
<div class="fuse-vertical-navigation-item-title">
|
||||
<span [ngClass]="item.classes?.title">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
@if (item.subtitle) {
|
||||
<div class="fuse-vertical-navigation-item-subtitle">
|
||||
<span [ngClass]="item.classes?.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
@if (item.badge) {
|
||||
<div class="fuse-vertical-navigation-item-badge">
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-badge-content"
|
||||
[ngClass]="item.badge.classes"
|
||||
>
|
||||
{{ item.badge.title }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Arrow -->
|
||||
<mat-icon
|
||||
class="fuse-vertical-navigation-item-arrow icon-size-4"
|
||||
[svgIcon]="'heroicons_solid:chevron-right'"
|
||||
></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!isCollapsed) {
|
||||
<div class="fuse-vertical-navigation-item-children" @expandCollapse>
|
||||
@for (item of item.children; track trackByFn($index, item)) {
|
||||
<!-- Skip the hidden items -->
|
||||
@if ((item.hidden && !item.hidden(item)) || !item.hidden) {
|
||||
<!-- Basic -->
|
||||
@if (item.type === 'basic') {
|
||||
<fuse-vertical-navigation-basic-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-basic-item>
|
||||
}
|
||||
|
||||
<!-- Collapsable -->
|
||||
@if (item.type === 'collapsable') {
|
||||
<fuse-vertical-navigation-collapsable-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"
|
||||
></fuse-vertical-navigation-collapsable-item>
|
||||
}
|
||||
|
||||
<!-- Divider -->
|
||||
@if (item.type === 'divider') {
|
||||
<fuse-vertical-navigation-divider-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-divider-item>
|
||||
}
|
||||
|
||||
<!-- Group -->
|
||||
@if (item.type === 'group') {
|
||||
<fuse-vertical-navigation-group-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-group-item>
|
||||
}
|
||||
|
||||
<!-- Spacer -->
|
||||
@if (item.type === 'spacer') {
|
||||
<fuse-vertical-navigation-spacer-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-spacer-item>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@ -0,0 +1,343 @@
|
||||
import { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
forwardRef,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { fuseAnimations } from '@fuse/animations';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseVerticalNavigationBasicItemComponent } from '@fuse/components/navigation/vertical/components/basic/basic.component';
|
||||
import { FuseVerticalNavigationDividerItemComponent } from '@fuse/components/navigation/vertical/components/divider/divider.component';
|
||||
import { FuseVerticalNavigationGroupItemComponent } from '@fuse/components/navigation/vertical/components/group/group.component';
|
||||
import { FuseVerticalNavigationSpacerItemComponent } from '@fuse/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component';
|
||||
import { Subject, filter, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-vertical-navigation-collapsable-item',
|
||||
templateUrl: './collapsable.component.html',
|
||||
animations: fuseAnimations,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgClass,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
FuseVerticalNavigationBasicItemComponent,
|
||||
forwardRef(() => FuseVerticalNavigationCollapsableItemComponent),
|
||||
FuseVerticalNavigationDividerItemComponent,
|
||||
FuseVerticalNavigationGroupItemComponent,
|
||||
FuseVerticalNavigationSpacerItemComponent,
|
||||
],
|
||||
})
|
||||
export class FuseVerticalNavigationCollapsableItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_autoCollapse: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _router = inject(Router);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
|
||||
@Input() autoCollapse: boolean;
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
|
||||
isCollapsed: boolean = true;
|
||||
isExpanded: boolean = false;
|
||||
private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Host binding for component classes
|
||||
*/
|
||||
@HostBinding('class') get classList(): any {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
return {
|
||||
'fuse-vertical-navigation-item-collapsed': this.isCollapsed,
|
||||
'fuse-vertical-navigation-item-expanded': this.isExpanded,
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Get the parent navigation component
|
||||
this._fuseVerticalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// If the item has a children that has a matching url with the current url, expand...
|
||||
if (this._hasActiveChild(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._fuseVerticalNavigationComponent.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._fuseVerticalNavigationComponent.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._hasActiveChild(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 is NavigationEnd =>
|
||||
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._hasActiveChild(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._fuseVerticalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 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 = !this.isCollapsed;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Execute the observable
|
||||
this._fuseVerticalNavigationComponent.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 = !this.isCollapsed;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Execute the observable
|
||||
this._fuseVerticalNavigationComponent.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;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if the given item has the given url
|
||||
* in one of its children
|
||||
*
|
||||
* @param item
|
||||
* @param currentUrl
|
||||
* @private
|
||||
*/
|
||||
private _hasActiveChild(
|
||||
item: FuseNavigationItem,
|
||||
currentUrl: string
|
||||
): boolean {
|
||||
const children = item.children;
|
||||
|
||||
if (!children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
if (child.children) {
|
||||
if (this._hasActiveChild(child, currentUrl)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the child has a link and is active
|
||||
if (
|
||||
child.link &&
|
||||
this._router.isActive(child.link, child.exactMatch || false)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a children
|
||||
* of the given item
|
||||
*
|
||||
* @param parent
|
||||
* @param item
|
||||
* @private
|
||||
*/
|
||||
private _isChildrenOf(
|
||||
parent: FuseNavigationItem,
|
||||
item: FuseNavigationItem
|
||||
): 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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<!-- Divider -->
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-wrapper divider"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
></div>
|
||||
@ -0,0 +1,64 @@
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-vertical-navigation-divider-item',
|
||||
templateUrl: './divider.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgClass],
|
||||
})
|
||||
export class FuseVerticalNavigationDividerItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
|
||||
private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Get the parent navigation component
|
||||
this._fuseVerticalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseVerticalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
<!-- Item wrapper -->
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-wrapper"
|
||||
[class.fuse-vertical-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
>
|
||||
<div class="fuse-vertical-navigation-item">
|
||||
<!-- Icon -->
|
||||
@if (item.icon) {
|
||||
<mat-icon
|
||||
class="fuse-vertical-navigation-item-icon"
|
||||
[ngClass]="item.classes?.icon"
|
||||
[svgIcon]="item.icon"
|
||||
></mat-icon>
|
||||
}
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="fuse-vertical-navigation-item-title-wrapper">
|
||||
<div class="fuse-vertical-navigation-item-title">
|
||||
<span [ngClass]="item.classes?.title">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
@if (item.subtitle) {
|
||||
<div class="fuse-vertical-navigation-item-subtitle">
|
||||
<span [ngClass]="item.classes?.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
@if (item.badge) {
|
||||
<div class="fuse-vertical-navigation-item-badge">
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-badge-content"
|
||||
[ngClass]="item.badge.classes"
|
||||
>
|
||||
{{ item.badge.title }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@for (item of item.children; track trackByFn($index, item)) {
|
||||
<!-- Skip the hidden items -->
|
||||
@if ((item.hidden && !item.hidden(item)) || !item.hidden) {
|
||||
<!-- Basic -->
|
||||
@if (item.type === 'basic') {
|
||||
<fuse-vertical-navigation-basic-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-basic-item>
|
||||
}
|
||||
|
||||
<!-- Collapsable -->
|
||||
@if (item.type === 'collapsable') {
|
||||
<fuse-vertical-navigation-collapsable-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"
|
||||
></fuse-vertical-navigation-collapsable-item>
|
||||
}
|
||||
|
||||
<!-- Divider -->
|
||||
@if (item.type === 'divider') {
|
||||
<fuse-vertical-navigation-divider-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-divider-item>
|
||||
}
|
||||
|
||||
<!-- Group -->
|
||||
@if (item.type === 'group') {
|
||||
<fuse-vertical-navigation-group-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-group-item>
|
||||
}
|
||||
|
||||
<!-- Spacer -->
|
||||
@if (item.type === 'spacer') {
|
||||
<fuse-vertical-navigation-spacer-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-spacer-item>
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
import { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
forwardRef,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseVerticalNavigationBasicItemComponent } from '@fuse/components/navigation/vertical/components/basic/basic.component';
|
||||
import { FuseVerticalNavigationCollapsableItemComponent } from '@fuse/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { FuseVerticalNavigationDividerItemComponent } from '@fuse/components/navigation/vertical/components/divider/divider.component';
|
||||
import { FuseVerticalNavigationSpacerItemComponent } from '@fuse/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-vertical-navigation-group-item',
|
||||
templateUrl: './group.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgClass,
|
||||
MatIconModule,
|
||||
FuseVerticalNavigationBasicItemComponent,
|
||||
FuseVerticalNavigationCollapsableItemComponent,
|
||||
FuseVerticalNavigationDividerItemComponent,
|
||||
forwardRef(() => FuseVerticalNavigationGroupItemComponent),
|
||||
FuseVerticalNavigationSpacerItemComponent,
|
||||
],
|
||||
})
|
||||
export class FuseVerticalNavigationGroupItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_autoCollapse: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
|
||||
@Input() autoCollapse: boolean;
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
|
||||
private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Get the parent navigation component
|
||||
this._fuseVerticalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseVerticalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any {
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<!-- Spacer -->
|
||||
<div
|
||||
class="fuse-vertical-navigation-item-wrapper"
|
||||
[ngClass]="item.classes?.wrapper"
|
||||
></div>
|
||||
@ -0,0 +1,64 @@
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-vertical-navigation-spacer-item',
|
||||
templateUrl: './spacer.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgClass],
|
||||
})
|
||||
export class FuseVerticalNavigationSpacerItemComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
|
||||
@Input() item: FuseNavigationItem;
|
||||
@Input() name: string;
|
||||
|
||||
private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Get the parent navigation component
|
||||
this._fuseVerticalNavigationComponent =
|
||||
this._fuseNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._fuseVerticalNavigationComponent.onRefreshed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
/* Variables */
|
||||
:root {
|
||||
--fuse-vertical-navigation-compact-width: 112px;
|
||||
}
|
||||
|
||||
fuse-vertical-navigation {
|
||||
/* Compact appearance overrides */
|
||||
&.fuse-vertical-navigation-appearance-compact {
|
||||
width: var(--fuse-vertical-navigation-compact-width);
|
||||
min-width: var(--fuse-vertical-navigation-compact-width);
|
||||
max-width: var(--fuse-vertical-navigation-compact-width);
|
||||
|
||||
/* Left positioned */
|
||||
&.fuse-vertical-navigation-position-left {
|
||||
/* Side mode */
|
||||
&.fuse-vertical-navigation-mode-side {
|
||||
margin-left: calc(
|
||||
var(--fuse-vertical-navigation-compact-width) * -1
|
||||
);
|
||||
}
|
||||
|
||||
/* Opened */
|
||||
&.fuse-vertical-navigation-opened {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right positioned */
|
||||
&.fuse-vertical-navigation-position-right {
|
||||
/* Side mode */
|
||||
&.fuse-vertical-navigation-mode-side {
|
||||
margin-right: calc(
|
||||
var(--fuse-vertical-navigation-compact-width) * -1
|
||||
);
|
||||
}
|
||||
|
||||
/* Opened */
|
||||
&.fuse-vertical-navigation-opened {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Aside wrapper */
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: auto;
|
||||
right: var(--fuse-vertical-navigation-compact-width);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrapper */
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
/* Content */
|
||||
.fuse-vertical-navigation-content {
|
||||
> fuse-vertical-navigation-aside-item,
|
||||
> fuse-vertical-navigation-basic-item {
|
||||
.fuse-vertical-navigation-item-wrapper {
|
||||
margin: 4px 8px 0 8px;
|
||||
|
||||
.fuse-vertical-navigation-item {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
|
||||
.fuse-vertical-navigation-item-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
margin-top: 8px;
|
||||
|
||||
.fuse-vertical-navigation-item-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-subtitle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> fuse-vertical-navigation-collapsable-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> fuse-vertical-navigation-group-item {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Aside wrapper */
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: var(--fuse-vertical-navigation-compact-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,559 @@
|
||||
/* Variables */
|
||||
:root {
|
||||
--fuse-vertical-navigation-width: 280px;
|
||||
}
|
||||
|
||||
fuse-vertical-navigation {
|
||||
position: sticky;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
top: 0;
|
||||
width: var(--fuse-vertical-navigation-width);
|
||||
min-width: var(--fuse-vertical-navigation-width);
|
||||
max-width: var(--fuse-vertical-navigation-width);
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
z-index: 200;
|
||||
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
/* @ Navigation Drawer
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
|
||||
/* Animations */
|
||||
&.fuse-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 */
|
||||
.fuse-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 */
|
||||
&.fuse-vertical-navigation-mode-over {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* Left position */
|
||||
&.fuse-vertical-navigation-position-left {
|
||||
/* Side mode */
|
||||
&.fuse-vertical-navigation-mode-side {
|
||||
margin-left: calc(#{var(--fuse-vertical-navigation-width)} * -1);
|
||||
|
||||
&.fuse-vertical-navigation-opened {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Over mode */
|
||||
&.fuse-vertical-navigation-mode-over {
|
||||
left: 0;
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
|
||||
&.fuse-vertical-navigation-opened {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrapper */
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right position */
|
||||
&.fuse-vertical-navigation-position-right {
|
||||
/* Side mode */
|
||||
&.fuse-vertical-navigation-mode-side {
|
||||
margin-right: calc(var(--fuse-vertical-navigation-width) * -1);
|
||||
|
||||
&.fuse-vertical-navigation-opened {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Over mode */
|
||||
&.fuse-vertical-navigation-mode-over {
|
||||
right: 0;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
|
||||
&.fuse-vertical-navigation-opened {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrapper */
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inner mode */
|
||||
&.fuse-vertical-navigation-inner {
|
||||
position: relative;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
box-shadow: none;
|
||||
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
height: auto;
|
||||
|
||||
.fuse-vertical-navigation-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrapper */
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
background: inherit;
|
||||
box-shadow: inset -1px 0 0 var(--fuse-border);
|
||||
|
||||
/* Header */
|
||||
.fuse-vertical-navigation-header {
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.fuse-vertical-navigation-content {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
/* Divider */
|
||||
> fuse-vertical-navigation-divider-item {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
/* Group */
|
||||
> fuse-vertical-navigation-group-item {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.fuse-vertical-navigation-footer {
|
||||
}
|
||||
}
|
||||
|
||||
/* Aside wrapper */
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: var(--fuse-vertical-navigation-width);
|
||||
width: var(--fuse-vertical-navigation-width);
|
||||
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);
|
||||
background: inherit;
|
||||
|
||||
> fuse-vertical-navigation-aside-item {
|
||||
padding: 24px 0;
|
||||
|
||||
/* First item of the aside */
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fuse-vertical-navigation-position-right {
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: auto;
|
||||
right: var(--fuse-vertical-navigation-width);
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
/* @ Navigation Items
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
|
||||
/* Navigation items common */
|
||||
fuse-vertical-navigation-aside-item,
|
||||
fuse-vertical-navigation-basic-item,
|
||||
fuse-vertical-navigation-collapsable-item,
|
||||
fuse-vertical-navigation-divider-item,
|
||||
fuse-vertical-navigation-group-item,
|
||||
fuse-vertical-navigation-spacer-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
user-select: none;
|
||||
|
||||
.fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
|
||||
/* Disabled state */
|
||||
&.fuse-vertical-navigation-item-disabled {
|
||||
cursor: default;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
.fuse-vertical-navigation-item-subtitle {
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-badge {
|
||||
margin-left: auto;
|
||||
|
||||
.fuse-vertical-navigation-item-badge-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Aside, Basic, Collapsable, Group */
|
||||
fuse-vertical-navigation-aside-item,
|
||||
fuse-vertical-navigation-basic-item,
|
||||
fuse-vertical-navigation-collapsable-item,
|
||||
fuse-vertical-navigation-group-item {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Aside, Basic, Collapsable */
|
||||
fuse-vertical-navigation-aside-item,
|
||||
fuse-vertical-navigation-basic-item,
|
||||
fuse-vertical-navigation-collapsable-item {
|
||||
margin-bottom: 4px;
|
||||
|
||||
.fuse-vertical-navigation-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* Aside */
|
||||
fuse-vertical-navigation-aside-item {
|
||||
}
|
||||
|
||||
/* Basic */
|
||||
fuse-vertical-navigation-basic-item {
|
||||
}
|
||||
|
||||
/* Collapsable */
|
||||
fuse-vertical-navigation-collapsable-item {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
.fuse-vertical-navigation-item-badge {
|
||||
+ .fuse-vertical-navigation-item-arrow {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fuse-vertical-navigation-item-expanded {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
.fuse-vertical-navigation-item-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .fuse-vertical-navigation-item-children {
|
||||
> *:first-child {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
padding-bottom: 6px;
|
||||
|
||||
> .fuse-vertical-navigation-item-children {
|
||||
> *:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 1st level */
|
||||
.fuse-vertical-navigation-item-children {
|
||||
overflow: hidden;
|
||||
|
||||
.fuse-vertical-navigation-item {
|
||||
padding-left: 56px;
|
||||
}
|
||||
|
||||
/* 2nd level */
|
||||
.fuse-vertical-navigation-item-children {
|
||||
.fuse-vertical-navigation-item {
|
||||
padding-left: 72px;
|
||||
}
|
||||
|
||||
/* 3rd level */
|
||||
.fuse-vertical-navigation-item-children {
|
||||
.fuse-vertical-navigation-item {
|
||||
padding-left: 88px;
|
||||
}
|
||||
|
||||
/* 4th level */
|
||||
.fuse-vertical-navigation-item-children {
|
||||
.fuse-vertical-navigation-item {
|
||||
padding-left: 104px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
fuse-vertical-navigation-divider-item {
|
||||
margin: 12px 0;
|
||||
|
||||
.fuse-vertical-navigation-item-wrapper {
|
||||
height: 1px;
|
||||
box-shadow: 0 1px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Group */
|
||||
fuse-vertical-navigation-group-item {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
.fuse-vertical-navigation-item-badge,
|
||||
.fuse-vertical-navigation-item-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
.fuse-vertical-navigation-item-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Spacer */
|
||||
fuse-vertical-navigation-spacer-item {
|
||||
margin: 6px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
/* @ Overlay
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
.fuse-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);
|
||||
|
||||
+ .fuse-vertical-navigation-aside-overlay {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
/* @ Aside overlay
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
.fuse-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);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
/* @ Navigation Items Colors
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
|
||||
/* Navigation items common */
|
||||
fuse-vertical-navigation-aside-item,
|
||||
fuse-vertical-navigation-basic-item,
|
||||
fuse-vertical-navigation-collapsable-item,
|
||||
fuse-vertical-navigation-group-item {
|
||||
.fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
color: currentColor;
|
||||
|
||||
.fuse-vertical-navigation-item-icon {
|
||||
@apply text-current opacity-60;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
.fuse-vertical-navigation-item-title {
|
||||
@apply text-current opacity-80;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-subtitle {
|
||||
@apply text-current opacity-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Aside, Basic, Collapsable */
|
||||
fuse-vertical-navigation-aside-item,
|
||||
fuse-vertical-navigation-basic-item,
|
||||
fuse-vertical-navigation-collapsable-item {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
/* Active state */
|
||||
&:not(.fuse-vertical-navigation-item-disabled) {
|
||||
&.fuse-vertical-navigation-item-active,
|
||||
&.fuse-vertical-navigation-item-active-forced {
|
||||
@apply bg-gray-800 bg-opacity-5 dark:bg-white dark:bg-opacity-12;
|
||||
|
||||
.fuse-vertical-navigation-item-icon {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-title {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-subtitle {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover state */
|
||||
&:not(.fuse-vertical-navigation-item-active-forced):not(
|
||||
.fuse-vertical-navigation-item-active
|
||||
):not(.fuse-vertical-navigation-item-disabled) {
|
||||
&:hover {
|
||||
@apply bg-gray-800 bg-opacity-5 dark:bg-white dark:bg-opacity-12;
|
||||
|
||||
.fuse-vertical-navigation-item-icon {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-title,
|
||||
.fuse-vertical-navigation-item-arrow {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-subtitle {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapsable */
|
||||
fuse-vertical-navigation-collapsable-item {
|
||||
/* Expanded state */
|
||||
&.fuse-vertical-navigation-item-expanded {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
.fuse-vertical-navigation-item-icon {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-title,
|
||||
.fuse-vertical-navigation-item-arrow {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-subtitle {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Group */
|
||||
fuse-vertical-navigation-group-item {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
.fuse-vertical-navigation-item-title {
|
||||
@apply text-primary-600 opacity-100 dark:text-primary-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
/* Variables */
|
||||
:root {
|
||||
--fuse-vertical-navigation-width: 280px;
|
||||
--fuse-vertical-navigation-dense-width: 80px;
|
||||
}
|
||||
|
||||
fuse-vertical-navigation {
|
||||
/* Dense appearance overrides */
|
||||
&.fuse-vertical-navigation-appearance-dense {
|
||||
&:not(.fuse-vertical-navigation-mode-over) {
|
||||
width: var(--fuse-vertical-navigation-dense-width);
|
||||
min-width: var(--fuse-vertical-navigation-dense-width);
|
||||
max-width: var(--fuse-vertical-navigation-dense-width);
|
||||
|
||||
/* Left positioned */
|
||||
&.fuse-vertical-navigation-position-left {
|
||||
/* Side mode */
|
||||
&.fuse-vertical-navigation-mode-side {
|
||||
margin-left: calc(
|
||||
var(--fuse-vertical-navigation-dense-width) * -1
|
||||
);
|
||||
}
|
||||
|
||||
/* Opened */
|
||||
&.fuse-vertical-navigation-opened {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right positioned */
|
||||
&.fuse-vertical-navigation-position-right {
|
||||
/* Side mode */
|
||||
&.fuse-vertical-navigation-mode-side {
|
||||
margin-right: calc(
|
||||
var(--fuse-vertical-navigation-dense-width) * -1
|
||||
);
|
||||
}
|
||||
|
||||
/* Opened */
|
||||
&.fuse-vertical-navigation-opened {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Aside wrapper */
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: auto;
|
||||
right: var(--fuse-vertical-navigation-dense-width);
|
||||
}
|
||||
|
||||
&.fuse-vertical-navigation-hover {
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: auto;
|
||||
right: var(--fuse-vertical-navigation-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrapper */
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
/* Content */
|
||||
.fuse-vertical-navigation-content {
|
||||
fuse-vertical-navigation-aside-item,
|
||||
fuse-vertical-navigation-basic-item,
|
||||
fuse-vertical-navigation-collapsable-item,
|
||||
fuse-vertical-navigation-group-item {
|
||||
.fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
width: calc(
|
||||
var(--fuse-vertical-navigation-dense-width) -
|
||||
24px
|
||||
);
|
||||
min-width: calc(
|
||||
var(--fuse-vertical-navigation-dense-width) -
|
||||
24px
|
||||
);
|
||||
max-width: calc(
|
||||
var(--fuse-vertical-navigation-dense-width) -
|
||||
24px
|
||||
);
|
||||
|
||||
.fuse-vertical-navigation-item-arrow,
|
||||
.fuse-vertical-navigation-item-badge,
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
transition: opacity 400ms
|
||||
cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fuse-vertical-navigation-group-item {
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.fuse-vertical-navigation-hover):not(
|
||||
.fuse-vertical-navigation-mode-over
|
||||
) {
|
||||
/* Wrapper */
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
/* Content */
|
||||
.fuse-vertical-navigation-content {
|
||||
.fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
padding: 10px 16px;
|
||||
|
||||
.fuse-vertical-navigation-item-arrow,
|
||||
.fuse-vertical-navigation-item-badge,
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fuse-vertical-navigation-collapsable-item {
|
||||
.fuse-vertical-navigation-item-children {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
fuse-vertical-navigation-group-item {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: 23px;
|
||||
border-top-width: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Aside wrapper */
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: var(--fuse-vertical-navigation-dense-width);
|
||||
}
|
||||
|
||||
/* Hover */
|
||||
&.fuse-vertical-navigation-hover {
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
width: var(--fuse-vertical-navigation-width);
|
||||
|
||||
.fuse-vertical-navigation-content {
|
||||
.fuse-vertical-navigation-item-wrapper {
|
||||
.fuse-vertical-navigation-item {
|
||||
width: calc(
|
||||
var(--fuse-vertical-navigation-width) - 24px
|
||||
);
|
||||
min-width: calc(
|
||||
var(--fuse-vertical-navigation-width) - 24px
|
||||
);
|
||||
max-width: calc(
|
||||
var(--fuse-vertical-navigation-width) - 24px
|
||||
);
|
||||
|
||||
.fuse-vertical-navigation-item-arrow,
|
||||
.fuse-vertical-navigation-item-badge,
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
white-space: nowrap;
|
||||
animation: removeWhiteSpaceNoWrap 1ms linear
|
||||
350ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: var(--fuse-vertical-navigation-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes removeWhiteSpaceNoWrap {
|
||||
0% {
|
||||
white-space: nowrap;
|
||||
}
|
||||
99% {
|
||||
white-space: nowrap;
|
||||
}
|
||||
100% {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
/* Variables */
|
||||
:root {
|
||||
--fuse-vertical-navigation-thin-width: 80px;
|
||||
}
|
||||
|
||||
fuse-vertical-navigation {
|
||||
/* Thin appearance overrides */
|
||||
&.fuse-vertical-navigation-appearance-thin {
|
||||
width: var(--fuse-vertical-navigation-thin-width);
|
||||
min-width: var(--fuse-vertical-navigation-thin-width);
|
||||
max-width: var(--fuse-vertical-navigation-thin-width);
|
||||
|
||||
/* Left positioned */
|
||||
&.fuse-vertical-navigation-position-left {
|
||||
&.fuse-vertical-navigation-mode-side {
|
||||
margin-left: calc(
|
||||
var(--fuse-vertical-navigation-thin-width) * -1
|
||||
);
|
||||
}
|
||||
|
||||
&.fuse-vertical-navigation-opened {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right positioned */
|
||||
&.fuse-vertical-navigation-position-right {
|
||||
&.fuse-vertical-navigation-mode-side {
|
||||
margin-right: calc(
|
||||
var(--fuse-vertical-navigation-thin-width) * -1
|
||||
);
|
||||
}
|
||||
|
||||
&.fuse-vertical-navigation-opened {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: auto;
|
||||
right: var(--fuse-vertical-navigation-thin-width);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrapper */
|
||||
.fuse-vertical-navigation-wrapper {
|
||||
/* Content */
|
||||
.fuse-vertical-navigation-content {
|
||||
> fuse-vertical-navigation-aside-item,
|
||||
> fuse-vertical-navigation-basic-item {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 64px;
|
||||
min-height: 64px;
|
||||
max-height: 64px;
|
||||
padding: 0 16px;
|
||||
|
||||
.fuse-vertical-navigation-item-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fuse-vertical-navigation-item {
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
.fuse-vertical-navigation-item-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fuse-vertical-navigation-item-arrow,
|
||||
.fuse-vertical-navigation-item-badge-content,
|
||||
.fuse-vertical-navigation-item-title-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> fuse-vertical-navigation-collapsable-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> fuse-vertical-navigation-group-item {
|
||||
> .fuse-vertical-navigation-item-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Aside wrapper */
|
||||
.fuse-vertical-navigation-aside-wrapper {
|
||||
left: var(--fuse-vertical-navigation-thin-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
<div class="fuse-vertical-navigation-wrapper">
|
||||
<!-- Header -->
|
||||
<div class="fuse-vertical-navigation-header">
|
||||
<ng-content select="[fuseVerticalNavigationHeader]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div
|
||||
class="fuse-vertical-navigation-content"
|
||||
fuseScrollbar
|
||||
[fuseScrollbarOptions]="{
|
||||
wheelPropagation: inner,
|
||||
suppressScrollX: true,
|
||||
}"
|
||||
#navigationContent
|
||||
>
|
||||
<!-- Content header -->
|
||||
<div class="fuse-vertical-navigation-content-header">
|
||||
<ng-content
|
||||
select="[fuseVerticalNavigationContentHeader]"
|
||||
></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Items -->
|
||||
@for (item of navigation; track trackByFn($index, item)) {
|
||||
<!-- Skip the hidden items -->
|
||||
@if ((item.hidden && !item.hidden(item)) || !item.hidden) {
|
||||
<!-- Aside -->
|
||||
@if (item.type === 'aside') {
|
||||
<fuse-vertical-navigation-aside-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[activeItemId]="activeAsideItemId"
|
||||
[autoCollapse]="autoCollapse"
|
||||
[skipChildren]="true"
|
||||
(click)="toggleAside(item)"
|
||||
></fuse-vertical-navigation-aside-item>
|
||||
}
|
||||
|
||||
<!-- Basic -->
|
||||
@if (item.type === 'basic') {
|
||||
<fuse-vertical-navigation-basic-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-basic-item>
|
||||
}
|
||||
|
||||
<!-- Collapsable -->
|
||||
@if (item.type === 'collapsable') {
|
||||
<fuse-vertical-navigation-collapsable-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"
|
||||
></fuse-vertical-navigation-collapsable-item>
|
||||
}
|
||||
|
||||
<!-- Divider -->
|
||||
@if (item.type === 'divider') {
|
||||
<fuse-vertical-navigation-divider-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-divider-item>
|
||||
}
|
||||
|
||||
<!-- Group -->
|
||||
@if (item.type === 'group') {
|
||||
<fuse-vertical-navigation-group-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"
|
||||
></fuse-vertical-navigation-group-item>
|
||||
}
|
||||
|
||||
<!-- Spacer -->
|
||||
@if (item.type === 'spacer') {
|
||||
<fuse-vertical-navigation-spacer-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
></fuse-vertical-navigation-spacer-item>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Content footer -->
|
||||
<div class="fuse-vertical-navigation-content-footer">
|
||||
<ng-content
|
||||
select="[fuseVerticalNavigationContentFooter]"
|
||||
></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="fuse-vertical-navigation-footer">
|
||||
<ng-content select="[fuseVerticalNavigationFooter]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aside -->
|
||||
@if (activeAsideItemId) {
|
||||
<div
|
||||
class="fuse-vertical-navigation-aside-wrapper"
|
||||
fuseScrollbar
|
||||
[fuseScrollbarOptions]="{
|
||||
wheelPropagation: false,
|
||||
suppressScrollX: true,
|
||||
}"
|
||||
[@fadeInLeft]="position === 'left'"
|
||||
[@fadeInRight]="position === 'right'"
|
||||
[@fadeOutLeft]="position === 'left'"
|
||||
[@fadeOutRight]="position === 'right'"
|
||||
>
|
||||
<!-- Items -->
|
||||
@for (item of navigation; track trackByFn($index, item)) {
|
||||
<!-- Skip the hidden items -->
|
||||
@if ((item.hidden && !item.hidden(item)) || !item.hidden) {
|
||||
<!-- Aside -->
|
||||
@if (item.type === 'aside' && item.id === activeAsideItemId) {
|
||||
<fuse-vertical-navigation-aside-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"
|
||||
></fuse-vertical-navigation-aside-item>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
@use 'styles/appearances/default';
|
||||
@use 'styles/appearances/compact';
|
||||
@use 'styles/appearances/dense';
|
||||
@use 'styles/appearances/thin';
|
||||
@ -0,0 +1,831 @@
|
||||
import {
|
||||
animate,
|
||||
AnimationBuilder,
|
||||
AnimationPlayer,
|
||||
style,
|
||||
} from '@angular/animations';
|
||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import { ScrollStrategy, ScrollStrategyOptions } from '@angular/cdk/overlay';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
QueryList,
|
||||
Renderer2,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { fuseAnimations } from '@fuse/animations';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation/navigation.service';
|
||||
import {
|
||||
FuseNavigationItem,
|
||||
FuseVerticalNavigationAppearance,
|
||||
FuseVerticalNavigationMode,
|
||||
FuseVerticalNavigationPosition,
|
||||
} from '@fuse/components/navigation/navigation.types';
|
||||
import { FuseVerticalNavigationAsideItemComponent } from '@fuse/components/navigation/vertical/components/aside/aside.component';
|
||||
import { FuseVerticalNavigationBasicItemComponent } from '@fuse/components/navigation/vertical/components/basic/basic.component';
|
||||
import { FuseVerticalNavigationCollapsableItemComponent } from '@fuse/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { FuseVerticalNavigationDividerItemComponent } from '@fuse/components/navigation/vertical/components/divider/divider.component';
|
||||
import { FuseVerticalNavigationGroupItemComponent } from '@fuse/components/navigation/vertical/components/group/group.component';
|
||||
import { FuseVerticalNavigationSpacerItemComponent } from '@fuse/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { FuseScrollbarDirective } from '@fuse/directives/scrollbar/scrollbar.directive';
|
||||
import { FuseUtilsService } from '@fuse/services/utils/utils.service';
|
||||
import {
|
||||
delay,
|
||||
filter,
|
||||
merge,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
Subscription,
|
||||
takeUntil,
|
||||
} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fuse-vertical-navigation',
|
||||
templateUrl: './vertical.component.html',
|
||||
styleUrls: ['./vertical.component.scss'],
|
||||
animations: fuseAnimations,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs: 'fuseVerticalNavigation',
|
||||
standalone: true,
|
||||
imports: [
|
||||
FuseScrollbarDirective,
|
||||
FuseVerticalNavigationAsideItemComponent,
|
||||
FuseVerticalNavigationBasicItemComponent,
|
||||
FuseVerticalNavigationCollapsableItemComponent,
|
||||
FuseVerticalNavigationDividerItemComponent,
|
||||
FuseVerticalNavigationGroupItemComponent,
|
||||
FuseVerticalNavigationSpacerItemComponent,
|
||||
],
|
||||
})
|
||||
export class FuseVerticalNavigationComponent
|
||||
implements OnChanges, OnInit, AfterViewInit, OnDestroy
|
||||
{
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_inner: BooleanInput;
|
||||
static ngAcceptInputType_opened: BooleanInput;
|
||||
static ngAcceptInputType_transparentOverlay: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
private _animationBuilder = inject(AnimationBuilder);
|
||||
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
private _document = inject(DOCUMENT);
|
||||
private _elementRef = inject(ElementRef);
|
||||
private _renderer2 = inject(Renderer2);
|
||||
private _router = inject(Router);
|
||||
private _scrollStrategyOptions = inject(ScrollStrategyOptions);
|
||||
private _fuseNavigationService = inject(FuseNavigationService);
|
||||
private _fuseUtilsService = inject(FuseUtilsService);
|
||||
|
||||
@Input() appearance: FuseVerticalNavigationAppearance = 'default';
|
||||
@Input() autoCollapse: boolean = true;
|
||||
@Input() inner: boolean = false;
|
||||
@Input() mode: FuseVerticalNavigationMode = 'side';
|
||||
@Input() name: string = this._fuseUtilsService.randomId();
|
||||
@Input() navigation: FuseNavigationItem[];
|
||||
@Input() opened: boolean = true;
|
||||
@Input() position: FuseVerticalNavigationPosition = 'left';
|
||||
@Input() transparentOverlay: boolean = false;
|
||||
@Output()
|
||||
readonly appearanceChanged: EventEmitter<FuseVerticalNavigationAppearance> =
|
||||
new EventEmitter<FuseVerticalNavigationAppearance>();
|
||||
@Output() readonly modeChanged: EventEmitter<FuseVerticalNavigationMode> =
|
||||
new EventEmitter<FuseVerticalNavigationMode>();
|
||||
@Output() readonly openedChanged: EventEmitter<boolean> =
|
||||
new EventEmitter<boolean>();
|
||||
@Output()
|
||||
readonly positionChanged: EventEmitter<FuseVerticalNavigationPosition> =
|
||||
new EventEmitter<FuseVerticalNavigationPosition>();
|
||||
@ViewChild('navigationContent') private _navigationContentEl: ElementRef;
|
||||
|
||||
activeAsideItemId: string | null = null;
|
||||
onCollapsableItemCollapsed: ReplaySubject<FuseNavigationItem> =
|
||||
new ReplaySubject<FuseNavigationItem>(1);
|
||||
onCollapsableItemExpanded: ReplaySubject<FuseNavigationItem> =
|
||||
new ReplaySubject<FuseNavigationItem>(1);
|
||||
onRefreshed: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);
|
||||
private _animationsEnabled: boolean = false;
|
||||
private _asideOverlay: HTMLElement;
|
||||
private readonly _handleAsideOverlayClick: any;
|
||||
private readonly _handleOverlayClick: any;
|
||||
private _hovered: boolean = false;
|
||||
private _mutationObserver: MutationObserver;
|
||||
private _overlay: HTMLElement;
|
||||
private _player: AnimationPlayer;
|
||||
private _scrollStrategy: ScrollStrategy =
|
||||
this._scrollStrategyOptions.block();
|
||||
private _fuseScrollbarDirectives!: QueryList<FuseScrollbarDirective>;
|
||||
private _fuseScrollbarDirectivesSubscription: Subscription;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor() {
|
||||
this._handleAsideOverlayClick = (): void => {
|
||||
this.closeAside();
|
||||
};
|
||||
this._handleOverlayClick = (): void => {
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Host binding for component classes
|
||||
*/
|
||||
@HostBinding('class') get classList(): any {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
return {
|
||||
'fuse-vertical-navigation-animations-enabled':
|
||||
this._animationsEnabled,
|
||||
[`fuse-vertical-navigation-appearance-${this.appearance}`]: true,
|
||||
'fuse-vertical-navigation-hover': this._hovered,
|
||||
'fuse-vertical-navigation-inner': this.inner,
|
||||
'fuse-vertical-navigation-mode-over': this.mode === 'over',
|
||||
'fuse-vertical-navigation-mode-side': this.mode === 'side',
|
||||
'fuse-vertical-navigation-opened': this.opened,
|
||||
'fuse-vertical-navigation-position-left': this.position === 'left',
|
||||
'fuse-vertical-navigation-position-right':
|
||||
this.position === 'right',
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}
|
||||
|
||||
/**
|
||||
* Host binding for component inline styles
|
||||
*/
|
||||
@HostBinding('style') get styleList(): any {
|
||||
return {
|
||||
visibility: this.opened ? 'visible' : 'hidden',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for fuseScrollbarDirectives
|
||||
*/
|
||||
@ViewChildren(FuseScrollbarDirective)
|
||||
set fuseScrollbarDirectives(
|
||||
fuseScrollbarDirectives: QueryList<FuseScrollbarDirective>
|
||||
) {
|
||||
// Store the directives
|
||||
this._fuseScrollbarDirectives = fuseScrollbarDirectives;
|
||||
|
||||
// Return if there are no directives
|
||||
if (fuseScrollbarDirectives.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unsubscribe the previous subscriptions
|
||||
if (this._fuseScrollbarDirectivesSubscription) {
|
||||
this._fuseScrollbarDirectivesSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Update the scrollbars on collapsable items' collapse/expand
|
||||
this._fuseScrollbarDirectivesSubscription = merge(
|
||||
this.onCollapsableItemCollapsed,
|
||||
this.onCollapsableItemExpanded
|
||||
)
|
||||
.pipe(takeUntil(this._unsubscribeAll), delay(250))
|
||||
.subscribe(() => {
|
||||
// Loop through the scrollbars and update them
|
||||
fuseScrollbarDirectives.forEach((fuseScrollbarDirective) => {
|
||||
fuseScrollbarDirective.update();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Decorated methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On mouseenter
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
@HostListener('mouseenter')
|
||||
private _onMouseenter(): void {
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// Set the hovered
|
||||
this._hovered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* On mouseleave
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
@HostListener('mouseleave')
|
||||
private _onMouseleave(): void {
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// Set the hovered
|
||||
this._hovered = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Appearance
|
||||
if ('appearance' in changes) {
|
||||
// Execute the observable
|
||||
this.appearanceChanged.next(changes.appearance.currentValue);
|
||||
}
|
||||
|
||||
// Inner
|
||||
if ('inner' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.inner = coerceBooleanProperty(changes.inner.currentValue);
|
||||
}
|
||||
|
||||
// Mode
|
||||
if ('mode' in changes) {
|
||||
// Get the previous and current values
|
||||
const currentMode = changes.mode.currentValue;
|
||||
const previousMode = changes.mode.previousValue;
|
||||
|
||||
// Disable the animations
|
||||
this._disableAnimations();
|
||||
|
||||
// If the mode changes: 'over -> side'
|
||||
if (previousMode === 'over' && currentMode === 'side') {
|
||||
// Hide the overlay
|
||||
this._hideOverlay();
|
||||
}
|
||||
|
||||
// If the mode changes: 'side -> over'
|
||||
if (previousMode === 'side' && currentMode === 'over') {
|
||||
// Close the aside
|
||||
this.closeAside();
|
||||
|
||||
// If the navigation is opened
|
||||
if (this.opened) {
|
||||
// Show the overlay
|
||||
this._showOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the observable
|
||||
this.modeChanged.next(currentMode);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Navigation
|
||||
if ('navigation' in changes) {
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
// Opened
|
||||
if ('opened' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.opened = coerceBooleanProperty(changes.opened.currentValue);
|
||||
|
||||
// Open/close the navigation
|
||||
this._toggleOpened(this.opened);
|
||||
}
|
||||
|
||||
// Position
|
||||
if ('position' in changes) {
|
||||
// Execute the observable
|
||||
this.positionChanged.next(changes.position.currentValue);
|
||||
}
|
||||
|
||||
// Transparent overlay
|
||||
if ('transparentOverlay' in changes) {
|
||||
// Coerce the value to a boolean
|
||||
this.transparentOverlay = coerceBooleanProperty(
|
||||
changes.transparentOverlay.currentValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Make sure the name input is not an empty string
|
||||
if (this.name === '') {
|
||||
this.name = this._fuseUtilsService.randomId();
|
||||
}
|
||||
|
||||
// Register the navigation component
|
||||
this._fuseNavigationService.registerComponent(this.name, this);
|
||||
|
||||
// Subscribe to the 'NavigationEnd' event
|
||||
this._router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe(() => {
|
||||
// If the mode is 'over' and the navigation is opened...
|
||||
if (this.mode === 'over' && this.opened) {
|
||||
// Close the navigation
|
||||
this.close();
|
||||
}
|
||||
|
||||
// If the mode is 'side' and the aside is active...
|
||||
if (this.mode === 'side' && this.activeAsideItemId) {
|
||||
// Close the aside
|
||||
this.closeAside();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* After view init
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
// Fix for Firefox.
|
||||
//
|
||||
// Because 'position: sticky' doesn't work correctly inside a 'position: fixed' parent,
|
||||
// adding the '.cdk-global-scrollblock' to the html element breaks the navigation's position.
|
||||
// This fixes the problem by reading the 'top' value from the html element and adding it as a
|
||||
// 'marginTop' to the navigation itself.
|
||||
this._mutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
const mutationTarget = mutation.target as HTMLElement;
|
||||
if (mutation.attributeName === 'class') {
|
||||
if (
|
||||
mutationTarget.classList.contains(
|
||||
'cdk-global-scrollblock'
|
||||
)
|
||||
) {
|
||||
const top = parseInt(mutationTarget.style.top, 10);
|
||||
this._renderer2.setStyle(
|
||||
this._elementRef.nativeElement,
|
||||
'margin-top',
|
||||
`${Math.abs(top)}px`
|
||||
);
|
||||
} else {
|
||||
this._renderer2.setStyle(
|
||||
this._elementRef.nativeElement,
|
||||
'margin-top',
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
this._mutationObserver.observe(this._document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// Return if 'navigation content' element does not exist
|
||||
if (!this._navigationContentEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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(
|
||||
'.fuse-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._fuseScrollbarDirectives.forEach(
|
||||
(fuseScrollbarDirective) => {
|
||||
// Skip if not enabled
|
||||
if (!fuseScrollbarDirective.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll to the active element
|
||||
fuseScrollbarDirective.scrollToElement(
|
||||
'.fuse-vertical-navigation-item-active',
|
||||
-120,
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Disconnect the mutation observer
|
||||
this._mutationObserver.disconnect();
|
||||
|
||||
// Forcefully close the navigation and aside in case they are opened
|
||||
this.close();
|
||||
this.closeAside();
|
||||
|
||||
// Deregister the navigation component from the registry
|
||||
this._fuseNavigationService.deregisterComponent(this.name);
|
||||
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the navigation
|
||||
*/
|
||||
open(): void {
|
||||
// Return if the navigation is already open
|
||||
if (this.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the opened
|
||||
this._toggleOpened(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the navigation
|
||||
*/
|
||||
close(): void {
|
||||
// Return if the navigation is already closed
|
||||
if (!this.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the aside
|
||||
this.closeAside();
|
||||
|
||||
// Set the opened
|
||||
this._toggleOpened(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the navigation
|
||||
*/
|
||||
toggle(): void {
|
||||
// Toggle
|
||||
if (this.opened) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the aside
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
openAside(item: FuseNavigationItem): void {
|
||||
// Return if the item is disabled
|
||||
if (item.disabled || !item.id) {
|
||||
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: FuseNavigationItem): 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;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Enable the animations
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _enableAnimations(): void {
|
||||
// Return if the animations are already enabled
|
||||
if (this._animationsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable the animations
|
||||
this._animationsEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the animations
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _disableAnimations(): void {
|
||||
// Return if the animations are already disabled
|
||||
if (!this._animationsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable the animations
|
||||
this._animationsEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overlay
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _showOverlay(): void {
|
||||
// Return if there is already an overlay
|
||||
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('fuse-vertical-navigation-overlay');
|
||||
|
||||
// Add a class depending on the transparentOverlay option
|
||||
if (this.transparentOverlay) {
|
||||
this._overlay.classList.add(
|
||||
'fuse-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 {
|
||||
// Return if there is already an overlay
|
||||
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(
|
||||
'fuse-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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open/close the navigation
|
||||
*
|
||||
* @param open
|
||||
* @private
|
||||
*/
|
||||
private _toggleOpened(open: boolean): void {
|
||||
// Set the opened
|
||||
this.opened = open;
|
||||
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// If the navigation opened, and the mode
|
||||
// is 'over', show the overlay
|
||||
if (this.mode === 'over') {
|
||||
if (this.opened) {
|
||||
this._showOverlay();
|
||||
} else {
|
||||
this._hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the observable
|
||||
this.openedChanged.next(open);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/directives/scroll-reset/public-api';
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/directives/scroll-reset/scroll-reset.directive';
|
||||
@ -0,0 +1,50 @@
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { filter, Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Directive({
|
||||
selector: '[fuseScrollReset]',
|
||||
exportAs: 'fuseScrollReset',
|
||||
standalone: true,
|
||||
})
|
||||
export class FuseScrollResetDirective implements OnInit, OnDestroy {
|
||||
private _elementRef = inject(ElementRef);
|
||||
private _router = inject(Router);
|
||||
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Subscribe to NavigationEnd event
|
||||
this._router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe(() => {
|
||||
// Reset the element's scroll position to the top
|
||||
this._elementRef.nativeElement.scrollTop = 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/directives/scrollbar/public-api';
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/directives/scrollbar/scrollbar.directive';
|
||||
@ -0,0 +1,446 @@
|
||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ScrollbarGeometry,
|
||||
ScrollbarPosition,
|
||||
} from '@fuse/directives/scrollbar/scrollbar.types';
|
||||
import { merge } from 'lodash-es';
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
import { Subject, debounceTime, fromEvent, takeUntil } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Wrapper directive for the Perfect Scrollbar: https://github.com/mdbootstrap/perfect-scrollbar
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[fuseScrollbar]',
|
||||
exportAs: 'fuseScrollbar',
|
||||
standalone: true,
|
||||
})
|
||||
export class FuseScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
static ngAcceptInputType_fuseScrollbar: BooleanInput;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
private _elementRef = inject(ElementRef);
|
||||
private _platform = inject(Platform);
|
||||
|
||||
@Input() fuseScrollbar: boolean = true;
|
||||
@Input() fuseScrollbarOptions: PerfectScrollbar.Options;
|
||||
|
||||
private _animation: number;
|
||||
private _options: PerfectScrollbar.Options;
|
||||
private _ps: PerfectScrollbar;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for _elementRef
|
||||
*/
|
||||
get elementRef(): ElementRef {
|
||||
return this._elementRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for _ps
|
||||
*/
|
||||
get ps(): PerfectScrollbar | null {
|
||||
return this._ps;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On changes
|
||||
*
|
||||
* @param changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Enabled
|
||||
if ('fuseScrollbar' in changes) {
|
||||
// Interpret empty string as 'true'
|
||||
this.fuseScrollbar = coerceBooleanProperty(
|
||||
changes.fuseScrollbar.currentValue
|
||||
);
|
||||
|
||||
// If enabled, init the directive
|
||||
if (this.fuseScrollbar) {
|
||||
this._init();
|
||||
}
|
||||
// Otherwise destroy it
|
||||
else {
|
||||
this._destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollbar options
|
||||
if ('fuseScrollbarOptions' in changes) {
|
||||
// Merge the options
|
||||
this._options = merge(
|
||||
{},
|
||||
this._options,
|
||||
changes.fuseScrollbarOptions.currentValue
|
||||
);
|
||||
|
||||
// Return if not initialized
|
||||
if (!this._ps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy and re-init the PerfectScrollbar to update its options
|
||||
setTimeout(() => {
|
||||
this._destroy();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this._init();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Is enabled
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.fuseScrollbar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the scrollbar
|
||||
*/
|
||||
update(): void {
|
||||
// Return if not initialized
|
||||
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 {
|
||||
return new ScrollbarGeometry(
|
||||
this._elementRef.nativeElement[prefix + 'Left'],
|
||||
this._elementRef.nativeElement[prefix + 'Top'],
|
||||
this._elementRef.nativeElement[prefix + 'Width'],
|
||||
this._elementRef.nativeElement[prefix + 'Height']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 x
|
||||
* @param speed
|
||||
*/
|
||||
scrollToX(x: number, speed?: number): void {
|
||||
this.animateScrolling('scrollLeft', x, speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to Y
|
||||
*
|
||||
* @param y
|
||||
* @param speed
|
||||
*/
|
||||
scrollToY(y: number, speed?: number): void {
|
||||
this.animateScrolling('scrollTop', y, speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to top
|
||||
*
|
||||
* @param offset
|
||||
* @param speed
|
||||
*/
|
||||
scrollToTop(offset: number = 0, speed?: number): void {
|
||||
this.animateScrolling('scrollTop', offset, speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to bottom
|
||||
*
|
||||
* @param offset
|
||||
* @param 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 offset
|
||||
* @param speed
|
||||
*/
|
||||
scrollToLeft(offset: number = 0, speed?: number): void {
|
||||
this.animateScrolling('scrollLeft', offset, speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to right
|
||||
*
|
||||
* @param offset
|
||||
* @param 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 qs
|
||||
* @param offset
|
||||
* @param ignoreVisible If true, scrollToElement won't happen if element is already inside the current viewport
|
||||
* @param 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): void => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _init(): void {
|
||||
// Return if already initialized
|
||||
if (this._ps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return if on mobile or not on browser
|
||||
if (
|
||||
this._platform.ANDROID ||
|
||||
this._platform.IOS ||
|
||||
!this._platform.isBrowser
|
||||
) {
|
||||
this.fuseScrollbar = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the PerfectScrollbar
|
||||
this._ps = new PerfectScrollbar(this._elementRef.nativeElement, {
|
||||
...this._options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _destroy(): void {
|
||||
// Return if not initialized
|
||||
if (!this._ps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy the PerfectScrollbar
|
||||
this._ps.destroy();
|
||||
|
||||
// Clean up
|
||||
this._ps = null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
121
fuse-starter-v20.0.0/src/@fuse/fuse.provider.ts
Normal file
121
fuse-starter-v20.0.0/src/@fuse/fuse.provider.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import {
|
||||
APP_INITIALIZER,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
EnvironmentProviders,
|
||||
Provider,
|
||||
importProvidersFrom,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MATERIAL_SANITY_CHECKS } from '@angular/material/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
import {
|
||||
FUSE_MOCK_API_DEFAULT_DELAY,
|
||||
mockApiInterceptor,
|
||||
} from '@fuse/lib/mock-api';
|
||||
import { FuseConfig } from '@fuse/services/config';
|
||||
import { FUSE_CONFIG } from '@fuse/services/config/config.constants';
|
||||
import { FuseConfirmationService } from '@fuse/services/confirmation';
|
||||
import {
|
||||
FuseLoadingService,
|
||||
fuseLoadingInterceptor,
|
||||
} from '@fuse/services/loading';
|
||||
import { FuseMediaWatcherService } from '@fuse/services/media-watcher';
|
||||
import { FusePlatformService } from '@fuse/services/platform';
|
||||
import { FuseSplashScreenService } from '@fuse/services/splash-screen';
|
||||
import { FuseUtilsService } from '@fuse/services/utils';
|
||||
|
||||
export type FuseProviderConfig = {
|
||||
mockApi?: {
|
||||
delay?: number;
|
||||
services?: any[];
|
||||
};
|
||||
fuse?: FuseConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fuse provider
|
||||
*/
|
||||
export const provideFuse = (
|
||||
config: FuseProviderConfig
|
||||
): Array<Provider | EnvironmentProviders> => {
|
||||
// Base providers
|
||||
const providers: Array<Provider | EnvironmentProviders> = [
|
||||
{
|
||||
// Disable 'theme' sanity check
|
||||
provide: MATERIAL_SANITY_CHECKS,
|
||||
useValue: {
|
||||
doctype: true,
|
||||
theme: false,
|
||||
version: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Use the 'fill' appearance on Angular Material form fields by default
|
||||
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||
useValue: {
|
||||
appearance: 'fill',
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FUSE_MOCK_API_DEFAULT_DELAY,
|
||||
useValue: config?.mockApi?.delay ?? 0,
|
||||
},
|
||||
{
|
||||
provide: FUSE_CONFIG,
|
||||
useValue: config?.fuse ?? {},
|
||||
},
|
||||
|
||||
importProvidersFrom(MatDialogModule),
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => inject(FuseConfirmationService),
|
||||
multi: true,
|
||||
},
|
||||
|
||||
provideHttpClient(withInterceptors([fuseLoadingInterceptor])),
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => inject(FuseLoadingService),
|
||||
multi: true,
|
||||
},
|
||||
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => inject(FuseMediaWatcherService),
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => inject(FusePlatformService),
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => inject(FuseSplashScreenService),
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => inject(FuseUtilsService),
|
||||
multi: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock Api services
|
||||
if (config?.mockApi?.services) {
|
||||
providers.push(
|
||||
provideHttpClient(withInterceptors([mockApiInterceptor])),
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
deps: [...config.mockApi.services],
|
||||
useFactory: () => (): any => null,
|
||||
multi: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Return the providers
|
||||
return providers;
|
||||
};
|
||||
1
fuse-starter-v20.0.0/src/@fuse/index.ts
Normal file
1
fuse-starter-v20.0.0/src/@fuse/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './fuse.provider';
|
||||
1
fuse-starter-v20.0.0/src/@fuse/lib/mock-api/index.ts
Normal file
1
fuse-starter-v20.0.0/src/@fuse/lib/mock-api/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@fuse/lib/mock-api/public-api';
|
||||
@ -0,0 +1,5 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const FUSE_MOCK_API_DEFAULT_DELAY = new InjectionToken<number>(
|
||||
'FUSE_MOCK_API_DEFAULT_DELAY'
|
||||
);
|
||||
@ -0,0 +1,82 @@
|
||||
import {
|
||||
HttpErrorResponse,
|
||||
HttpEvent,
|
||||
HttpHandlerFn,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
} from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { FUSE_MOCK_API_DEFAULT_DELAY } from '@fuse/lib/mock-api/mock-api.constants';
|
||||
import { FuseMockApiService } from '@fuse/lib/mock-api/mock-api.service';
|
||||
import { Observable, delay, of, switchMap, throwError } from 'rxjs';
|
||||
|
||||
export const mockApiInterceptor = (
|
||||
request: HttpRequest<unknown>,
|
||||
next: HttpHandlerFn
|
||||
): Observable<HttpEvent<unknown>> => {
|
||||
const defaultDelay = inject(FUSE_MOCK_API_DEFAULT_DELAY);
|
||||
const fuseMockApiService = inject(FuseMockApiService);
|
||||
|
||||
// Try to get the request handler
|
||||
const { handler, urlParams } = fuseMockApiService.findHandler(
|
||||
request.method.toUpperCase(),
|
||||
request.url
|
||||
);
|
||||
|
||||
// Pass through if the request handler does not exist
|
||||
if (!handler) {
|
||||
return next(request);
|
||||
}
|
||||
|
||||
// Set the intercepted request on the handler
|
||||
handler.request = request;
|
||||
|
||||
// Set the url params on the handler
|
||||
handler.urlParams = urlParams;
|
||||
|
||||
// Subscribe to the response function observable
|
||||
return handler.response.pipe(
|
||||
delay(handler.delay ?? defaultDelay ?? 0),
|
||||
switchMap((response) => {
|
||||
// If there is no response data,
|
||||
// throw an error response
|
||||
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 code is in between 200 and 300,
|
||||
// return a success response
|
||||
if (data.status >= 200 && data.status < 300) {
|
||||
response = new HttpResponse({
|
||||
body: data.body,
|
||||
status: data.status,
|
||||
statusText: 'OK',
|
||||
});
|
||||
|
||||
return of(response);
|
||||
}
|
||||
|
||||
// For other status codes,
|
||||
// throw an error response
|
||||
response = new HttpErrorResponse({
|
||||
error: data.body.error,
|
||||
status: data.status,
|
||||
statusText: 'ERROR',
|
||||
});
|
||||
|
||||
return throwError(response);
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,87 @@
|
||||
import { HttpRequest } from '@angular/common/http';
|
||||
import { FuseMockApiReplyCallback } from '@fuse/lib/mock-api/mock-api.types';
|
||||
import { Observable, of, take, throwError } from 'rxjs';
|
||||
|
||||
export class FuseMockApiHandler {
|
||||
request!: HttpRequest<any>;
|
||||
urlParams!: { [key: string]: string };
|
||||
|
||||
// Private
|
||||
private _reply: FuseMockApiReplyCallback = undefined;
|
||||
private _replyCount = 0;
|
||||
private _replied = 0;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
public url: string,
|
||||
public delay?: number
|
||||
) {}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for response callback
|
||||
*/
|
||||
get response(): Observable<any> {
|
||||
// If the execution limit has been reached, throw an error
|
||||
if (this._replyCount > 0 && this._replyCount <= this._replied) {
|
||||
return throwError('Execution limit has been reached!');
|
||||
}
|
||||
|
||||
// If the response callback has not been set, throw an error
|
||||
if (!this._reply) {
|
||||
return throwError('Response callback function does not exist!');
|
||||
}
|
||||
|
||||
// If the request has not been set, throw an error
|
||||
if (!this.request) {
|
||||
return throwError('Request does not exist!');
|
||||
}
|
||||
|
||||
// Increase the replied count
|
||||
this._replied++;
|
||||
|
||||
// Execute the reply callback
|
||||
const replyResult = this._reply({
|
||||
request: this.request,
|
||||
urlParams: this.urlParams,
|
||||
});
|
||||
|
||||
// If the result of the reply callback is an observable...
|
||||
if (replyResult instanceof Observable) {
|
||||
// Return the result as it is
|
||||
return replyResult.pipe(take(1));
|
||||
}
|
||||
|
||||
// Otherwise, return the result as an observable
|
||||
return of(replyResult).pipe(take(1));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reply
|
||||
*
|
||||
* @param callback
|
||||
*/
|
||||
reply(callback: FuseMockApiReplyCallback): void {
|
||||
// Store the reply
|
||||
this._reply = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply count
|
||||
*
|
||||
* @param count
|
||||
*/
|
||||
replyCount(count: number): void {
|
||||
// Store the reply count
|
||||
this._replyCount = count;
|
||||
}
|
||||
}
|
||||
201
fuse-starter-v20.0.0/src/@fuse/lib/mock-api/mock-api.service.ts
Normal file
201
fuse-starter-v20.0.0/src/@fuse/lib/mock-api/mock-api.service.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FuseMockApiHandler } from '@fuse/lib/mock-api/mock-api.request-handler';
|
||||
import { FuseMockApiMethods } from '@fuse/lib/mock-api/mock-api.types';
|
||||
import { compact, fromPairs } from 'lodash-es';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FuseMockApiService {
|
||||
private _handlers: { [key: string]: Map<string, FuseMockApiHandler> } = {
|
||||
get: new Map<string, FuseMockApiHandler>(),
|
||||
post: new Map<string, FuseMockApiHandler>(),
|
||||
patch: new Map<string, FuseMockApiHandler>(),
|
||||
delete: new Map<string, FuseMockApiHandler>(),
|
||||
put: new Map<string, FuseMockApiHandler>(),
|
||||
head: new Map<string, FuseMockApiHandler>(),
|
||||
jsonp: new Map<string, FuseMockApiHandler>(),
|
||||
options: new Map<string, FuseMockApiHandler>(),
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find the handler from the service
|
||||
* with the given method and url
|
||||
*
|
||||
* @param method
|
||||
* @param url
|
||||
*/
|
||||
findHandler(
|
||||
method: string,
|
||||
url: string
|
||||
): {
|
||||
handler: FuseMockApiHandler | undefined;
|
||||
urlParams: { [key: string]: string };
|
||||
} {
|
||||
// Prepare the return object
|
||||
const matchingHandler: {
|
||||
handler: FuseMockApiHandler | undefined;
|
||||
urlParams: { [key: string]: string };
|
||||
} = {
|
||||
handler: undefined,
|
||||
urlParams: {},
|
||||
};
|
||||
|
||||
// Split the url
|
||||
const urlParts = url.split('/');
|
||||
|
||||
// Get all related request handlers
|
||||
const handlers = this._handlers[method.toLowerCase()];
|
||||
|
||||
// Iterate through the handlers
|
||||
handlers.forEach((handler, handlerUrl) => {
|
||||
// Skip if there is already a matching handler
|
||||
if (matchingHandler.handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Split the handler url
|
||||
const handlerUrlParts = handlerUrl.split('/');
|
||||
|
||||
// Skip if the lengths of the urls we are comparing are not the same
|
||||
if (urlParts.length !== handlerUrlParts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare
|
||||
const matches = handlerUrlParts.every(
|
||||
(handlerUrlPart, index) =>
|
||||
handlerUrlPart === urlParts[index] ||
|
||||
handlerUrlPart.startsWith(':')
|
||||
);
|
||||
|
||||
// If there is a match...
|
||||
if (matches) {
|
||||
// Assign the matching handler
|
||||
matchingHandler.handler = handler;
|
||||
|
||||
// Extract and assign the parameters
|
||||
matchingHandler.urlParams = fromPairs(
|
||||
compact(
|
||||
handlerUrlParts.map((handlerUrlPart, index) =>
|
||||
handlerUrlPart.startsWith(':')
|
||||
? [handlerUrlPart.substring(1), urlParts[index]]
|
||||
: undefined
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return matchingHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register GET request handler
|
||||
*
|
||||
* @param url - URL address of the mocked API endpoint
|
||||
* @param delay - Delay of the response in milliseconds
|
||||
*/
|
||||
onGet(url: string, delay?: number): FuseMockApiHandler {
|
||||
return this._registerHandler('get', url, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register POST request handler
|
||||
*
|
||||
* @param url - URL address of the mocked API endpoint
|
||||
* @param delay - Delay of the response in milliseconds
|
||||
*/
|
||||
onPost(url: string, delay?: number): FuseMockApiHandler {
|
||||
return this._registerHandler('post', url, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register PATCH request handler
|
||||
*
|
||||
* @param url - URL address of the mocked API endpoint
|
||||
* @param delay - Delay of the response in milliseconds
|
||||
*/
|
||||
onPatch(url: string, delay?: number): FuseMockApiHandler {
|
||||
return this._registerHandler('patch', url, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register DELETE request handler
|
||||
*
|
||||
* @param url - URL address of the mocked API endpoint
|
||||
* @param delay - Delay of the response in milliseconds
|
||||
*/
|
||||
onDelete(url: string, delay?: number): FuseMockApiHandler {
|
||||
return this._registerHandler('delete', url, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register PUT request handler
|
||||
*
|
||||
* @param url - URL address of the mocked API endpoint
|
||||
* @param delay - Delay of the response in milliseconds
|
||||
*/
|
||||
onPut(url: string, delay?: number): FuseMockApiHandler {
|
||||
return this._registerHandler('put', url, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register HEAD request handler
|
||||
*
|
||||
* @param url - URL address of the mocked API endpoint
|
||||
* @param delay - Delay of the response in milliseconds
|
||||
*/
|
||||
onHead(url: string, delay?: number): FuseMockApiHandler {
|
||||
return this._registerHandler('head', url, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register JSONP request handler
|
||||
*
|
||||
* @param url - URL address of the mocked API endpoint
|
||||
* @param delay - Delay of the response in milliseconds
|
||||
*/
|
||||
onJsonp(url: string, delay?: number): FuseMockApiHandler {
|
||||
return this._registerHandler('jsonp', url, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register OPTIONS request handler
|
||||
*
|
||||
* @param url - URL address of the mocked API endpoint
|
||||
* @param delay - Delay of the response in milliseconds
|
||||
*/
|
||||
onOptions(url: string, delay?: number): FuseMockApiHandler {
|
||||
return this._registerHandler('options', url, delay);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register and return a new instance of the handler
|
||||
*
|
||||
* @param method
|
||||
* @param url
|
||||
* @param delay
|
||||
* @private
|
||||
*/
|
||||
private _registerHandler(
|
||||
method: FuseMockApiMethods,
|
||||
url: string,
|
||||
delay?: number
|
||||
): FuseMockApiHandler {
|
||||
// Create a new instance of FuseMockApiRequestHandler
|
||||
const fuseMockHttp = new FuseMockApiHandler(url, delay);
|
||||
|
||||
// Store the handler to access it from the interceptor
|
||||
this._handlers[method].set(url, fuseMockHttp);
|
||||
|
||||
// Return the instance
|
||||
return fuseMockHttp;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { HttpRequest } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export type FuseMockApiReplyCallback =
|
||||
| ((data: {
|
||||
request: HttpRequest<any>;
|
||||
urlParams: { [key: string]: string };
|
||||
}) => [number, string | any] | Observable<any>)
|
||||
| undefined;
|
||||
|
||||
export type FuseMockApiMethods =
|
||||
| 'get'
|
||||
| 'post'
|
||||
| 'patch'
|
||||
| 'delete'
|
||||
| 'put'
|
||||
| 'head'
|
||||
| 'jsonp'
|
||||
| 'options';
|
||||
@ -0,0 +1,30 @@
|
||||
export class FuseMockApiUtils {
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generate a globally unique id
|
||||
*/
|
||||
static guid(): string {
|
||||
/* eslint-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);
|
||||
});
|
||||
|
||||
/* eslint-enable */
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export * from '@fuse/lib/mock-api/mock-api.constants';
|
||||
export * from '@fuse/lib/mock-api/mock-api.interceptor';
|
||||
export * from '@fuse/lib/mock-api/mock-api.service';
|
||||
export * from '@fuse/lib/mock-api/mock-api.types';
|
||||
export * from '@fuse/lib/mock-api/mock-api.utils';
|
||||
@ -0,0 +1,30 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Finds an object from given source using the given key - value pairs
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'fuseFindByKey',
|
||||
pure: false,
|
||||
standalone: true,
|
||||
})
|
||||
export class FuseFindByKeyPipe implements PipeTransform {
|
||||
/**
|
||||
* 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) =>
|
||||
source.find((sourceItem) => sourceItem[key] === item)
|
||||
);
|
||||
}
|
||||
|
||||
// If the value is a string...
|
||||
return source.find((sourceItem) => sourceItem[key] === value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from '@fuse/pipes/find-by-key/public-api';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user