diff --git a/transparency_dashboard_frontend/src/@fuse/animations/defaults.ts b/transparency_dashboard_frontend/src/@fuse/animations/defaults.ts new file mode 100644 index 0000000000000000000000000000000000000000..784fd751ef7f6fbaeb14c335afb0be17bd9bb30d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/animations/defaults.ts @@ -0,0 +1,14 @@ +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'; +} diff --git a/transparency_dashboard_frontend/src/@fuse/animations/expand-collapse.ts b/transparency_dashboard_frontend/src/@fuse/animations/expand-collapse.ts new file mode 100644 index 0000000000000000000000000000000000000000..60b6390336529bd40e663eafb9fd8cf33dade2b7 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/animations/expand-collapse.ts @@ -0,0 +1,34 @@ +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 }; diff --git a/transparency_dashboard_frontend/src/@fuse/animations/fade.ts b/transparency_dashboard_frontend/src/@fuse/animations/fade.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2982b70b4052630fd310fd19839982ef6930eab --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/animations/fade.ts @@ -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, fadeInTop, fadeInBottom, fadeInLeft, fadeInRight, fadeOut, fadeOutTop, fadeOutBottom, fadeOutLeft, fadeOutRight }; diff --git a/transparency_dashboard_frontend/src/@fuse/animations/index.ts b/transparency_dashboard_frontend/src/@fuse/animations/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3669783488c0bde6b4de6b4188d33b646c2c3fc --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/animations/index.ts @@ -0,0 +1 @@ +export * from '@fuse/animations/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/animations/public-api.ts b/transparency_dashboard_frontend/src/@fuse/animations/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e7cd5a542bb4745b8050d800929576ce50de708 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/animations/public-api.ts @@ -0,0 +1,15 @@ +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 +]; diff --git a/transparency_dashboard_frontend/src/@fuse/animations/shake.ts b/transparency_dashboard_frontend/src/@fuse/animations/shake.ts new file mode 100644 index 0000000000000000000000000000000000000000..2742345bafaf7f9cc2b34dc7454c003680c73126 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/animations/shake.ts @@ -0,0 +1,73 @@ +import { animate, keyframes, style, transition, trigger } from '@angular/animations'; + +// ----------------------------------------------------------------------------------------------------- +// @ Shake +// ----------------------------------------------------------------------------------------------------- +const shake = trigger('shake', + [ + + // Prevent the transition if the state is false + transition('void => false', []), + + // Transition + transition('void => *, * => true', + [ + animate('{{timings}}', + keyframes([ + style({ + transform: 'translate3d(0, 0, 0)', + offset : 0 + }), + style({ + transform: 'translate3d(-10px, 0, 0)', + offset : 0.1 + }), + style({ + transform: 'translate3d(10px, 0, 0)', + offset : 0.2 + }), + style({ + transform: 'translate3d(-10px, 0, 0)', + offset : 0.3 + }), + style({ + transform: 'translate3d(10px, 0, 0)', + offset : 0.4 + }), + style({ + transform: 'translate3d(-10px, 0, 0)', + offset : 0.5 + }), + style({ + transform: 'translate3d(10px, 0, 0)', + offset : 0.6 + }), + style({ + transform: 'translate3d(-10px, 0, 0)', + offset : 0.7 + }), + style({ + transform: 'translate3d(10px, 0, 0)', + offset : 0.8 + }), + style({ + transform: 'translate3d(-10px, 0, 0)', + offset : 0.9 + }), + style({ + transform: 'translate3d(0, 0, 0)', + offset : 1 + }) + ]) + ) + ], + { + params: { + timings: '0.8s cubic-bezier(0.455, 0.03, 0.515, 0.955)' + } + } + ) + ] +); + +export { shake }; diff --git a/transparency_dashboard_frontend/src/@fuse/animations/slide.ts b/transparency_dashboard_frontend/src/@fuse/animations/slide.ts new file mode 100644 index 0000000000000000000000000000000000000000..08a80ba7f44b8ec47e49f17700cd0951b2fcfde2 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/animations/slide.ts @@ -0,0 +1,252 @@ +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 { slideInTop, slideInBottom, slideInLeft, slideInRight, slideOutTop, slideOutBottom, slideOutLeft, slideOutRight }; diff --git a/transparency_dashboard_frontend/src/@fuse/animations/zoom.ts b/transparency_dashboard_frontend/src/@fuse/animations/zoom.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9742515952f8dad6c76f614f5aba427a315e5dd --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/animations/zoom.ts @@ -0,0 +1,73 @@ +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 }; + diff --git a/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.html b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.html new file mode 100644 index 0000000000000000000000000000000000000000..6efa4c99bbe67147542359344611d1ff3648d147 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.html @@ -0,0 +1,82 @@ +<div + class="fuse-alert-container" + *ngIf="!dismissible || dismissible && !dismissed" + [@fadeIn]="!dismissed" + [@fadeOut]="!dismissed"> + + <!-- Border --> + <div + class="fuse-alert-border" + *ngIf="appearance === 'border'"></div> + + <!-- Icon --> + <div + class="fuse-alert-icon" + *ngIf="showIcon"> + + <!-- Custom icon --> + <div class="fuse-alert-custom-icon"> + <ng-content select="[fuseAlertIcon]"></ng-content> + </div> + + <!-- Default icons --> + <div class="fuse-alert-default-icon"> + + <mat-icon + *ngIf="type === 'primary'" + [svgIcon]="'heroicons_solid:check-circle'"></mat-icon> + + <mat-icon + *ngIf="type === 'accent'" + [svgIcon]="'heroicons_solid:check-circle'"></mat-icon> + + <mat-icon + *ngIf="type === 'warn'" + [svgIcon]="'heroicons_solid:x-circle'"></mat-icon> + + <mat-icon + *ngIf="type === 'basic'" + [svgIcon]="'heroicons_solid:check-circle'"></mat-icon> + + <mat-icon + *ngIf="type === 'info'" + [svgIcon]="'heroicons_solid:information-circle'"></mat-icon> + + <mat-icon + *ngIf="type === 'success'" + [svgIcon]="'heroicons_solid:check-circle'"></mat-icon> + + <mat-icon + *ngIf="type === 'warning'" + [svgIcon]="'heroicons_solid:exclamation'"></mat-icon> + + <mat-icon + *ngIf="type === 'error'" + [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'"></mat-icon> + </button> + +</div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.scss b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..4b1a86cee847cd706b41653fd5c6e7a54e135d89 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.scss @@ -0,0 +1,1340 @@ +fuse-alert { + display: block; + + /* Common */ + .fuse-alert-container { + position: relative; + display: flex; + padding: 16px; + font-size: 14px; + line-height: 1; + + /* All icons */ + .mat-icon { + color: currentColor !important; + } + + /* Icon */ + .fuse-alert-icon { + display: flex; + align-items: flex-start; + + .fuse-alert-custom-icon, + .fuse-alert-default-icon { + display: none; + align-items: center; + justify-content: center; + border-radius: 50%; + + &:not(:empty) { + display: flex; + margin-right: 12px; + } + } + + .fuse-alert-default-icon { + + .mat-icon { + @apply icon-size-5; + } + } + + .fuse-alert-custom-icon { + display: none; + + &:not(:empty) { + display: flex; + + + .fuse-alert-default-icon { + display: none; + } + } + } + } + + /* Content */ + .fuse-alert-content { + display: flex; + flex-direction: column; + justify-content: center; + line-height: 1; + + /* Title */ + .fuse-alert-title { + display: none; + font-weight: 600; + line-height: 20px; + + &:not(:empty) { + display: block; + + /* Alert that comes after the title */ + + .fuse-alert-message { + + &:not(:empty) { + margin-top: 4px; + } + } + } + } + + /* Alert */ + .fuse-alert-message { + display: none; + line-height: 20px; + + &:not(:empty) { + display: block; + } + } + } + + /* Dismiss button */ + .fuse-alert-dismiss-button { + position: absolute; + top: 10px; + right: 10px; + width: 32px !important; + min-width: 32px !important; + height: 32px !important; + min-height: 32px !important; + line-height: 32px !important; + + .mat-icon { + @apply icon-size-4; + } + } + } + + /* Dismissible */ + &.fuse-alert-dismissible { + + .fuse-alert-container { + + .fuse-alert-content { + margin-right: 32px; + } + } + } + + &:not(.fuse-alert-dismissible) { + + .fuse-alert-container { + + .fuse-alert-dismiss-button { + display: none !important; + } + } + } + + /* Border */ + &.fuse-alert-appearance-border { + + .fuse-alert-container { + position: relative; + overflow: hidden; + border-radius: 6px; + @apply shadow-md bg-card; + + .fuse-alert-border { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + } + + .fuse-alert-message { + @apply text-gray-600; + } + } + + /* Primary */ + &.fuse-alert-type-primary { + + .fuse-alert-container { + + .fuse-alert-border { + @apply bg-primary; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-primary; + } + + .dark & { + @apply bg-gray-700; + + .fuse-alert-border { + @apply bg-primary-400; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-primary-400; + } + + .fuse-alert-message { + @apply text-gray-300; + } + + code { + @apply bg-gray-400 text-gray-800; + } + } + } + } + + /* Accent */ + &.fuse-alert-type-accent { + + .fuse-alert-container { + + .fuse-alert-border { + @apply bg-accent; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-accent; + } + + .dark & { + @apply bg-gray-700; + + .fuse-alert-border { + @apply bg-accent-400; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-accent-400; + } + + .fuse-alert-message { + @apply text-gray-300; + } + + code { + @apply bg-gray-400 text-gray-800; + } + } + } + } + + /* Warn */ + &.fuse-alert-type-warn { + + .fuse-alert-container { + + .fuse-alert-border { + @apply bg-warn; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-warn; + } + + .dark & { + @apply bg-gray-700; + + .fuse-alert-border { + @apply bg-warn-400; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-warn-400; + } + + .fuse-alert-message { + @apply text-gray-300; + } + + code { + @apply bg-gray-400 text-gray-800; + } + } + } + } + + /* Basic */ + &.fuse-alert-type-basic { + + .fuse-alert-container { + + .fuse-alert-border { + @apply bg-gray-600; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-gray-600; + } + + .dark & { + @apply bg-gray-700; + + .fuse-alert-border { + @apply bg-gray-400; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-gray-400; + } + + .fuse-alert-message { + @apply text-gray-300; + } + + code { + @apply bg-gray-400 text-gray-800; + } + } + } + } + + /* Info */ + &.fuse-alert-type-info { + + .fuse-alert-container { + + .fuse-alert-border { + @apply bg-blue-600; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-blue-700; + } + + .dark & { + @apply bg-gray-700; + + .fuse-alert-border { + @apply bg-blue-400; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-blue-400; + } + + .fuse-alert-message { + @apply text-gray-300; + } + + code { + @apply bg-gray-400 text-gray-800; + } + } + } + } + + /* Success */ + &.fuse-alert-type-success { + + .fuse-alert-container { + + .fuse-alert-border { + @apply bg-green-500; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-green-500; + } + + .dark & { + @apply bg-gray-700; + + .fuse-alert-border { + @apply bg-green-400; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-green-400; + } + + .fuse-alert-message { + @apply text-gray-300; + } + + code { + @apply bg-gray-400 text-gray-800; + } + } + } + } + + /* Warning */ + &.fuse-alert-type-warning { + + .fuse-alert-container { + + .fuse-alert-border { + @apply bg-amber-500; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-amber-500; + } + + .dark & { + @apply bg-gray-700; + + .fuse-alert-border { + @apply bg-amber-400; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-amber-400; + } + + .fuse-alert-message { + @apply text-gray-300; + } + + code { + @apply bg-gray-400 text-gray-800; + } + } + } + } + + /* Error */ + &.fuse-alert-type-error { + + .fuse-alert-container { + + .fuse-alert-border { + @apply bg-red-600; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-red-700; + } + + .dark & { + @apply bg-gray-700; + + .fuse-alert-border { + @apply bg-red-400; + } + + .fuse-alert-title, + .fuse-alert-icon { + @apply text-red-400; + } + + .fuse-alert-message { + @apply text-gray-300; + } + + code { + @apply bg-gray-400 text-gray-800; + } + } + } + } + } + + /* Fill */ + &.fuse-alert-appearance-fill { + + .fuse-alert-container { + border-radius: 6px; + + .fuse-alert-dismiss-button { + @apply text-white; + } + } + + /* Primary */ + &.fuse-alert-type-primary { + + .fuse-alert-container { + @apply bg-primary-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title { + @apply text-white; + } + + .fuse-alert-message { + @apply text-primary-100; + } + + code { + @apply text-primary-800 bg-primary-200; + } + } + } + + /* Accent */ + &.fuse-alert-type-accent { + + .fuse-alert-container { + @apply bg-accent-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title { + @apply text-white; + } + + .fuse-alert-message { + @apply text-accent-100; + } + + code { + @apply text-accent-800 bg-accent-200; + } + } + } + + /* Warn */ + &.fuse-alert-type-warn { + + .fuse-alert-container { + @apply bg-warn-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title { + @apply text-white; + } + + .fuse-alert-message { + @apply text-warn-100; + } + + code { + @apply text-warn-800 bg-warn-200; + } + } + } + + /* Basic */ + &.fuse-alert-type-basic { + + .fuse-alert-container { + @apply bg-gray-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title { + @apply text-white; + } + + .fuse-alert-message { + @apply text-gray-100; + } + + code { + @apply bg-gray-200 text-gray-800; + } + } + } + + /* Info */ + &.fuse-alert-type-info { + + .fuse-alert-container { + @apply bg-blue-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title { + @apply text-white; + } + + .fuse-alert-message { + @apply text-blue-100; + } + + code { + @apply bg-blue-200 text-blue-800; + } + } + } + + /* Success */ + &.fuse-alert-type-success { + + .fuse-alert-container { + @apply bg-green-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title { + @apply text-white; + } + + .fuse-alert-message { + @apply text-green-100; + } + + code { + @apply bg-green-200 text-gray-800; + } + } + } + + /* Warning */ + &.fuse-alert-type-warning { + + .fuse-alert-container { + @apply bg-amber-500; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title { + @apply text-white; + } + + .fuse-alert-message { + @apply text-amber-100; + } + + code { + @apply bg-amber-200 text-amber-800; + } + } + } + + /* Error */ + &.fuse-alert-type-error { + + .fuse-alert-container { + @apply bg-red-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title { + @apply text-white; + } + + .fuse-alert-message { + @apply text-red-100; + } + + code { + @apply bg-red-200 text-red-800; + } + } + } + } + + /* Outline */ + &.fuse-alert-appearance-outline { + + .fuse-alert-container { + border-radius: 6px; + } + + /* Primary */ + &.fuse-alert-type-primary { + + .fuse-alert-container { + @apply bg-primary-50 ring-1 ring-primary-400 ring-inset; + + .fuse-alert-icon { + @apply text-primary-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-primary-900; + } + + .fuse-alert-message { + @apply text-primary-700; + } + + code { + @apply text-primary-800 bg-primary-200; + } + + .dark & { + @apply bg-primary-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-primary-200; + } + } + } + } + + /* Accent */ + &.fuse-alert-type-accent { + + .fuse-alert-container { + @apply bg-accent-100 ring-1 ring-accent-400 ring-inset; + + .fuse-alert-icon { + @apply text-accent-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-accent-900; + } + + .fuse-alert-message { + @apply text-accent-700; + } + + code { + @apply text-accent-800 bg-accent-200; + } + + .dark & { + @apply bg-accent-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-accent-200; + } + } + } + } + + /* Warn */ + &.fuse-alert-type-warn { + + .fuse-alert-container { + @apply bg-warn-50 ring-1 ring-warn-400 ring-inset; + + .fuse-alert-icon { + @apply text-warn-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-warn-900; + } + + .fuse-alert-message { + @apply text-warn-700; + } + + code { + @apply text-warn-800 bg-warn-200; + } + + .dark & { + @apply bg-warn-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-warn-200; + } + } + } + } + + /* Basic */ + &.fuse-alert-type-basic { + + .fuse-alert-container { + @apply bg-gray-100 ring-1 ring-gray-400 ring-inset; + + .fuse-alert-icon { + @apply text-gray-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-gray-900; + } + + .fuse-alert-message { + @apply text-gray-700; + } + + code { + @apply bg-gray-200 text-gray-800; + } + + .dark & { + @apply bg-gray-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-gray-200; + } + } + } + } + + /* Info */ + &.fuse-alert-type-info { + + .fuse-alert-container { + @apply bg-blue-50 ring-1 ring-blue-400 ring-inset; + + .fuse-alert-icon { + @apply text-blue-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-blue-900; + } + + .fuse-alert-message { + @apply text-blue-700; + } + + code { + @apply bg-blue-200 text-blue-800; + } + + .dark & { + @apply bg-blue-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-blue-200; + } + } + } + } + + /* Success */ + &.fuse-alert-type-success { + + .fuse-alert-container { + @apply bg-green-50 ring-1 ring-green-400 ring-inset; + + .fuse-alert-icon { + @apply text-green-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-green-900; + } + + .fuse-alert-message { + @apply text-green-700; + } + + code { + @apply bg-green-200 text-green-800; + } + + .dark & { + @apply bg-green-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-green-200; + } + } + } + } + + /* Warning */ + &.fuse-alert-type-warning { + + .fuse-alert-container { + @apply bg-amber-50 ring-1 ring-amber-400 ring-inset; + + .fuse-alert-icon { + @apply text-amber-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-amber-900; + } + + .fuse-alert-message { + @apply text-amber-700; + } + + code { + @apply bg-amber-200 text-amber-800; + } + + .dark & { + @apply bg-amber-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-amber-200; + } + } + } + } + + /* Error */ + &.fuse-alert-type-error { + + .fuse-alert-container { + @apply bg-red-50 ring-1 ring-red-400 ring-inset; + + .fuse-alert-icon { + @apply text-red-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-red-900; + } + + .fuse-alert-message { + @apply text-red-700; + } + + code { + @apply bg-red-200 text-red-800; + } + + .dark & { + @apply bg-red-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-red-200; + } + } + } + } + } + + /* Soft */ + &.fuse-alert-appearance-soft { + + .fuse-alert-container { + border-radius: 6px; + } + + /* Primary */ + &.fuse-alert-type-primary { + + .fuse-alert-container { + @apply bg-primary-50; + + .fuse-alert-icon { + @apply text-primary-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-primary-900; + } + + .fuse-alert-message { + @apply text-primary-700; + } + + code { + @apply text-primary-800 bg-primary-200; + } + + .dark & { + @apply bg-primary-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-primary-200; + } + } + } + } + + /* Accent */ + &.fuse-alert-type-accent { + + .fuse-alert-container { + @apply bg-accent-100; + + .fuse-alert-icon { + @apply text-accent-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-accent-900; + } + + .fuse-alert-message { + @apply text-accent-700; + } + + code { + @apply text-accent-800 bg-accent-200; + } + + .dark & { + @apply bg-accent-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-accent-200; + } + } + } + } + + /* Warn */ + &.fuse-alert-type-warn { + + .fuse-alert-container { + @apply bg-warn-50; + + .fuse-alert-icon { + @apply text-warn-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-warn-900; + } + + .fuse-alert-message { + @apply text-warn-700; + } + + code { + @apply text-warn-800 bg-warn-200; + } + + .dark & { + @apply bg-warn-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-warn-200; + } + } + } + } + + /* Basic */ + &.fuse-alert-type-basic { + + .fuse-alert-container { + @apply bg-gray-100; + + .fuse-alert-icon { + @apply text-gray-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-gray-900; + } + + .fuse-alert-message { + @apply text-gray-700; + } + + code { + @apply bg-gray-200 text-gray-800; + } + + .dark & { + @apply bg-gray-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-gray-200; + } + } + } + } + + /* Info */ + &.fuse-alert-type-info { + + .fuse-alert-container { + @apply bg-blue-50; + + .fuse-alert-icon { + @apply text-blue-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-blue-900; + } + + .fuse-alert-message { + @apply text-blue-700; + } + + code { + @apply bg-blue-200 text-blue-800; + } + + .dark & { + @apply bg-blue-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-blue-200; + } + } + } + } + + /* Success */ + &.fuse-alert-type-success { + + .fuse-alert-container { + @apply bg-green-50; + + .fuse-alert-icon { + @apply text-green-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-green-900; + } + + .fuse-alert-message { + @apply text-green-700; + } + + code { + @apply bg-green-200 text-green-800; + } + + .dark & { + @apply bg-green-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-green-200; + } + } + } + } + + /* Warning */ + &.fuse-alert-type-warning { + + .fuse-alert-container { + @apply bg-amber-50; + + .fuse-alert-icon { + @apply text-amber-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-amber-900; + } + + .fuse-alert-message { + @apply text-amber-700; + } + + code { + @apply bg-amber-200 text-amber-800; + } + + .dark & { + @apply bg-amber-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-amber-200; + } + } + } + } + + /* Error */ + &.fuse-alert-type-error { + + .fuse-alert-container { + @apply bg-red-50; + + .fuse-alert-icon { + @apply text-red-600; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-red-900; + } + + .fuse-alert-message { + @apply text-red-700; + } + + code { + @apply bg-red-200 text-red-800; + } + + .dark & { + @apply bg-red-600; + + .fuse-alert-icon { + @apply text-white; + } + + .fuse-alert-title, + .fuse-alert-dismiss-button { + @apply text-white; + } + + .fuse-alert-message { + @apply text-red-200; + } + } + } + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.ts b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d8e0fd6dc8566788362fec5bc52a269ef3576b5a --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.component.ts @@ -0,0 +1,212 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { filter, Subject, takeUntil } from 'rxjs'; +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; +import { fuseAnimations } from '@fuse/animations'; +import { FuseAlertAppearance, FuseAlertType } from '@fuse/components/alert/alert.types'; +import { FuseAlertService } from '@fuse/components/alert/alert.service'; +import { FuseUtilsService } from '@fuse/services/utils/utils.service'; + +@Component({ + selector : 'fuse-alert', + templateUrl : './alert.component.html', + styleUrls : ['./alert.component.scss'], + encapsulation : ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + animations : fuseAnimations, + exportAs : 'fuseAlert' +}) +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 */ + + @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>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseAlertService: FuseAlertService, + private _fuseUtilsService: FuseUtilsService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Host binding for component classes + */ + @HostBinding('class') get classList(): any + { + 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' + }; + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/alert/alert.module.ts b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..beecabdd92767a3171224bddfcea7b7b61ac2ff3 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { FuseAlertComponent } from '@fuse/components/alert/alert.component'; + +@NgModule({ + declarations: [ + FuseAlertComponent + ], + imports : [ + CommonModule, + MatButtonModule, + MatIconModule + ], + exports : [ + FuseAlertComponent + ] +}) +export class FuseAlertModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/alert/alert.service.ts b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a04dbe5c7bbb78d16be6f2c894e87dec98c43fdf --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.service.ts @@ -0,0 +1,77 @@ +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); + + /** + * Constructor + */ + constructor() + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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); + } + +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/alert/alert.types.ts b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc3516e1b6dd0800181eb072fa72de1a92e50ac5 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/alert/alert.types.ts @@ -0,0 +1,15 @@ +export type FuseAlertAppearance = + | 'border' + | 'fill' + | 'outline' + | 'soft'; + +export type FuseAlertType = + | 'primary' + | 'accent' + | 'warn' + | 'basic' + | 'info' + | 'success' + | 'warning' + | 'error'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/alert/index.ts b/transparency_dashboard_frontend/src/@fuse/components/alert/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c18a807a8095773e19a5d97c0d9bf14776fba703 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/alert/index.ts @@ -0,0 +1 @@ +export * from '@fuse/components/alert/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/alert/public-api.ts b/transparency_dashboard_frontend/src/@fuse/components/alert/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..fdf984ae59f2bf50eaa4cd484c0272e9c4de7776 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/alert/public-api.ts @@ -0,0 +1,4 @@ +export * from '@fuse/components/alert/alert.component'; +export * from '@fuse/components/alert/alert.module'; +export * from '@fuse/components/alert/alert.service'; +export * from '@fuse/components/alert/alert.types'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/card/card.component.html b/transparency_dashboard_frontend/src/@fuse/components/card/card.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5728d204b827ce762a5e4ba5a8ee0cc6b8076705 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/card/card.component.html @@ -0,0 +1,30 @@ +<!-- Flippable card --> +<ng-container *ngIf="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> + +</ng-container> + +<!-- Normal card --> +<ng-container *ngIf="!flippable"> + + <!-- Content --> + <ng-content></ng-content> + + <!-- Expansion --> + <div + class="fuse-card-expansion" + *ngIf="expanded" + [@expandCollapse]> + <ng-content select="[fuseCardExpansion]"></ng-content> + </div> + +</ng-container> diff --git a/transparency_dashboard_frontend/src/@fuse/components/card/card.component.scss b/transparency_dashboard_frontend/src/@fuse/components/card/card.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..2e2719d0dc6a5627d0e19d037a3d41faa5e19b34 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/card/card.component.scss @@ -0,0 +1,63 @@ +fuse-card { + position: relative; + display: flex; + overflow: hidden; + @apply rounded-2xl shadow bg-card; + + /* 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 rounded-2xl shadow bg-card; + } + + .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; + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/card/card.component.ts b/transparency_dashboard_frontend/src/@fuse/components/card/card.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd9e0f7d3b6a7b854ca4420852f2210a4ba6fea6 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/card/card.component.ts @@ -0,0 +1,74 @@ +import { Component, HostBinding, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; +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' +}) +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; + + /** + * Constructor + */ + constructor() + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Host binding for component classes + */ + @HostBinding('class') get classList(): any + { + 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 + }; + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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); + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/card/card.module.ts b/transparency_dashboard_frontend/src/@fuse/components/card/card.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..c600e30c3ed3972d6fc3d9914cc6d620884350cb --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/card/card.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FuseCardComponent } from '@fuse/components/card/card.component'; + +@NgModule({ + declarations: [ + FuseCardComponent + ], + imports : [ + CommonModule + ], + exports : [ + FuseCardComponent + ] +}) +export class FuseCardModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/card/card.types.ts b/transparency_dashboard_frontend/src/@fuse/components/card/card.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..d925642692fee4244aaeb7c692ca54e8986b95d6 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/card/card.types.ts @@ -0,0 +1,3 @@ +export type FuseCardFace = + | 'front' + | 'back'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/card/index.ts b/transparency_dashboard_frontend/src/@fuse/components/card/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac978a7894c28846d19f51cd768ba902826d38e4 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/card/index.ts @@ -0,0 +1 @@ +export * from '@fuse/components/card/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/card/public-api.ts b/transparency_dashboard_frontend/src/@fuse/components/card/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee2ca6e847029c413509d39a8dcf1b8d031a8a65 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/card/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/components/card/card.component'; +export * from '@fuse/components/card/card.module'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.html b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b3a8acc45612b20241e9348a573208c1d94b1f78 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.html @@ -0,0 +1,3 @@ +<div class="fuse-drawer-content"> + <ng-content></ng-content> +</div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.scss b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e10cad2afd377062cbc90a27a9dbd43d0e4b5b77 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.scss @@ -0,0 +1,133 @@ +/* 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, .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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.ts b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d644c14b15908aa5a39eda27b8484dbf81dcc945 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.component.ts @@ -0,0 +1,437 @@ +import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations'; +import { FuseDrawerMode, FuseDrawerPosition } from '@fuse/components/drawer/drawer.types'; +import { FuseDrawerService } from '@fuse/components/drawer/drawer.service'; +import { FuseUtilsService } from '@fuse/services/utils/utils.service'; +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector : 'fuse-drawer', + templateUrl : './drawer.component.html', + styleUrls : ['./drawer.component.scss'], + encapsulation: ViewEncapsulation.None, + exportAs : 'fuseDrawer' +}) +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 */ + + @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 _hovered: boolean = false; + private _overlay: HTMLElement; + private _player: AnimationPlayer; + + /** + * Constructor + */ + constructor( + private _animationBuilder: AnimationBuilder, + private _elementRef: ElementRef, + private _renderer2: Renderer2, + private _fuseDrawerService: FuseDrawerService, + private _fuseUtilsService: FuseUtilsService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Host binding for component classes + */ + @HostBinding('class') get classList(): any + { + 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 + }; + } + + /** + * 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'); + + // Return if overlay couldn't be create for some reason + if ( !this._overlay ) + { + return; + } + + // 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 the 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); + + // Once the animation is done... + this._player.onDone(() => { + + // Destroy the player + this._player.destroy(); + this._player = null; + }); + + // Play the animation + this._player.play(); + + // Add an event listener to the overlay + this._overlay.addEventListener('click', () => { + this.close(); + }); + } + + /** + * Hide the backdrop + * + * @private + */ + private _hideOverlay(): void + { + if ( !this._overlay ) + { + return; + } + + // Create the leave animation and attach it to the player + this._player = this._animationBuilder.build([ + animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 0})) + ]).create(this._overlay); + + // Play the animation + this._player.play(); + + // Once the animation is done... + this._player.onDone(() => { + + // Destroy the player + this._player.destroy(); + this._player = null; + + // If the backdrop still exists... + if ( this._overlay ) + { + // Remove the backdrop + 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); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.module.ts b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..d8383a078238e3267be4b26146fc371c708b82b7 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FuseDrawerComponent } from '@fuse/components/drawer/drawer.component'; + +@NgModule({ + declarations: [ + FuseDrawerComponent + ], + imports : [ + CommonModule + ], + exports : [ + FuseDrawerComponent + ] +}) +export class FuseDrawerModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.service.ts b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7de348aa731a9132d97e562b0b137b4c295983d5 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.service.ts @@ -0,0 +1,52 @@ +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>(); + + /** + * Constructor + */ + constructor() + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.types.ts b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..40e2ee3961f7444801112e962ad8dd744ebc32a4 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/drawer/drawer.types.ts @@ -0,0 +1,7 @@ +export type FuseDrawerMode = + | 'over' + | 'side'; + +export type FuseDrawerPosition = + | 'left' + | 'right'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/drawer/index.ts b/transparency_dashboard_frontend/src/@fuse/components/drawer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3611d47d9dab6df5a30986fa7145c3e3fce6dd55 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/drawer/index.ts @@ -0,0 +1 @@ +export * from '@fuse/components/drawer/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/drawer/public-api.ts b/transparency_dashboard_frontend/src/@fuse/components/drawer/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..2439ec120232d3b4e4ea360623517ec5cc4ed7a2 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/drawer/public-api.ts @@ -0,0 +1,4 @@ +export * from '@fuse/components/drawer/drawer.component'; +export * from '@fuse/components/drawer/drawer.module'; +export * from '@fuse/components/drawer/drawer.service'; +export * from '@fuse/components/drawer/drawer.types'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.component.html b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.component.html new file mode 100644 index 0000000000000000000000000000000000000000..77cb0f1dafc82ce8917684a20e0fe45938e9bdfa --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.component.html @@ -0,0 +1,12 @@ +<!-- 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-expand'"></mat-icon> +</ng-template> diff --git a/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.component.ts b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..38ec191df5c3ba3c317f4179696b4272bb2b246d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.component.ts @@ -0,0 +1,166 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, OnInit, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { FSDocument, FSDocumentElement } from '@fuse/components/fullscreen/fullscreen.types'; + +@Component({ + selector : 'fuse-fullscreen', + templateUrl : './fullscreen.component.html', + encapsulation : ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + exportAs : 'fuseFullscreen' +}) +export class FuseFullscreenComponent implements OnInit +{ + @Input() iconTpl: TemplateRef<any>; + @Input() tooltip: string; + private _fsDoc: FSDocument; + private _fsDocEl: FSDocumentElement; + private _isFullscreen: boolean = false; + + /** + * Constructor + */ + constructor(@Inject(DOCUMENT) private _document: Document) + { + this._fsDoc = _document as FSDocument; + } + + // ----------------------------------------------------------------------------------------------------- + // @ Lifecycle hooks + // ----------------------------------------------------------------------------------------------------- + + /** + * On init + */ + ngOnInit(): void + { + this._fsDocEl = document.documentElement as FSDocumentElement; + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Toggle the fullscreen mode + */ + toggleFullscreen(): void + { + // Check if the fullscreen is open + this._isFullscreen = this._getBrowserFullscreenElement() !== null; + + // Toggle the fullscreen + if ( this._isFullscreen ) + { + this._closeFullscreen(); + } + else + { + this._openFullscreen(); + } + } + + // ----------------------------------------------------------------------------------------------------- + // @ Private methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Get browser's fullscreen element + * + * @private + */ + private _getBrowserFullscreenElement(): Element + { + if ( typeof this._fsDoc.fullscreenElement !== 'undefined' ) + { + return this._fsDoc.fullscreenElement; + } + + if ( typeof this._fsDoc.mozFullScreenElement !== 'undefined' ) + { + return this._fsDoc.mozFullScreenElement; + } + + if ( typeof this._fsDoc.msFullscreenElement !== 'undefined' ) + { + return this._fsDoc.msFullscreenElement; + } + + if ( typeof this._fsDoc.webkitFullscreenElement !== 'undefined' ) + { + return this._fsDoc.webkitFullscreenElement; + } + + throw new Error('Fullscreen mode is not supported by this browser'); + } + + /** + * Open the fullscreen + * + * @private + */ + private _openFullscreen(): void + { + if ( this._fsDocEl.requestFullscreen ) + { + this._fsDocEl.requestFullscreen(); + return; + } + + // Firefox + if ( this._fsDocEl.mozRequestFullScreen ) + { + this._fsDocEl.mozRequestFullScreen(); + return; + } + + // Chrome, Safari and Opera + if ( this._fsDocEl.webkitRequestFullscreen ) + { + this._fsDocEl.webkitRequestFullscreen(); + return; + } + + // IE/Edge + if ( this._fsDocEl.msRequestFullscreen ) + { + this._fsDocEl.msRequestFullscreen(); + return; + } + } + + /** + * Close the fullscreen + * + * @private + */ + private _closeFullscreen(): void + { + if ( this._fsDoc.exitFullscreen ) + { + this._fsDoc.exitFullscreen(); + return; + } + + // Firefox + if ( this._fsDoc.mozCancelFullScreen ) + { + this._fsDoc.mozCancelFullScreen(); + return; + } + + // Chrome, Safari and Opera + if ( this._fsDoc.webkitExitFullscreen ) + { + this._fsDoc.webkitExitFullscreen(); + return; + } + + // IE/Edge + else if ( this._fsDoc.msExitFullscreen ) + { + this._fsDoc.msExitFullscreen(); + return; + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.module.ts b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..2237f2af2b523c55770604c02f5065c8a8ea2a6c --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { FuseFullscreenComponent } from '@fuse/components/fullscreen/fullscreen.component'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + declarations: [ + FuseFullscreenComponent + ], + imports : [ + MatButtonModule, + MatIconModule, + MatTooltipModule, + CommonModule + ], + exports : [ + FuseFullscreenComponent + ] +}) +export class FuseFullscreenModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.types.ts b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..0477a1d414c742d9ee8e9c023136019fc4f00f8f --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/fullscreen.types.ts @@ -0,0 +1,16 @@ +export interface FSDocument extends HTMLDocument +{ + mozFullScreenElement?: Element; + mozCancelFullScreen?: () => void; + msFullscreenElement?: Element; + msExitFullscreen?: () => void; + webkitFullscreenElement?: Element; + webkitExitFullscreen?: () => void; +} + +export interface FSDocumentElement extends HTMLElement +{ + mozRequestFullScreen?: () => void; + msRequestFullscreen?: () => void; + webkitRequestFullscreen?: () => void; +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/fullscreen/index.ts b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..15b41faf99d406fd5343bc83bf8605968f92ff97 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/index.ts @@ -0,0 +1 @@ +export * from '@fuse/components/fullscreen/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/fullscreen/public-api.ts b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..e6264e3c7c1fe1d57acbf8720a340213f0177413 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/fullscreen/public-api.ts @@ -0,0 +1,3 @@ +export * from '@fuse/components/fullscreen/fullscreen.component'; +export * from '@fuse/components/fullscreen/fullscreen.module'; +export * from '@fuse/components/fullscreen/fullscreen.types'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.html b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.html new file mode 100644 index 0000000000000000000000000000000000000000..da345b9bb283a0fe04354a51b6b7d0fe61ff6903 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.html @@ -0,0 +1,9 @@ +<ng-content></ng-content> + +<!-- @formatter:off --> +<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> +<!-- @formatter:on --> diff --git a/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.scss b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..b433c6ffc787067cd335865d2717f80cc9fbcd34 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.scss @@ -0,0 +1,3 @@ +textarea[fuse-highlight] { + display: none; +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.ts b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..13923796eaf8c45d826354907dcfa3e1f4246ead --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.component.ts @@ -0,0 +1,132 @@ +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EmbeddedViewRef, Input, OnChanges, Renderer2, 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' +}) +export class FuseHighlightComponent implements OnChanges, AfterViewInit +{ + @Input() code: string; + @Input() lang: string; + @ViewChild(TemplateRef) templateRef: TemplateRef<any>; + + highlightedCode: string; + private _viewRef: EmbeddedViewRef<any>; + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _domSanitizer: DomSanitizer, + private _elementRef: ElementRef, + private _renderer2: Renderer2, + private _fuseHighlightService: FuseHighlightService, + private _viewContainerRef: ViewContainerRef + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.module.ts b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..a61149d1e3693e7de2cbd28df6adbef4cd9824df --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FuseHighlightComponent } from '@fuse/components/highlight/highlight.component'; + +@NgModule({ + declarations: [ + FuseHighlightComponent + ], + imports : [ + CommonModule + ], + exports : [ + FuseHighlightComponent + ] +}) +export class FuseHighlightModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.service.ts b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..bb627bc2f972bc7e588a53bfe7f407d8f618b0e5 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/highlight/highlight.service.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@angular/core'; +import hljs from 'highlight.js'; + +@Injectable({ + providedIn: 'root' +}) +export class FuseHighlightService +{ + /** + * Constructor + */ + constructor() + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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'); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/highlight/index.ts b/transparency_dashboard_frontend/src/@fuse/components/highlight/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..46f52e2ef82fc672d88d6dee82541af74dd0a18b --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/highlight/index.ts @@ -0,0 +1 @@ +export * from '@fuse/components/highlight/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/highlight/public-api.ts b/transparency_dashboard_frontend/src/@fuse/components/highlight/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..15c2edc16a0f9c31d7edaf4d3abc0dcdf4ab8978 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/highlight/public-api.ts @@ -0,0 +1,3 @@ +export * from '@fuse/components/highlight/highlight.component'; +export * from '@fuse/components/highlight/highlight.module'; +export * from '@fuse/components/highlight/highlight.service'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/loading-bar/index.ts b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..780b263a80304809b232b55fb33cf442c195e168 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/index.ts @@ -0,0 +1 @@ +export * from '@fuse/components/loading-bar/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.html b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3f884fef4634119ad6fdecb6f853dedd914496a3 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.html @@ -0,0 +1,5 @@ +<ng-container *ngIf="show"> + <mat-progress-bar + [mode]="mode" + [value]="progress"></mat-progress-bar> +</ng-container> diff --git a/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.scss b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..7a46ec0eae3864c8c6612d3ce97ab97c5a770148 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.scss @@ -0,0 +1,7 @@ +fuse-loading-bar { + position: fixed; + top: 0; + z-index: 999; + width: 100%; + height: 6px; +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.ts b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..74c0346581f1eda15eb16170fe06a302477a4f77 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.component.ts @@ -0,0 +1,82 @@ +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Subject, takeUntil } from 'rxjs'; +import { FuseLoadingService } from '@fuse/services/loading'; + +@Component({ + selector : 'fuse-loading-bar', + templateUrl : './loading-bar.component.html', + styleUrls : ['./loading-bar.component.scss'], + encapsulation: ViewEncapsulation.None, + exportAs : 'fuseLoadingBar' +}) +export class FuseLoadingBarComponent implements OnChanges, OnInit, OnDestroy +{ + @Input() autoMode: boolean = true; + mode: 'determinate' | 'indeterminate'; + progress: number = 0; + show: boolean = false; + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor(private _fuseLoadingService: FuseLoadingService) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.module.ts b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..b371b8ac3601ecd0166524ee78ea5499a0332737 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/loading-bar.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { FuseLoadingBarComponent } from '@fuse/components/loading-bar/loading-bar.component'; + +@NgModule({ + declarations: [ + FuseLoadingBarComponent + ], + imports : [ + CommonModule, + MatProgressBarModule + ], + exports : [ + FuseLoadingBarComponent + ] +}) +export class FuseLoadingBarModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/loading-bar/public-api.ts b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..471ccd33dfe19a48619d615966abd38718e493e3 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/loading-bar/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/components/loading-bar/loading-bar.component'; +export * from '@fuse/components/loading-bar/loading-bar.module'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/masonry/index.ts b/transparency_dashboard_frontend/src/@fuse/components/masonry/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2f1a036d4fac5fb2e5df0333fb01ec0958e70c1 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/masonry/index.ts @@ -0,0 +1 @@ +export * from '@fuse/components/masonry/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.component.html b/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4b0a67198f67b33f1b1f36bc1ec428c63f97bb2b --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.component.html @@ -0,0 +1,3 @@ +<div class="flex"> + <ng-container *ngTemplateOutlet="columnsTemplate; context: { $implicit: distributedColumns }"></ng-container> +</div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.component.ts b/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6737ab97f925cdcb0f350c66fe6201a8877f9967 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.component.ts @@ -0,0 +1,86 @@ +import { AfterViewInit, Component, Input, OnChanges, SimpleChanges, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { fuseAnimations } from '@fuse/animations'; +import { FuseMediaWatcherService } from '@fuse/services/media-watcher'; + +@Component({ + selector : 'fuse-masonry', + templateUrl : './masonry.component.html', + encapsulation: ViewEncapsulation.None, + animations : fuseAnimations, + exportAs : 'fuseMasonry' +}) +export class FuseMasonryComponent implements OnChanges, AfterViewInit +{ + @Input() columnsTemplate: TemplateRef<any>; + @Input() columns: number; + @Input() items: any[] = []; + distributedColumns: any[] = []; + + /** + * Constructor + */ + constructor(private _fuseMediaWatcherService: FuseMediaWatcherService) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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]); + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.module.ts b/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..651c5510a4818a768a96d702dc8d271d2f6172de --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/masonry/masonry.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FuseMasonryComponent } from '@fuse/components/masonry/masonry.component'; + +@NgModule({ + declarations: [ + FuseMasonryComponent + ], + imports : [ + CommonModule + ], + exports : [ + FuseMasonryComponent + ] +}) +export class FuseMasonryModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/masonry/public-api.ts b/transparency_dashboard_frontend/src/@fuse/components/masonry/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..e074c48cb96ebeb32991d61f71f7d214b3cc3b54 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/masonry/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/components/masonry/masonry.component'; +export * from '@fuse/components/masonry/masonry.module'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/basic/basic.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/basic/basic.component.html new file mode 100644 index 0000000000000000000000000000000000000000..48aab97de423ad6941a1abfeb2acd9b77fcaa399 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/basic/basic.component.html @@ -0,0 +1,126 @@ +<!-- 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 --> + <ng-container *ngIf="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]" + [routerLinkActive]="'fuse-horizontal-navigation-item-active'" + [routerLinkActiveOptions]="isActiveMatchOptions" + [matTooltip]="item.tooltip || ''"> + <ng-container *ngTemplateOutlet="itemTemplate"></ng-container> + </div> + </ng-container> + + <!-- Item with an external link --> + <ng-container *ngIf="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> + </ng-container> + + <!-- Item with a function --> + <ng-container *ngIf="!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> + </ng-container> + + <!-- Item with an internal link and function --> + <ng-container *ngIf="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]" + [routerLinkActive]="'fuse-horizontal-navigation-item-active'" + [routerLinkActiveOptions]="isActiveMatchOptions" + [matTooltip]="item.tooltip || ''" + (click)="item.function(item)"> + <ng-container *ngTemplateOutlet="itemTemplate"></ng-container> + </div> + </ng-container> + + <!-- Item with an external link and function --> + <ng-container *ngIf="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> + </ng-container> + + <!-- Item with a no link and no function --> + <ng-container *ngIf="!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> + </ng-container> + + <!-- Item is disabled --> + <ng-container *ngIf="item.disabled"> + <div class="fuse-horizontal-navigation-item fuse-horizontal-navigation-item-disabled"> + <ng-container *ngTemplateOutlet="itemTemplate"></ng-container> + </div> + </ng-container> + +</div> + +<!-- Item template --> +<ng-template #itemTemplate> + + <!-- Icon --> + <ng-container *ngIf="item.icon"> + <mat-icon + class="fuse-horizontal-navigation-item-icon" + [ngClass]="item.classes?.icon" + [svgIcon]="item.icon"></mat-icon> + </ng-container> + + <!-- 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> + <ng-container *ngIf="item.subtitle"> + <div class="fuse-horizontal-navigation-item-subtitle text-hint"> + <span [ngClass]="item.classes?.subtitle"> + {{item.subtitle}} + </span> + </div> + </ng-container> + </div> + + <!-- Badge --> + <ng-container *ngIf="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-container> + +</ng-template> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/basic/basic.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/basic/basic.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..af49292aaddce54bdfd8ad51dcdf49de19ff89ee --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/basic/basic.component.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { IsActiveMatchOptions } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +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'; + +@Component({ + selector : 'fuse-horizontal-navigation-basic-item', + templateUrl : './basic.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseHorizontalNavigationBasicItemComponent implements OnInit, OnDestroy +{ + @Input() item: FuseNavigationItem; + @Input() name: string; + + isActiveMatchOptions: IsActiveMatchOptions; + private _fuseHorizontalNavigationComponent: FuseHorizontalNavigationComponent; + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService, + private _fuseUtilsService: FuseUtilsService + ) + { + // 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. + this.isActiveMatchOptions = this._fuseUtilsService.subsetMatchOptions; + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/branch/branch.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/branch/branch.component.html new file mode 100644 index 0000000000000000000000000000000000000000..87aafa1ee9c24381e9199155c5c6c426d424ce84 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/branch/branch.component.html @@ -0,0 +1,121 @@ +<ng-container *ngIf="!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> +</ng-container> + +<mat-menu + class="fuse-horizontal-navigation-menu-panel" + [overlapTrigger]="false" + #matMenu="matMenu"> + + <ng-container *ngFor="let item of item.children; trackBy: trackByFn"> + + <!-- Skip the hidden items --> + <ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden"> + + <!-- Basic --> + <ng-container *ngIf="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> + </ng-container> + + <!-- Branch: aside, collapsable, group --> + <ng-container *ngIf="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> + </ng-container> + + <!-- Divider --> + <ng-container *ngIf="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> + </ng-container> + + </ng-container> + + </ng-container> + +</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 --> + <ng-container *ngIf="item.icon"> + <mat-icon + class="fuse-horizontal-navigation-item-icon" + [ngClass]="item.classes?.icon" + [svgIcon]="item.icon"></mat-icon> + </ng-container> + + <!-- 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> + <ng-container *ngIf="item.subtitle"> + <div class="fuse-horizontal-navigation-item-subtitle text-hint"> + <span [ngClass]="item.classes?.subtitle"> + {{item.subtitle}} + </span> + </div> + </ng-container> + </div> + + <!-- Badge --> + <ng-container *ngIf="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-container> + </div> + </div> + +</ng-template> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/branch/branch.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/branch/branch.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..69464d8a5c50f9babc8882f3b1919be81acb39d6 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/branch/branch.component.ts @@ -0,0 +1,93 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { BooleanInput } from '@angular/cdk/coercion'; +import { MatMenu } from '@angular/material/menu'; +import { Subject, takeUntil } from 'rxjs'; +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'; + +@Component({ + selector : 'fuse-horizontal-navigation-branch-item', + templateUrl : './branch.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseHorizontalNavigationBranchItemComponent implements OnInit, OnDestroy +{ + /* eslint-disable @typescript-eslint/naming-convention */ + static ngAcceptInputType_child: BooleanInput; + /* eslint-enable @typescript-eslint/naming-convention */ + + @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>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/divider/divider.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/divider/divider.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5675966aad48927f1da89d3c695a95a3e5531542 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/divider/divider.component.html @@ -0,0 +1,4 @@ +<!-- Divider --> +<div + class="fuse-horizontal-navigation-item-wrapper divider" + [ngClass]="item.classes?.wrapper"></div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/divider/divider.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/divider/divider.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..06fb7087087e1591f47808ea3a8bad6ada64e597 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/divider/divider.component.ts @@ -0,0 +1,61 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; +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'; + +@Component({ + selector : 'fuse-horizontal-navigation-divider-item', + templateUrl : './divider.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseHorizontalNavigationDividerItemComponent implements OnInit, OnDestroy +{ + @Input() item: FuseNavigationItem; + @Input() name: string; + + private _fuseHorizontalNavigationComponent: FuseHorizontalNavigationComponent; + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/spacer/spacer.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/spacer/spacer.component.html new file mode 100644 index 0000000000000000000000000000000000000000..97fbd30f5fa6029a262d8be84f0dc83cb9920f3c --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/spacer/spacer.component.html @@ -0,0 +1,4 @@ +<!-- Spacer --> +<div + class="fuse-horizontal-navigation-item-wrapper" + [ngClass]="item.classes?.wrapper"></div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/spacer/spacer.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/spacer/spacer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f05107d5f0a7d19af8871d8a87ffa7f8c7793b60 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/components/spacer/spacer.component.ts @@ -0,0 +1,61 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; +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'; + +@Component({ + selector : 'fuse-horizontal-navigation-spacer-item', + templateUrl : './spacer.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseHorizontalNavigationSpacerItemComponent implements OnInit, OnDestroy +{ + @Input() item: FuseNavigationItem; + @Input() name: string; + + private _fuseHorizontalNavigationComponent: FuseHorizontalNavigationComponent; + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..7db23d9950f8bddcaaa48f827ffa363ee2b7e8b6 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.html @@ -0,0 +1,36 @@ +<div class="fuse-horizontal-navigation-wrapper"> + + <ng-container *ngFor="let item of navigation; trackBy: trackByFn"> + + <!-- Skip the hidden items --> + <ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden"> + + <!-- Basic --> + <ng-container *ngIf="item.type === 'basic'"> + <fuse-horizontal-navigation-basic-item + class="fuse-horizontal-navigation-menu-item" + [item]="item" + [name]="name"></fuse-horizontal-navigation-basic-item> + </ng-container> + + <!-- Branch: aside, collapsable, group --> + <ng-container *ngIf="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> + </ng-container> + + <!-- Spacer --> + <ng-container *ngIf="item.type === 'spacer'"> + <fuse-horizontal-navigation-spacer-item + class="fuse-horizontal-navigation-menu-item" + [item]="item" + [name]="name"></fuse-horizontal-navigation-spacer-item> + </ng-container> + + </ng-container> + + </ng-container> + +</div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.scss b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..78e2efdfb897f6bfb8c766b547bdce98126de8d3 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.scss @@ -0,0 +1,181 @@ +/* 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; + } + } + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..96cbd6e9d3b343cf8894ba19c7e2ef4f8c84d93b --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/horizontal/horizontal.component.ts @@ -0,0 +1,109 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { ReplaySubject, Subject } from 'rxjs'; +import { fuseAnimations } from '@fuse/animations'; +import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types'; +import { FuseNavigationService } from '@fuse/components/navigation/navigation.service'; +import { FuseUtilsService } from '@fuse/services/utils/utils.service'; + +@Component({ + selector : 'fuse-horizontal-navigation', + templateUrl : './horizontal.component.html', + styleUrls : ['./horizontal.component.scss'], + animations : fuseAnimations, + encapsulation : ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + exportAs : 'fuseHorizontalNavigation' +}) +export class FuseHorizontalNavigationComponent implements OnChanges, OnInit, OnDestroy +{ + @Input() name: string = this._fuseUtilsService.randomId(); + @Input() navigation: FuseNavigationItem[]; + + onRefreshed: ReplaySubject<boolean> = new ReplaySubject<boolean>(1); + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService, + private _fuseUtilsService: FuseUtilsService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/index.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2598d801b4309f67c4266f6adbb8fec978bdbe6c --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/index.ts @@ -0,0 +1 @@ +export * from '@fuse/components/navigation/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.module.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..d93c6b7bb577555338b2f30d9cb0dce704002df1 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.module.ts @@ -0,0 +1,55 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { FuseScrollbarModule } from '@fuse/directives/scrollbar/public-api'; +import { FuseHorizontalNavigationBasicItemComponent } from '@fuse/components/navigation/horizontal/components/basic/basic.component'; +import { FuseHorizontalNavigationBranchItemComponent } from '@fuse/components/navigation/horizontal/components/branch/branch.component'; +import { FuseHorizontalNavigationDividerItemComponent } from '@fuse/components/navigation/horizontal/components/divider/divider.component'; +import { FuseHorizontalNavigationSpacerItemComponent } from '@fuse/components/navigation/horizontal/components/spacer/spacer.component'; +import { FuseHorizontalNavigationComponent } from '@fuse/components/navigation/horizontal/horizontal.component'; +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 { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component'; + +@NgModule({ + declarations: [ + FuseHorizontalNavigationBasicItemComponent, + FuseHorizontalNavigationBranchItemComponent, + FuseHorizontalNavigationDividerItemComponent, + FuseHorizontalNavigationSpacerItemComponent, + FuseHorizontalNavigationComponent, + FuseVerticalNavigationAsideItemComponent, + FuseVerticalNavigationBasicItemComponent, + FuseVerticalNavigationCollapsableItemComponent, + FuseVerticalNavigationDividerItemComponent, + FuseVerticalNavigationGroupItemComponent, + FuseVerticalNavigationSpacerItemComponent, + FuseVerticalNavigationComponent + ], + imports : [ + CommonModule, + RouterModule, + MatButtonModule, + MatDividerModule, + MatIconModule, + MatMenuModule, + MatTooltipModule, + FuseScrollbarModule + ], + exports : [ + FuseHorizontalNavigationComponent, + FuseVerticalNavigationComponent + ] +}) +export class FuseNavigationModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.service.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c5bea807627e2483bc1d048c6c2d0c69118c892 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.service.ts @@ -0,0 +1,186 @@ +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>(); + + /** + * Constructor + */ + constructor() + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.types.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b6b35045f5d0ba850415d2de979f55916545b12 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/navigation.types.ts @@ -0,0 +1,57 @@ +import { IsActiveMatchOptions } 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; + 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'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/public-api.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..032e3ef674f8821d44baa71e8e8a5ab23269cda7 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/public-api.ts @@ -0,0 +1,5 @@ +export * from '@fuse/components/navigation/horizontal/horizontal.component'; +export * from '@fuse/components/navigation/vertical/vertical.component'; +export * from '@fuse/components/navigation/navigation.module'; +export * from '@fuse/components/navigation/navigation.service'; +export * from '@fuse/components/navigation/navigation.types'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/aside/aside.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/aside/aside.component.html new file mode 100644 index 0000000000000000000000000000000000000000..469d7b61a8a6e90053fd27e3cc43878c91bfff52 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/aside/aside.component.html @@ -0,0 +1,103 @@ +<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 --> + <ng-container *ngIf="item.icon"> + <mat-icon + class="fuse-vertical-navigation-item-icon" + [ngClass]="item.classes?.icon" + [svgIcon]="item.icon"></mat-icon> + </ng-container> + + <!-- 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> + <ng-container *ngIf="item.subtitle"> + <div class="fuse-vertical-navigation-item-subtitle"> + <span [ngClass]="item.classes?.subtitle"> + {{item.subtitle}} + </span> + </div> + </ng-container> + </div> + + <!-- Badge --> + <ng-container *ngIf="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-container> + + </div> + +</div> + +<ng-container *ngIf="!skipChildren"> + + <div class="fuse-vertical-navigation-item-children"> + + <ng-container *ngFor="let item of item.children; trackBy: trackByFn"> + + <!-- Skip the hidden items --> + <ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden"> + + <!-- Basic --> + <ng-container *ngIf="item.type === 'basic'"> + <fuse-vertical-navigation-basic-item + [item]="item" + [name]="name"></fuse-vertical-navigation-basic-item> + </ng-container> + + <!-- Collapsable --> + <ng-container *ngIf="item.type === 'collapsable'"> + <fuse-vertical-navigation-collapsable-item + [item]="item" + [name]="name" + [autoCollapse]="autoCollapse"></fuse-vertical-navigation-collapsable-item> + </ng-container> + + <!-- Divider --> + <ng-container *ngIf="item.type === 'divider'"> + <fuse-vertical-navigation-divider-item + [item]="item" + [name]="name"></fuse-vertical-navigation-divider-item> + </ng-container> + + <!-- Group --> + <ng-container *ngIf="item.type === 'group'"> + <fuse-vertical-navigation-group-item + [item]="item" + [name]="name"></fuse-vertical-navigation-group-item> + </ng-container> + + <!-- Spacer --> + <ng-container *ngIf="item.type === 'spacer'"> + <fuse-vertical-navigation-spacer-item + [item]="item" + [name]="name"></fuse-vertical-navigation-spacer-item> + </ng-container> + + </ng-container> + + </ng-container> + + </div> + +</ng-container> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/aside/aside.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/aside/aside.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..de4c51527e56361f5cfd65b86315297425210219 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/aside/aside.component.ts @@ -0,0 +1,186 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { BooleanInput } from '@angular/cdk/coercion'; +import { filter, Subject, takeUntil } from 'rxjs'; +import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component'; +import { FuseNavigationService } from '@fuse/components/navigation/navigation.service'; +import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types'; + +@Component({ + selector : 'fuse-vertical-navigation-aside-item', + templateUrl : './aside.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +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 */ + + @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>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _router: Router, + private _fuseNavigationService: FuseNavigationService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/basic/basic.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/basic/basic.component.html new file mode 100644 index 0000000000000000000000000000000000000000..a9a9dd98f48a95b5a2879beba46d2d7a2f39f3a8 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/basic/basic.component.html @@ -0,0 +1,127 @@ +<!-- 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 --> + <ng-container *ngIf="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]" + [routerLinkActive]="'fuse-vertical-navigation-item-active'" + [routerLinkActiveOptions]="isActiveMatchOptions" + [matTooltip]="item.tooltip || ''"> + <ng-container *ngTemplateOutlet="itemTemplate"></ng-container> + </a> + </ng-container> + + <!-- Item with an external link --> + <ng-container *ngIf="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> + </ng-container> + + <!-- Item with a function --> + <ng-container *ngIf="!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> + </ng-container> + + <!-- Item with an internal link and function --> + <ng-container *ngIf="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]" + [routerLinkActive]="'fuse-vertical-navigation-item-active'" + [routerLinkActiveOptions]="isActiveMatchOptions" + [matTooltip]="item.tooltip || ''" + (click)="item.function(item)"> + <ng-container *ngTemplateOutlet="itemTemplate"></ng-container> + </a> + </ng-container> + + <!-- Item with an external link and function --> + <ng-container *ngIf="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> + </ng-container> + + <!-- Item with a no link and no function --> + <ng-container *ngIf="!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> + </ng-container> + + <!-- Item is disabled --> + <ng-container *ngIf="item.disabled"> + <div + class="fuse-vertical-navigation-item fuse-vertical-navigation-item-disabled" + [matTooltip]="item.tooltip || ''"> + <ng-container *ngTemplateOutlet="itemTemplate"></ng-container> + </div> + </ng-container> + +</div> + +<!-- Item template --> +<ng-template #itemTemplate> + + <!-- Icon --> + <ng-container *ngIf="item.icon"> + <mat-icon + class="fuse-vertical-navigation-item-icon" + [ngClass]="item.classes?.icon" + [svgIcon]="item.icon"></mat-icon> + </ng-container> + + <!-- 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> + <ng-container *ngIf="item.subtitle"> + <div class="fuse-vertical-navigation-item-subtitle"> + <span [ngClass]="item.classes?.subtitle"> + {{item.subtitle}} + </span> + </div> + </ng-container> + </div> + + <!-- Badge --> + <ng-container *ngIf="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-container> + +</ng-template> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/basic/basic.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/basic/basic.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5cf689a3b3892889357c4c19538b25770f816a36 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/basic/basic.component.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { IsActiveMatchOptions } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.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'; + +@Component({ + selector : 'fuse-vertical-navigation-basic-item', + templateUrl : './basic.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseVerticalNavigationBasicItemComponent implements OnInit, OnDestroy +{ + @Input() item: FuseNavigationItem; + @Input() name: string; + + isActiveMatchOptions: IsActiveMatchOptions; + private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent; + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService, + private _fuseUtilsService: FuseUtilsService + ) + { + // 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. + this.isActiveMatchOptions = this._fuseUtilsService.subsetMatchOptions; + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/collapsable/collapsable.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/collapsable/collapsable.component.html new file mode 100644 index 0000000000000000000000000000000000000000..770efdb3ab161c66163e6c7fb501e4138db7ed70 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/collapsable/collapsable.component.html @@ -0,0 +1,106 @@ +<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 --> + <ng-container *ngIf="item.icon"> + <mat-icon + class="fuse-vertical-navigation-item-icon" + [ngClass]="item.classes?.icon" + [svgIcon]="item.icon"></mat-icon> + </ng-container> + + <!-- 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> + <ng-container *ngIf="item.subtitle"> + <div class="fuse-vertical-navigation-item-subtitle"> + <span [ngClass]="item.classes?.subtitle"> + {{item.subtitle}} + </span> + </div> + </ng-container> + </div> + + <!-- Badge --> + <ng-container *ngIf="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-container> + + <!-- Arrow --> + <mat-icon + class="fuse-vertical-navigation-item-arrow icon-size-4" + [svgIcon]="'heroicons_solid:chevron-right'"></mat-icon> + + </div> + +</div> + +<div + class="fuse-vertical-navigation-item-children" + *ngIf="!isCollapsed" + @expandCollapse> + + <ng-container *ngFor="let item of item.children; trackBy: trackByFn"> + + <!-- Skip the hidden items --> + <ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden"> + + <!-- Basic --> + <ng-container *ngIf="item.type === 'basic'"> + <fuse-vertical-navigation-basic-item + [item]="item" + [name]="name"></fuse-vertical-navigation-basic-item> + </ng-container> + + <!-- Collapsable --> + <ng-container *ngIf="item.type === 'collapsable'"> + <fuse-vertical-navigation-collapsable-item + [item]="item" + [name]="name" + [autoCollapse]="autoCollapse"></fuse-vertical-navigation-collapsable-item> + </ng-container> + + <!-- Divider --> + <ng-container *ngIf="item.type === 'divider'"> + <fuse-vertical-navigation-divider-item + [item]="item" + [name]="name"></fuse-vertical-navigation-divider-item> + </ng-container> + + <!-- Group --> + <ng-container *ngIf="item.type === 'group'"> + <fuse-vertical-navigation-group-item + [item]="item" + [name]="name"></fuse-vertical-navigation-group-item> + </ng-container> + + <!-- Spacer --> + <ng-container *ngIf="item.type === 'spacer'"> + <fuse-vertical-navigation-spacer-item + [item]="item" + [name]="name"></fuse-vertical-navigation-spacer-item> + </ng-container> + + </ng-container> + + </ng-container> + +</div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/collapsable/collapsable.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/collapsable/collapsable.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..68d6c38be5c9861eaa0081b3f175cfb316105459 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/collapsable/collapsable.component.ts @@ -0,0 +1,345 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { BooleanInput } from '@angular/cdk/coercion'; +import { filter, Subject, takeUntil } from 'rxjs'; +import { fuseAnimations } from '@fuse/animations'; +import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component'; +import { FuseNavigationService } from '@fuse/components/navigation/navigation.service'; +import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types'; + +@Component({ + selector : 'fuse-vertical-navigation-collapsable-item', + templateUrl : './collapsable.component.html', + animations : fuseAnimations, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseVerticalNavigationCollapsableItemComponent implements OnInit, OnDestroy +{ + /* eslint-disable @typescript-eslint/naming-convention */ + static ngAcceptInputType_autoCollapse: BooleanInput; + /* eslint-enable @typescript-eslint/naming-convention */ + + @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>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _router: Router, + private _fuseNavigationService: FuseNavigationService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Host binding for component classes + */ + @HostBinding('class') get classList(): any + { + return { + 'fuse-vertical-navigation-item-collapsed': this.isCollapsed, + 'fuse-vertical-navigation-item-expanded' : this.isExpanded + }; + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/divider/divider.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/divider/divider.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f786705fa178f63422c855c879754d6548b93fb4 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/divider/divider.component.html @@ -0,0 +1,4 @@ +<!-- Divider --> +<div + class="fuse-vertical-navigation-item-wrapper divider" + [ngClass]="item.classes?.wrapper"></div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/divider/divider.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/divider/divider.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..15255b389ff4fef801b82599b39a585ba7dbf34d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/divider/divider.component.ts @@ -0,0 +1,61 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; +import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component'; +import { FuseNavigationService } from '@fuse/components/navigation/navigation.service'; +import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types'; + +@Component({ + selector : 'fuse-vertical-navigation-divider-item', + templateUrl : './divider.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseVerticalNavigationDividerItemComponent implements OnInit, OnDestroy +{ + @Input() item: FuseNavigationItem; + @Input() name: string; + + private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent; + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/group/group.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/group/group.component.html new file mode 100644 index 0000000000000000000000000000000000000000..49218739797bb2b5765a51a92d79f7aee5f15983 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/group/group.component.html @@ -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 --> + <ng-container *ngIf="item.icon"> + <mat-icon + class="fuse-vertical-navigation-item-icon" + [ngClass]="item.classes?.icon" + [svgIcon]="item.icon"></mat-icon> + </ng-container> + + <!-- 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> + <ng-container *ngIf="item.subtitle"> + <div class="fuse-vertical-navigation-item-subtitle"> + <span [ngClass]="item.classes?.subtitle"> + {{item.subtitle}} + </span> + </div> + </ng-container> + </div> + + <!-- Badge --> + <ng-container *ngIf="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-container> + + </div> + +</div> + +<ng-container *ngFor="let item of item.children; trackBy: trackByFn"> + + <!-- Skip the hidden items --> + <ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden"> + + <!-- Basic --> + <ng-container *ngIf="item.type === 'basic'"> + <fuse-vertical-navigation-basic-item + [item]="item" + [name]="name"></fuse-vertical-navigation-basic-item> + </ng-container> + + <!-- Collapsable --> + <ng-container *ngIf="item.type === 'collapsable'"> + <fuse-vertical-navigation-collapsable-item + [item]="item" + [name]="name" + [autoCollapse]="autoCollapse"></fuse-vertical-navigation-collapsable-item> + </ng-container> + + <!-- Divider --> + <ng-container *ngIf="item.type === 'divider'"> + <fuse-vertical-navigation-divider-item + [item]="item" + [name]="name"></fuse-vertical-navigation-divider-item> + </ng-container> + + <!-- Group --> + <ng-container *ngIf="item.type === 'group'"> + <fuse-vertical-navigation-group-item + [item]="item" + [name]="name"></fuse-vertical-navigation-group-item> + </ng-container> + + <!-- Spacer --> + <ng-container *ngIf="item.type === 'spacer'"> + <fuse-vertical-navigation-spacer-item + [item]="item" + [name]="name"></fuse-vertical-navigation-spacer-item> + </ng-container> + + </ng-container> + +</ng-container> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/group/group.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/group/group.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..290daeec60c8889e6649d6440a08864c338db899 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/group/group.component.ts @@ -0,0 +1,82 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { BooleanInput } from '@angular/cdk/coercion'; +import { Subject, takeUntil } from 'rxjs'; +import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component'; +import { FuseNavigationService } from '@fuse/components/navigation/navigation.service'; +import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types'; + +@Component({ + selector : 'fuse-vertical-navigation-group-item', + templateUrl : './group.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseVerticalNavigationGroupItemComponent implements OnInit, OnDestroy +{ + /* eslint-disable @typescript-eslint/naming-convention */ + static ngAcceptInputType_autoCollapse: BooleanInput; + /* eslint-enable @typescript-eslint/naming-convention */ + + @Input() autoCollapse: boolean; + @Input() item: FuseNavigationItem; + @Input() name: string; + + private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent; + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/spacer/spacer.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/spacer/spacer.component.html new file mode 100644 index 0000000000000000000000000000000000000000..719ed674c11494f93c1f186e811607daab5ec47d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/spacer/spacer.component.html @@ -0,0 +1,4 @@ +<!-- Spacer --> +<div + class="fuse-vertical-navigation-item-wrapper" + [ngClass]="item.classes?.wrapper"></div> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/spacer/spacer.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/spacer/spacer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..925bfc1cd0a748e9b64497fb0d57da08d81dfa54 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/components/spacer/spacer.component.ts @@ -0,0 +1,61 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; +import { FuseVerticalNavigationComponent } from '@fuse/components/navigation/vertical/vertical.component'; +import { FuseNavigationService } from '@fuse/components/navigation/navigation.service'; +import { FuseNavigationItem } from '@fuse/components/navigation/navigation.types'; + +@Component({ + selector : 'fuse-vertical-navigation-spacer-item', + templateUrl : './spacer.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FuseVerticalNavigationSpacerItemComponent implements OnInit, OnDestroy +{ + @Input() item: FuseNavigationItem; + @Input() name: string; + + private _fuseVerticalNavigationComponent: FuseVerticalNavigationComponent; + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _fuseNavigationService: FuseNavigationService + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/compact.scss b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/compact.scss new file mode 100644 index 0000000000000000000000000000000000000000..e91fbc6636d9c02364052552639b08e9ed6dd9d6 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/compact.scss @@ -0,0 +1,112 @@ +/* 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); + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/default.scss b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/default.scss new file mode 100644 index 0000000000000000000000000000000000000000..aa0d579247a049cf636e87e1c43e7f6a307ac916 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/default.scss @@ -0,0 +1,595 @@ +/* Variables */ +:root { + --fuse-vertical-navigation-width: 251px; +} + +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: 20px 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 { + margin-top: 6px; + + > *:last-child { + padding-bottom: 6px; + + > .fuse-vertical-navigation-item-children { + + > *:last-child { + padding-bottom: 0; + } + } + } + + .fuse-vertical-navigation-item { + padding: 20px 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 opacity-100 text-primary-600 dark:text-primary-400; + } + } + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/dense.scss b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/dense.scss new file mode 100644 index 0000000000000000000000000000000000000000..8bbebe249023bb7a25121277c6c36dd0f80dfd33 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/dense.scss @@ -0,0 +1,194 @@ +/* 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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/thin.scss b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/thin.scss new file mode 100644 index 0000000000000000000000000000000000000000..997bf254aa1c88db457f39f3bfe4ca694e57b63e --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/styles/appearances/thin.scss @@ -0,0 +1,99 @@ +/* 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); + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.html b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.html new file mode 100644 index 0000000000000000000000000000000000000000..ef0186e30004c66edb1c88323fb7e5214de43df4 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.html @@ -0,0 +1,122 @@ +<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 --> + <ng-container *ngFor="let item of navigation; trackBy: trackByFn"> + + <!-- Skip the hidden items --> + <ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden"> + + <!-- Aside --> + <ng-container *ngIf="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> + </ng-container> + + <!-- Basic --> + <ng-container *ngIf="item.type === 'basic'"> + <fuse-vertical-navigation-basic-item + [item]="item" + [name]="name"></fuse-vertical-navigation-basic-item> + </ng-container> + + <!-- Collapsable --> + <ng-container *ngIf="item.type === 'collapsable'"> + <fuse-vertical-navigation-collapsable-item + [item]="item" + [name]="name" + [autoCollapse]="autoCollapse"></fuse-vertical-navigation-collapsable-item> + </ng-container> + + <!-- Divider --> + <ng-container *ngIf="item.type === 'divider'"> + <fuse-vertical-navigation-divider-item + [item]="item" + [name]="name"></fuse-vertical-navigation-divider-item> + </ng-container> + + <!-- Group --> + <ng-container *ngIf="item.type === 'group'"> + <fuse-vertical-navigation-group-item + [item]="item" + [name]="name" + [autoCollapse]="autoCollapse"></fuse-vertical-navigation-group-item> + </ng-container> + + <!-- Spacer --> + <ng-container *ngIf="item.type === 'spacer'"> + <fuse-vertical-navigation-spacer-item + [item]="item" + [name]="name"></fuse-vertical-navigation-spacer-item> + </ng-container> + + </ng-container> + + </ng-container> + + <!-- 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 --> +<ng-container *ngIf="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 --> + <ng-container *ngFor="let item of navigation; trackBy: trackByFn"> + + <!-- Skip the hidden items --> + <ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden"> + + <!-- Aside --> + <ng-container *ngIf="item.type === 'aside' && item.id === activeAsideItemId"> + <fuse-vertical-navigation-aside-item + [item]="item" + [name]="name" + [autoCollapse]="autoCollapse"></fuse-vertical-navigation-aside-item> + </ng-container> + + </ng-container> + + </ng-container> + + </div> +</ng-container> diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.scss b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..8a50cef99a369de87dffed83ae48bdd55ba5ca69 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.scss @@ -0,0 +1,4 @@ +@import 'styles/appearances/default'; +@import 'styles/appearances/compact'; +@import 'styles/appearances/dense'; +@import 'styles/appearances/thin'; diff --git a/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.ts b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..33722098a91eb1f2cec98d6302795f0d382d23f6 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/components/navigation/vertical/vertical.component.ts @@ -0,0 +1,745 @@ +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnChanges, OnDestroy, OnInit, Output, QueryList, Renderer2, SimpleChanges, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core'; +import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations'; +import { NavigationEnd, Router } from '@angular/router'; +import { ScrollStrategy, ScrollStrategyOptions } from '@angular/cdk/overlay'; +import { delay, filter, merge, ReplaySubject, Subject, Subscription, takeUntil } from 'rxjs'; +import { fuseAnimations } from '@fuse/animations'; +import { FuseNavigationItem, FuseVerticalNavigationAppearance, FuseVerticalNavigationMode, FuseVerticalNavigationPosition } from '@fuse/components/navigation/navigation.types'; +import { FuseNavigationService } from '@fuse/components/navigation/navigation.service'; +import { FuseScrollbarDirective } from '@fuse/directives/scrollbar/scrollbar.directive'; +import { FuseUtilsService } from '@fuse/services/utils/utils.service'; +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector : 'fuse-vertical-navigation', + templateUrl : './vertical.component.html', + styleUrls : ['./vertical.component.scss'], + animations : fuseAnimations, + encapsulation : ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + exportAs : 'fuseVerticalNavigation' +}) +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 */ + + @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 _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( + private _animationBuilder: AnimationBuilder, + private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + private _renderer2: Renderer2, + private _router: Router, + private _scrollStrategyOptions: ScrollStrategyOptions, + private _fuseNavigationService: FuseNavigationService, + private _fuseUtilsService: FuseUtilsService + ) + { + this._handleAsideOverlayClick = (): void => { + this.closeAside(); + }; + this._handleOverlayClick = (): void => { + this.close(); + }; + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Host binding for component classes + */ + @HostBinding('class') get classList(): any + { + 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' + }; + } + + /** + * 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 + { + 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 + { + // 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); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/index.ts b/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1f563bc30caece4ee6f3bacc7419b902aa40020 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/index.ts @@ -0,0 +1 @@ +export * from '@fuse/directives/scroll-reset/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/public-api.ts b/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..c394b43c4babdee7513b8604e78c23c62b696d29 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/directives/scroll-reset/scroll-reset.directive'; +export * from '@fuse/directives/scroll-reset/scroll-reset.module'; diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/scroll-reset.directive.ts b/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/scroll-reset.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab8bae1ef0160f6b872b4a851fa6435a4f3c4c00 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/scroll-reset.directive.ts @@ -0,0 +1,52 @@ +import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { filter, Subject, takeUntil } from 'rxjs'; + +@Directive({ + selector: '[fuseScrollReset]', + exportAs: 'fuseScrollReset' +}) +export class FuseScrollResetDirective implements OnInit, OnDestroy +{ + private _unsubscribeAll: Subject<any> = new Subject<any>(); + + /** + * Constructor + */ + constructor( + private _elementRef: ElementRef, + private _router: Router + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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(); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/scroll-reset.module.ts b/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/scroll-reset.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..48715be34602ae9777b47bc1821f7d2c34bee279 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scroll-reset/scroll-reset.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FuseScrollResetDirective } from '@fuse/directives/scroll-reset/scroll-reset.directive'; + +@NgModule({ + declarations: [ + FuseScrollResetDirective + ], + exports : [ + FuseScrollResetDirective + ] +}) +export class FuseScrollResetModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/index.ts b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9aba5806f70d0c14ddff700b416b4a145896b183 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/index.ts @@ -0,0 +1 @@ +export * from '@fuse/directives/scrollbar/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/public-api.ts b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..c74ff507417cfcf6ad24d116f33a1557791dc994 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/directives/scrollbar/scrollbar.directive'; +export * from '@fuse/directives/scrollbar/scrollbar.module'; diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.directive.ts b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..03865f18a121bc6be4367d70466394fa347aa176 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.directive.ts @@ -0,0 +1,466 @@ +import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { Router } from '@angular/router'; +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Platform } from '@angular/cdk/platform'; +import { debounceTime, fromEvent, Subject, takeUntil } from 'rxjs'; +import PerfectScrollbar from 'perfect-scrollbar'; +import { merge } from 'lodash-es'; +import { ScrollbarGeometry, ScrollbarPosition } from '@fuse/directives/scrollbar/scrollbar.types'; + +/** + * Wrapper directive for the Perfect Scrollbar: https://github.com/mdbootstrap/perfect-scrollbar + */ +@Directive({ + selector: '[fuseScrollbar]', + exportAs: 'fuseScrollbar' +}) +export class FuseScrollbarDirective implements OnChanges, OnInit, OnDestroy +{ + /* eslint-disable @typescript-eslint/naming-convention */ + static ngAcceptInputType_fuseScrollbar: BooleanInput; + /* eslint-enable @typescript-eslint/naming-convention */ + + @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>(); + + /** + * Constructor + */ + constructor( + private _elementRef: ElementRef, + private _platform: Platform, + private _router: Router + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.module.ts b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..628645b68322f53292d69a97ccca495aad107a44 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FuseScrollbarDirective } from '@fuse/directives/scrollbar/scrollbar.directive'; + +@NgModule({ + declarations: [ + FuseScrollbarDirective + ], + exports : [ + FuseScrollbarDirective + ] +}) +export class FuseScrollbarModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.types.ts b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..11694a95182d4a79ff4599d505ca3fbd603c5e5f --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/directives/scrollbar/scrollbar.types.ts @@ -0,0 +1,28 @@ +export class ScrollbarGeometry +{ + public x: number; + public y: number; + + public w: number; + public h: number; + + constructor(x: number, y: number, w: number, h: number) + { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } +} + +export class ScrollbarPosition +{ + public x: number | 'start' | 'end'; + public y: number | 'start' | 'end'; + + constructor(x: number | 'start' | 'end', y: number | 'start' | 'end') + { + this.x = x; + this.y = y; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/fuse.module.ts b/transparency_dashboard_frontend/src/@fuse/fuse.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..5886f7a461f43a646ee6f6e4d961e2dc0886bd5c --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/fuse.module.ts @@ -0,0 +1,49 @@ +import { NgModule, Optional, SkipSelf } from '@angular/core'; +import { MATERIAL_SANITY_CHECKS } from '@angular/material/core'; +import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; +import { FuseConfirmationModule } from '@fuse/services/confirmation'; +import { FuseLoadingModule } from '@fuse/services/loading'; +import { FuseMediaWatcherModule } from '@fuse/services/media-watcher/media-watcher.module'; +import { FuseSplashScreenModule } from '@fuse/services/splash-screen/splash-screen.module'; +import { FuseUtilsModule } from '@fuse/services/utils/utils.module'; + +@NgModule({ + imports : [ + FuseConfirmationModule, + FuseLoadingModule, + FuseMediaWatcherModule, + FuseSplashScreenModule, + FuseUtilsModule + ], + providers: [ + { + // 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' + } + } + ] +}) +export class FuseModule +{ + /** + * Constructor + */ + constructor(@Optional() @SkipSelf() parentModule?: FuseModule) + { + if ( parentModule ) + { + throw new Error('FuseModule has already been loaded. Import this module in the AppModule only!'); + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/index.ts b/transparency_dashboard_frontend/src/@fuse/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b172fb6feab6c5b2300dcf5977a6d1fc90d9c7bd --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/index.ts @@ -0,0 +1 @@ +export * from './fuse.module'; diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/index.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..121e0f725edd307a7fafc633b38c99bb6a2a6e06 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/index.ts @@ -0,0 +1 @@ +export * from '@fuse/lib/mock-api/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.constants.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..de7440fd5dd69916ccb74eb6ddf8cdd4121b2ea4 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.constants.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const FUSE_MOCK_API_DEFAULT_DELAY = new InjectionToken<number>('FUSE_MOCK_API_DEFAULT_DELAY'); diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.interceptor.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.interceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..e94bdf1d235eeb8d10e169a412ffc7e25a901dad --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.interceptor.ts @@ -0,0 +1,96 @@ +import { Inject, Injectable } from '@angular/core'; +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'; +import { delay, Observable, of, switchMap, throwError } from 'rxjs'; +import { FUSE_MOCK_API_DEFAULT_DELAY } from '@fuse/lib/mock-api/mock-api.constants'; +import { FuseMockApiService } from '@fuse/lib/mock-api/mock-api.service'; + +@Injectable({ + providedIn: 'root' +}) +export class FuseMockApiInterceptor implements HttpInterceptor +{ + /** + * Constructor + */ + constructor( + @Inject(FUSE_MOCK_API_DEFAULT_DELAY) private _defaultDelay: number, + private _fuseMockApiService: FuseMockApiService + ) + { + } + + /** + * Intercept + * + * @param request + * @param next + */ + intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> + { + // Try to get the request handler + const { + handler, + urlParams + } = this._fuseMockApiService.findHandler(request.method.toUpperCase(), request.url); + + // Pass through if the request handler does not exist + if ( !handler ) + { + return next.handle(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 ?? this._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); + })); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.module.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff3b2c9380f9464b106f8e2eede8f2c692bdc9fd --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.module.ts @@ -0,0 +1,56 @@ +import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { FUSE_MOCK_API_DEFAULT_DELAY } from '@fuse/lib/mock-api/mock-api.constants'; +import { FuseMockApiInterceptor } from '@fuse/lib/mock-api/mock-api.interceptor'; +import { KeycloakService } from 'keycloak-angular'; + +@NgModule({ + providers: [ + { + provide : HTTP_INTERCEPTORS, + useClass: FuseMockApiInterceptor, + multi : true + } + ] +}) +export class FuseMockApiModule +{ + /** + * FuseMockApi module default configuration. + * + * @param mockApiServices - Array of services that register mock API handlers + * @param config - Configuration options + * @param config.delay - Default delay value in milliseconds to apply all responses + */ + static forRoot(mockApiServices: any[], config?: { delay?: number }): ModuleWithProviders<FuseMockApiModule> + { + return { + ngModule : FuseMockApiModule, + providers: [ + { + provide : APP_INITIALIZER, + deps : [...mockApiServices, KeycloakService], + useFactory: (keycloak: KeycloakService) => + keycloak.init({ + config: { + url: 'https://keycloak-security-dev.k8s.across-h2020.eu/auth', + realm: 'across-dev', + clientId: 'transparency-dashboard-frontend' + }, + initOptions: { + onLoad: 'login-required', + pkceMethod: 'S256', + enableLogging: true, + checkLoginIframe: false + }, + }), + multi : true + }, + { + provide : FUSE_MOCK_API_DEFAULT_DELAY, + useValue: config?.delay ?? 0 + } + ] + }; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.request-handler.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.request-handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..668c7b0856ffd3a2b626fff580396a4f94062b99 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.request-handler.ts @@ -0,0 +1,99 @@ +import { HttpRequest } from '@angular/common/http'; +import { Observable, of, take, throwError } from 'rxjs'; +import { FuseMockApiReplyCallback } from '@fuse/lib/mock-api/mock-api.types'; + +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; + } +} + + diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.service.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..30a065247cb1940311fd99cee12fe52baa204276 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.service.ts @@ -0,0 +1,202 @@ +import { Injectable } from '@angular/core'; +import { compact, fromPairs } from 'lodash-es'; +import { FuseMockApiHandler } from '@fuse/lib/mock-api/mock-api.request-handler'; +import { FuseMockApiMethods } from '@fuse/lib/mock-api/mock-api.types'; + +@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>() + }; + + /** + * Constructor + */ + constructor() + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.types.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..77236aa2d895c5e2a6f1ed55852175d3c73310b2 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.types.ts @@ -0,0 +1,16 @@ +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'; diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.utils.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..91af6a8f0f1b5d942ad6d05f2d9229c77d560af7 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/mock-api.utils.ts @@ -0,0 +1,37 @@ +export class FuseMockApiUtils +{ + /** + * Constructor + */ + constructor() + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ 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 */ + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/lib/mock-api/public-api.ts b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..77e2345616335c7dd13c3a2c42db09a9e8cfff9e --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/lib/mock-api/public-api.ts @@ -0,0 +1,5 @@ +export * from '@fuse/lib/mock-api/mock-api.constants'; +export * from '@fuse/lib/mock-api/mock-api.module'; +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'; diff --git a/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/find-by-key.module.ts b/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/find-by-key.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..ead347784d5b0fdb5f1c7934e6d9b399169224ca --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/find-by-key.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FuseFindByKeyPipe } from '@fuse/pipes/find-by-key/find-by-key.pipe'; + +@NgModule({ + declarations: [ + FuseFindByKeyPipe + ], + exports : [ + FuseFindByKeyPipe + ] +}) +export class FuseFindByKeyPipeModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/find-by-key.pipe.ts b/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/find-by-key.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef56d41064257835718b95d7becbccb7ac903a99 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/find-by-key.pipe.ts @@ -0,0 +1,37 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/** + * Finds an object from given source using the given key - value pairs + */ +@Pipe({ + name: 'fuseFindByKey', + pure: false +}) +export class FuseFindByKeyPipe implements PipeTransform +{ + /** + * Constructor + */ + constructor() + { + } + + /** + * Transform + * + * @param value A string or an array of strings to find from source + * @param key Key of the object property to look for + * @param source Array of objects to find from + */ + transform(value: string | string[], key: string, source: any[]): any + { + // If the given value is an array of strings... + if ( Array.isArray(value) ) + { + return value.map(item => source.find(sourceItem => sourceItem[key] === item)); + } + + // If the value is a string... + return source.find(sourceItem => sourceItem[key] === value); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/index.ts b/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..61efac020b9bfdc10045c32612fa3bba8ecfe31e --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/index.ts @@ -0,0 +1 @@ +export * from '@fuse/pipes/find-by-key/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/public-api.ts b/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..178a2c7fe569b71afb2ad3de259b860e9519c231 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/pipes/find-by-key/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/pipes/find-by-key/find-by-key.pipe'; +export * from '@fuse/pipes/find-by-key/find-by-key.module'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/config/config.constants.ts b/transparency_dashboard_frontend/src/@fuse/services/config/config.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef99d39c2edb2361618d40c4d59bbecdec507d5b --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/config/config.constants.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const FUSE_APP_CONFIG = new InjectionToken<any>('FUSE_APP_CONFIG'); diff --git a/transparency_dashboard_frontend/src/@fuse/services/config/config.module.ts b/transparency_dashboard_frontend/src/@fuse/services/config/config.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..4416a4ce9d02d8faa4e1b0efd3ef58e5838bdd72 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/config/config.module.ts @@ -0,0 +1,32 @@ +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { FuseConfigService } from '@fuse/services/config/config.service'; +import { FUSE_APP_CONFIG } from '@fuse/services/config/config.constants'; + +@NgModule() +export class FuseConfigModule +{ + /** + * Constructor + */ + constructor(private _fuseConfigService: FuseConfigService) + { + } + + /** + * forRoot method for setting user configuration + * + * @param config + */ + static forRoot(config: any): ModuleWithProviders<FuseConfigModule> + { + return { + ngModule : FuseConfigModule, + providers: [ + { + provide : FUSE_APP_CONFIG, + useValue: config + } + ] + }; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/config/config.service.ts b/transparency_dashboard_frontend/src/@fuse/services/config/config.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..33948a0f7c6abc92843616f557cf91f41f7da1d3 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/config/config.service.ts @@ -0,0 +1,55 @@ +import { Inject, Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { merge } from 'lodash-es'; +import { FUSE_APP_CONFIG } from '@fuse/services/config/config.constants'; + +@Injectable({ + providedIn: 'root' +}) +export class FuseConfigService +{ + private _config: BehaviorSubject<any>; + + /** + * Constructor + */ + constructor(@Inject(FUSE_APP_CONFIG) config: any) + { + // Private + this._config = new BehaviorSubject(config); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Setter & getter for config + */ + set config(value: any) + { + // Merge the new config over to the current config + const config = merge({}, this._config.getValue(), value); + + // Execute the observable + this._config.next(config); + } + + get config$(): Observable<any> + { + return this._config.asObservable(); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Resets the config to the default + */ + reset(): void + { + // Set the config + this._config.next(this.config); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/config/index.ts b/transparency_dashboard_frontend/src/@fuse/services/config/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0563cafef4ee8a510d8c2ff42f933bc2ff2887f9 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/config/index.ts @@ -0,0 +1 @@ +export * from '@fuse/services/config/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/config/public-api.ts b/transparency_dashboard_frontend/src/@fuse/services/config/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..36df7bdc4b5bbe2fe86e7ae5c24752b82c6d1c8c --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/config/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/services/config/config.module'; +export * from '@fuse/services/config/config.service'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.module.ts b/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce1432963d97aae4305b10dbf57ac0ad333cf1aa --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { FuseConfirmationService } from '@fuse/services/confirmation/confirmation.service'; +import { FuseConfirmationDialogComponent } from '@fuse/services/confirmation/dialog/dialog.component'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + declarations: [ + FuseConfirmationDialogComponent + ], + imports : [ + MatButtonModule, + MatDialogModule, + MatIconModule, + CommonModule + ], + providers : [ + FuseConfirmationService + ] +}) +export class FuseConfirmationModule +{ + /** + * Constructor + */ + constructor(private _fuseConfirmationService: FuseConfirmationService) + { + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.service.ts b/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b22ccae56189bb17f43759808ca957f9137cb86d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { merge } from 'lodash-es'; +import { FuseConfirmationDialogComponent } from '@fuse/services/confirmation/dialog/dialog.component'; +import { FuseConfirmationConfig } from '@fuse/services/confirmation/confirmation.types'; + +@Injectable() +export class FuseConfirmationService +{ + private _defaultConfig: FuseConfirmationConfig = { + title : 'Confirm action', + message : 'Are you sure you want to confirm this action?', + icon : { + show : true, + name : 'heroicons_outline:exclamation', + color: 'warn' + }, + actions : { + confirm: { + show : true, + label: 'Confirm', + color: 'warn' + }, + cancel : { + show : true, + label: 'Cancel' + } + }, + dismissible: false + }; + + /** + * Constructor + */ + constructor( + private _matDialog: MatDialog + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + open(config: FuseConfirmationConfig = {}): MatDialogRef<FuseConfirmationDialogComponent> + { + // Merge the user config with the default config + const userConfig = merge({}, this._defaultConfig, config); + + // Open the dialog + return this._matDialog.open(FuseConfirmationDialogComponent, { + autoFocus : false, + disableClose: !userConfig.dismissible, + data : userConfig, + panelClass : 'fuse-confirmation-dialog-panel' + }); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.types.ts b/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..53bd2dc54b64a03409b9f260874f84c922add3f6 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/confirmation/confirmation.types.ts @@ -0,0 +1,22 @@ +export interface FuseConfirmationConfig +{ + title?: string; + message?: string; + icon?: { + show?: boolean; + name?: string; + color?: 'primary' | 'accent' | 'warn' | 'basic' | 'info' | 'success' | 'warning' | 'error'; + }; + actions?: { + confirm?: { + show?: boolean; + label?: string; + color?: 'primary' | 'accent' | 'warn'; + }; + cancel?: { + show?: boolean; + label?: string; + }; + }; + dismissible?: boolean; +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/confirmation/dialog/dialog.component.html b/transparency_dashboard_frontend/src/@fuse/services/confirmation/dialog/dialog.component.html new file mode 100644 index 0000000000000000000000000000000000000000..1d15451d8ec9492d668a10de9caad29a584460f1 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/confirmation/dialog/dialog.component.html @@ -0,0 +1,85 @@ +<div class="relative flex flex-col w-full h-full"> + + <!-- Dismiss button --> + <ng-container *ngIf="data.dismissible"> + <div class="absolute top-0 right-0 pt-4 pr-4"> + <button + mat-icon-button + [matDialogClose]="undefined"> + <mat-icon + class="text-secondary" + [svgIcon]="'heroicons_outline:x'"></mat-icon> + </button> + </div> + </ng-container> + + <!-- Content --> + <div class="flex flex-col sm:flex-row flex-auto items-center sm:items-start p-8 pb-6 sm:pb-8"> + + <!-- Icon --> + <ng-container *ngIf="data.icon.show"> + <div + class="flex flex-0 items-center justify-center w-10 h-10 sm:mr-4 rounded-full" + [ngClass]="{'text-primary-600 bg-primary-100 dark:text-primary-50 dark:bg-primary-600': data.icon.color === 'primary', + 'text-accent-600 bg-accent-100 dark:text-accent-50 dark:bg-accent-600': data.icon.color === 'accent', + 'text-warn-600 bg-warn-100 dark:text-warn-50 dark:bg-warn-600': data.icon.color === 'warn', + 'text-gray-600 bg-gray-100 dark:text-gray-50 dark:bg-gray-600': data.icon.color === 'basic', + 'text-blue-600 bg-blue-100 dark:text-blue-50 dark:bg-blue-600': data.icon.color === 'info', + 'text-green-500 bg-green-100 dark:text-green-50 dark:bg-green-500': data.icon.color === 'success', + 'text-amber-500 bg-amber-100 dark:text-amber-50 dark:bg-amber-500': data.icon.color === 'warning', + 'text-red-600 bg-red-100 dark:text-red-50 dark:bg-red-600': data.icon.color === 'error' + }"> + <mat-icon + class="text-current" + [svgIcon]="data.icon.name"></mat-icon> + </div> + </ng-container> + + <ng-container *ngIf="data.title || data.message"> + <div class="flex flex-col items-center sm:items-start mt-4 sm:mt-0 sm:pr-8 space-y-1 text-center sm:text-left"> + + <!-- Title --> + <ng-container *ngIf="data.title"> + <div + class="text-xl leading-6 font-medium" + [innerHTML]="data.title"></div> + </ng-container> + + <!-- Message --> + <ng-container *ngIf="data.message"> + <div + class="text-secondary" + [innerHTML]="data.message"></div> + </ng-container> + </div> + </ng-container> + + </div> + + <!-- Actions --> + <ng-container *ngIf="data.actions.confirm.show || data.actions.cancel.show"> + <div class="flex items-center justify-center sm:justify-end px-6 py-4 space-x-3 bg-gray-50 dark:bg-black dark:bg-opacity-10"> + + <!-- Cancel --> + <ng-container *ngIf="data.actions.cancel.show"> + <button + mat-stroked-button + [matDialogClose]="'cancelled'"> + {{data.actions.cancel.label}} + </button> + </ng-container> + + <!-- Confirm --> + <ng-container *ngIf="data.actions.confirm.show"> + <button + mat-flat-button + [color]="data.actions.confirm.color" + [matDialogClose]="'confirmed'"> + {{data.actions.confirm.label}} + </button> + </ng-container> + + </div> + </ng-container> + +</div> diff --git a/transparency_dashboard_frontend/src/@fuse/services/confirmation/dialog/dialog.component.ts b/transparency_dashboard_frontend/src/@fuse/services/confirmation/dialog/dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4dd92d3ffab8e1b127446cd49ef70adb25b4110f --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/confirmation/dialog/dialog.component.ts @@ -0,0 +1,52 @@ +import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FuseConfirmationConfig } from '@fuse/services/confirmation/confirmation.types'; + +@Component({ + selector : 'fuse-confirmation-dialog', + templateUrl : './dialog.component.html', + styles : [ + /* language=SCSS */ + ` + .fuse-confirmation-dialog-panel { + @screen md { + @apply w-128; + } + + .mat-dialog-container { + padding: 0 !important; + } + } + ` + ], + encapsulation: ViewEncapsulation.None +}) +export class FuseConfirmationDialogComponent implements OnInit +{ + /** + * Constructor + */ + constructor( + @Inject(MAT_DIALOG_DATA) public data: FuseConfirmationConfig, + public matDialogRef: MatDialogRef<FuseConfirmationDialogComponent> + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Lifecycle hooks + // ----------------------------------------------------------------------------------------------------- + + /** + * On init + */ + ngOnInit(): void + { + + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/confirmation/index.ts b/transparency_dashboard_frontend/src/@fuse/services/confirmation/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6f2fee1bd233a4f33dc3c88ee56aa8e50a752dc --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/confirmation/index.ts @@ -0,0 +1 @@ +export * from '@fuse/services/confirmation/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/confirmation/public-api.ts b/transparency_dashboard_frontend/src/@fuse/services/confirmation/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..815db9fd5f150a99c517e33deff57843fd860354 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/confirmation/public-api.ts @@ -0,0 +1,3 @@ +export * from '@fuse/services/confirmation/confirmation.module'; +export * from '@fuse/services/confirmation/confirmation.service'; +export * from '@fuse/services/confirmation/confirmation.types'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/loading/index.ts b/transparency_dashboard_frontend/src/@fuse/services/loading/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..deaac8da9d736a5b2be9b185a82816ba54e6035b --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/loading/index.ts @@ -0,0 +1 @@ +export * from '@fuse/services/loading/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/loading/loading.interceptor.ts b/transparency_dashboard_frontend/src/@fuse/services/loading/loading.interceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..886243f2db2c8afc99298bfecc0e2ae8e9121dd8 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/loading/loading.interceptor.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { finalize, Observable } from 'rxjs'; +import { FuseLoadingService } from '@fuse/services/loading/loading.service'; + +@Injectable() +export class FuseLoadingInterceptor implements HttpInterceptor +{ + handleRequestsAutomatically: boolean; + + /** + * Constructor + */ + constructor( + private _fuseLoadingService: FuseLoadingService + ) + { + // Subscribe to the auto + this._fuseLoadingService.auto$ + .subscribe((value) => { + this.handleRequestsAutomatically = value; + }); + } + + /** + * Intercept + * + * @param req + * @param next + */ + intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> + { + // If the Auto mode is turned off, do nothing + if ( !this.handleRequestsAutomatically ) + { + return next.handle(req); + } + + // Set the loading status to true + this._fuseLoadingService._setLoadingStatus(true, req.url); + + return next.handle(req).pipe( + finalize(() => { + // Set the status to false if there are any errors or the request is completed + this._fuseLoadingService._setLoadingStatus(false, req.url); + })); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/loading/loading.module.ts b/transparency_dashboard_frontend/src/@fuse/services/loading/loading.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..778ddfd305b73d982f8d576a24198278935b771a --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/loading/loading.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { FuseLoadingInterceptor } from '@fuse/services/loading/loading.interceptor'; + +@NgModule({ + providers: [ + { + provide : HTTP_INTERCEPTORS, + useClass: FuseLoadingInterceptor, + multi : true + } + ] +}) +export class FuseLoadingModule +{ +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/loading/loading.service.ts b/transparency_dashboard_frontend/src/@fuse/services/loading/loading.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..84049f224f12ba095f42dfd6f8c0815c40f4e32d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/loading/loading.service.ts @@ -0,0 +1,146 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class FuseLoadingService +{ + private _auto$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); + private _mode$: BehaviorSubject<'determinate' | 'indeterminate'> = new BehaviorSubject<'determinate' | 'indeterminate'>('indeterminate'); + private _progress$: BehaviorSubject<number | null> = new BehaviorSubject<number | null>(0); + private _show$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); + private _urlMap: Map<string, boolean> = new Map<string, boolean>(); + + /** + * Constructor + */ + constructor(private _httpClient: HttpClient) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Getter for auto mode + */ + get auto$(): Observable<boolean> + { + return this._auto$.asObservable(); + } + + /** + * Getter for mode + */ + get mode$(): Observable<'determinate' | 'indeterminate'> + { + return this._mode$.asObservable(); + } + + /** + * Getter for progress + */ + get progress$(): Observable<number> + { + return this._progress$.asObservable(); + } + + /** + * Getter for show + */ + get show$(): Observable<boolean> + { + return this._show$.asObservable(); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Show the loading bar + */ + show(): void + { + this._show$.next(true); + } + + /** + * Hide the loading bar + */ + hide(): void + { + this._show$.next(false); + } + + /** + * Set the auto mode + * + * @param value + */ + setAutoMode(value: boolean): void + { + this._auto$.next(value); + } + + /** + * Set the mode + * + * @param value + */ + setMode(value: 'determinate' | 'indeterminate'): void + { + this._mode$.next(value); + } + + /** + * Set the progress of the bar manually + * + * @param value + */ + setProgress(value: number): void + { + if ( value < 0 || value > 100 ) + { + console.error('Progress value must be between 0 and 100!'); + return; + } + + this._progress$.next(value); + } + + /** + * Sets the loading status on the given url + * + * @param status + * @param url + */ + _setLoadingStatus(status: boolean, url: string): void + { + // Return if the url was not provided + if ( !url ) + { + console.error('The request URL must be provided!'); + return; + } + + if ( status === true ) + { + this._urlMap.set(url, status); + this._show$.next(true); + } + else if ( status === false && this._urlMap.has(url) ) + { + this._urlMap.delete(url); + } + + // Only set the status to 'false' if all outgoing requests are completed + if ( this._urlMap.size === 0 ) + { + this._show$.next(false); + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/loading/public-api.ts b/transparency_dashboard_frontend/src/@fuse/services/loading/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..c52faedd695f98d313cf6f411e1685081751c570 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/loading/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/services/loading/loading.service'; +export * from '@fuse/services/loading/loading.module'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/media-watcher/index.ts b/transparency_dashboard_frontend/src/@fuse/services/media-watcher/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2dad0cbebe706f95cced17473dc8c9407c3e483 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/media-watcher/index.ts @@ -0,0 +1 @@ +export * from '@fuse/services/media-watcher/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/media-watcher/media-watcher.module.ts b/transparency_dashboard_frontend/src/@fuse/services/media-watcher/media-watcher.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e5133e0e05b8d92d9e4e6ebf7018570a43a9417 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/media-watcher/media-watcher.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { FuseMediaWatcherService } from '@fuse/services/media-watcher/media-watcher.service'; + +@NgModule({ + providers: [ + FuseMediaWatcherService + ] +}) +export class FuseMediaWatcherModule +{ + /** + * Constructor + */ + constructor(private _fuseMediaWatcherService: FuseMediaWatcherService) + { + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/media-watcher/media-watcher.service.ts b/transparency_dashboard_frontend/src/@fuse/services/media-watcher/media-watcher.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..2066f065b800208360bcba63ba32f1d8781de4d6 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/media-watcher/media-watcher.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { map, Observable, ReplaySubject, switchMap } from 'rxjs'; +import { fromPairs } from 'lodash-es'; +import { FuseConfigService } from '@fuse/services/config'; + +@Injectable() +export class FuseMediaWatcherService +{ + private _onMediaChange: ReplaySubject<{ matchingAliases: string[]; matchingQueries: any }> = new ReplaySubject<{ matchingAliases: string[]; matchingQueries: any }>(1); + + /** + * Constructor + */ + constructor( + private _breakpointObserver: BreakpointObserver, + private _fuseConfigService: FuseConfigService + ) + { + this._fuseConfigService.config$.pipe( + map(config => fromPairs(Object.entries(config.screens).map(([alias, screen]) => ([alias, `(min-width: ${screen})`])))), + switchMap(screens => this._breakpointObserver.observe(Object.values(screens)).pipe( + map((state) => { + + // Prepare the observable values and set their defaults + const matchingAliases: string[] = []; + const matchingQueries: any = {}; + + // Get the matching breakpoints and use them to fill the subject + const matchingBreakpoints = Object.entries(state.breakpoints).filter(([query, matches]) => matches) ?? []; + for ( const [query] of matchingBreakpoints ) + { + // Find the alias of the matching query + const matchingAlias = Object.entries(screens).find(([alias, q]) => q === query)[0]; + + // Add the matching query to the observable values + if ( matchingAlias ) + { + matchingAliases.push(matchingAlias); + matchingQueries[matchingAlias] = query; + } + } + + // Execute the observable + this._onMediaChange.next({ + matchingAliases, + matchingQueries + }); + }) + )) + ).subscribe(); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Getter for _onMediaChange + */ + get onMediaChange$(): Observable<{ matchingAliases: string[]; matchingQueries: any }> + { + return this._onMediaChange.asObservable(); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * On media query change + * + * @param query + */ + onMediaQueryChange$(query: string | string[]): Observable<BreakpointState> + { + return this._breakpointObserver.observe(query); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/media-watcher/public-api.ts b/transparency_dashboard_frontend/src/@fuse/services/media-watcher/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd5905d7355c3890085a4616b3640934edafc2e9 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/media-watcher/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/services/media-watcher/media-watcher.module'; +export * from '@fuse/services/media-watcher/media-watcher.service'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/splash-screen/index.ts b/transparency_dashboard_frontend/src/@fuse/services/splash-screen/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..79ce6e491c8b5b2119aca88d45f3c5c8304e8d2a --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/splash-screen/index.ts @@ -0,0 +1 @@ +export * from '@fuse/services/splash-screen/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/splash-screen/public-api.ts b/transparency_dashboard_frontend/src/@fuse/services/splash-screen/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab0ada99e2e4ac6245b87579c6829e6d4a360d43 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/splash-screen/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/services/splash-screen/splash-screen.module'; +export * from '@fuse/services/splash-screen/splash-screen.service'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/splash-screen/splash-screen.module.ts b/transparency_dashboard_frontend/src/@fuse/services/splash-screen/splash-screen.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..913d595da5ceed0a5d6811aec18fa6d8039153cd --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/splash-screen/splash-screen.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { FuseSplashScreenService } from '@fuse/services/splash-screen/splash-screen.service'; + +@NgModule({ + providers: [ + FuseSplashScreenService + ] +}) +export class FuseSplashScreenModule +{ + /** + * Constructor + */ + constructor(private _fuseSplashScreenService: FuseSplashScreenService) + { + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/splash-screen/splash-screen.service.ts b/transparency_dashboard_frontend/src/@fuse/services/splash-screen/splash-screen.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..5743f4c56e14626c691b4ea75a89c6f2a6203736 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/splash-screen/splash-screen.service.ts @@ -0,0 +1,47 @@ +import { Inject, Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { NavigationEnd, Router } from '@angular/router'; +import { filter, take } from 'rxjs'; + +@Injectable() +export class FuseSplashScreenService +{ + /** + * Constructor + */ + constructor( + @Inject(DOCUMENT) private _document: any, + private _router: Router + ) + { + // Hide it on the first NavigationEnd event + this._router.events + .pipe( + filter(event => event instanceof NavigationEnd), + take(1) + ) + .subscribe(() => { + this.hide(); + }); + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Show the splash screen + */ + show(): void + { + this._document.body.classList.remove('fuse-splash-screen-hidden'); + } + + /** + * Hide the splash screen + */ + hide(): void + { + this._document.body.classList.add('fuse-splash-screen-hidden'); + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/utils/index.ts b/transparency_dashboard_frontend/src/@fuse/services/utils/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a507e7ffa073545faba203dc13f7d6b3fc8c7c5d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/utils/index.ts @@ -0,0 +1 @@ +export * from '@fuse/services/utils/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/utils/public-api.ts b/transparency_dashboard_frontend/src/@fuse/services/utils/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d2a55bd65d4c4e43cc7f575a90950f5458379f7 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/utils/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/services/utils/utils.module'; +export * from '@fuse/services/utils/utils.service'; diff --git a/transparency_dashboard_frontend/src/@fuse/services/utils/utils.module.ts b/transparency_dashboard_frontend/src/@fuse/services/utils/utils.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f974f339011f622bbebd5def8016221cf02355e --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/utils/utils.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { FuseUtilsService } from '@fuse/services/utils/utils.service'; + +@NgModule({ + providers: [ + FuseUtilsService + ] +}) +export class FuseUtilsModule +{ + /** + * Constructor + */ + constructor(private _fuseUtilsService: FuseUtilsService) + { + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/services/utils/utils.service.ts b/transparency_dashboard_frontend/src/@fuse/services/utils/utils.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c307ee655f3089bacb35b8faa17544dff29e312 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/services/utils/utils.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; +import { IsActiveMatchOptions } from '@angular/router'; + +@Injectable({ + providedIn: 'root' +}) +export class FuseUtilsService +{ + /** + * Constructor + */ + constructor() + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Accessors + // ----------------------------------------------------------------------------------------------------- + + /** + * Get the equivalent "IsActiveMatchOptions" options for "exact = true". + */ + get exactMatchOptions(): IsActiveMatchOptions + { + return { + paths : 'exact', + fragment : 'ignored', + matrixParams: 'ignored', + queryParams : 'exact' + }; + } + + /** + * Get the equivalent "IsActiveMatchOptions" options for "exact = false". + */ + get subsetMatchOptions(): IsActiveMatchOptions + { + return { + paths : 'subset', + fragment : 'ignored', + matrixParams: 'ignored', + queryParams : 'subset' + }; + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + /** + * Generates a random id + * + * @param length + */ + randomId(length: number = 10): string + { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let name = ''; + + for ( let i = 0; i < 10; i++ ) + { + name += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return name; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/styles/components/example-viewer.scss b/transparency_dashboard_frontend/src/@fuse/styles/components/example-viewer.scss new file mode 100644 index 0000000000000000000000000000000000000000..fe0fd30370f5eab54221aa824c1fa22570e1cdc2 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/components/example-viewer.scss @@ -0,0 +1,47 @@ +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Example viewer +/* ----------------------------------------------------------------------------------------------------- */ +.example-viewer { + display: flex; + flex-direction: column; + margin: 32px 0; + overflow: hidden; + @apply rounded-2xl shadow bg-card; + + .title { + display: flex; + align-items: center; + justify-content: space-between; + height: 88px; + min-height: 88px; + max-height: 88px; + padding: 0 40px; + + h6 { + font-weight: 700; + } + + .controls { + display: flex; + align-items: center; + + > * + * { + margin-left: 8px; + } + } + } + + mat-tab-group { + + .mat-tab-body-content { + + .fuse-highlight { + + pre { + margin: 0; + border-radius: 0; + } + } + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/styles/components/input.scss b/transparency_dashboard_frontend/src/@fuse/styles/components/input.scss new file mode 100644 index 0000000000000000000000000000000000000000..1ae31b841e04d44400636cdb6dc25b31eec8fa9d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/components/input.scss @@ -0,0 +1,41 @@ +input, +textarea { + background: transparent; + + /* Placeholder color */ + &::placeholder { + @apply text-hint; + } + + &::-moz-placeholder { + @apply text-hint; + } + + &::-webkit-input-placeholder { + @apply text-hint; + } + + &:-ms-input-placeholder { + @apply text-hint; + } + + &:-webkit-autofill { + -webkit-transition: 'background-color 9999s ease-out'; + -webkit-transition-delay: 9999s; + } + + &:-webkit-autofill:hover { + -webkit-transition: 'background-color 9999s ease-out'; + -webkit-transition-delay: 9999s; + } + + &:-webkit-autofill:focus { + -webkit-transition: 'background-color 9999s ease-out'; + -webkit-transition-delay: 9999s; + } + + &:-webkit-autofill:active { + -webkit-transition: 'background-color 9999s ease-out'; + -webkit-transition-delay: 9999s; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/styles/main.scss b/transparency_dashboard_frontend/src/@fuse/styles/main.scss new file mode 100644 index 0000000000000000000000000000000000000000..e964bfe967b8d61e1a0723a7a37cfde4fbc72b50 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/main.scss @@ -0,0 +1,9 @@ +/* 1. Components */ +@import 'components/example-viewer'; +@import 'components/input'; + +/* 2. Overrides */ +@import 'overrides/angular-material'; +@import 'overrides/highlightjs'; +@import 'overrides/perfect-scrollbar'; +@import 'overrides/quill'; diff --git a/transparency_dashboard_frontend/src/@fuse/styles/overrides/angular-material.scss b/transparency_dashboard_frontend/src/@fuse/styles/overrides/angular-material.scss new file mode 100644 index 0000000000000000000000000000000000000000..7d2b6a80394a95ee27cbd2519d4dc83fb9d5d35d --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/overrides/angular-material.scss @@ -0,0 +1,1383 @@ +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Angular Material CDK helpers & overrides +/* ----------------------------------------------------------------------------------------------------- */ + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Overlay +/* ----------------------------------------------------------------------------------------------------- */ +.fuse-backdrop-on-mobile { + @apply bg-black bg-opacity-60 sm:bg-transparent #{'!important'}; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Angular Material helpers & overrides +/* ----------------------------------------------------------------------------------------------------- */ + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Accordion +/* ----------------------------------------------------------------------------------------------------- */ +.mat-accordion { + .mat-expansion-panel { + margin-bottom: 24px; + border-radius: 8px !important; + transition: box-shadow 225ms cubic-bezier(0.4, 0, 0.2, 1); + @apply shadow #{'!important'}; + + &:last-child { + margin-bottom: 0; + } + + &.mat-expanded, + &:hover { + @apply shadow-lg #{'!important'}; + } + + &:not(.mat-expanded) { + .mat-expansion-panel-header { + &:not([aria-disabled="true"]) { + &.cdk-keyboard-focused, + &.cdk-program-focused, + &:hover { + background: transparent !important; + } + } + } + } + + .mat-expansion-panel-header { + font-size: 14px; + + &[aria-disabled="true"] { + .mat-expansion-panel-header-description { + margin-right: 28px; + } + } + + .mat-expansion-indicator { + display: inline-flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + + /* Do not override the border color of the expansion panel indicator */ + &:after { + border-color: currentColor !important; + } + } + } + + .mat-expansion-panel-body { + line-height: 1.7; + @apply text-secondary #{'!important'}; + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Buttons +/* ----------------------------------------------------------------------------------------------------- */ +.mat-button, +.mat-fab, +.mat-flat-button, +.mat-icon-button, +.mat-mini-fab, +.mat-raised-button, +.mat-stroked-button { + display: inline-flex !important; + align-items: center; + justify-content: center; + height: 40px; + min-height: 40px; + max-height: 40px; + line-height: 1 !important; + + .mat-button-wrapper { + position: relative; + display: inline-flex !important; + align-items: center; + justify-content: center; + height: 100%; + z-index: 2; /* Move mat-button-wrapper above the ripple and focus overlay */ + } + + .mat-button-focus-overlay, + .mat-button-ripple { + z-index: 1; + } + + /* Large button */ + &.fuse-mat-button-large { + height: 48px; + min-height: 48px; + max-height: 48px; + } + + /* Lower the icon opacity on disabled buttons */ + &.mat-button-disabled { + .mat-icon { + opacity: 0.38 !important; + } + } +} + +.mat-fab { + max-height: 56px; +} + +/* Rounded design */ +.mat-button, +.mat-flat-button, +.mat-raised-button, +.mat-stroked-button { + padding: 0 20px !important; + border-radius: 9999px !important; +} + +/* Target all buttons */ +.mat-button, +.mat-fab, +.mat-flat-button, +.mat-icon-button, +.mat-fab, +.mat-mini-fab, +.mat-raised-button, +.mat-stroked-button { + /* mat-progress-spinner inside buttons */ + .mat-progress-spinner { + &.mat-progress-spinner-indeterminate-animation[mode="indeterminate"] { + circle { + stroke: currentColor !important; + animation-duration: 6000ms; + } + } + } +} + +/* Colored background buttons */ +.mat-flat-button, +.mat-raised-button, +.mat-fab, +.mat-mini-fab { + .mat-icon { + color: currentColor !important; + } + + /* Add hover and focus style on all buttons */ + .mat-button-focus-overlay { + @apply bg-gray-400 bg-opacity-20 dark:bg-black dark:bg-opacity-5 #{'!important'}; + } + + /* On palette colored buttons, use a darker color */ + &.mat-primary, + &.mat-accent, + &.mat-warn { + .mat-button-focus-overlay { + background-color: rgba(0, 0, 0, 0.1) !important; + } + } + + &:hover, + &.cdk-keyboard-focused, + &.cdk-program-focused { + .mat-button-focus-overlay { + opacity: 1 !important; + } + } + + @media (hover: none) { + &:hover { + .mat-button-focus-overlay { + opacity: 0 !important; + } + } + } + + &.mat-button-disabled { + .mat-button-focus-overlay { + opacity: 0 !important; + } + } +} + +/* Transparent background buttons */ +.mat-button, +.mat-icon-button, +.mat-stroked-button { + /* Apply primary color */ + &.mat-primary:not(.mat-button-disabled) { + .mat-icon { + @apply text-primary #{'!important'}; + } + } + + /* Apply accent color */ + &.mat-accent:not(.mat-button-disabled) { + .mat-icon { + @apply text-accent #{'!important'}; + } + } + + /* Apply warn color */ + &.mat-warn:not(.mat-button-disabled) { + .mat-icon { + @apply text-warn #{'!important'}; + } + } + + /* Add hover and focus styles */ + .mat-button-focus-overlay { + @apply bg-gray-400 bg-opacity-20 dark:bg-black dark:bg-opacity-5 #{'!important'}; + } + + /* On primary colored buttons, use the primary color as focus overlay */ + &.mat-primary:not(.mat-button-disabled) { + .mat-button-focus-overlay { + @apply bg-primary #{'!important'}; + } + } + + /* On accent colored buttons, use the accent color as focus overlay */ + &.mat-accent:not(.mat-button-disabled) { + .mat-button-focus-overlay { + @apply bg-accent #{'!important'}; + } + } + + /* On warn colored buttons, use the warn color as focus overlay */ + &.mat-warn:not(.mat-button-disabled) { + .mat-button-focus-overlay { + @apply bg-warn #{'!important'}; + } + } + + &.mat-primary:not(.mat-button-disabled), + &.mat-accent:not(.mat-button-disabled), + &.mat-warn:not(.mat-button-disabled) { + &:hover, + &.cdk-keyboard-focused, + &.cdk-program-focused { + .mat-button-focus-overlay { + opacity: 0.1 !important; + } + } + } + + &:hover, + &.cdk-keyboard-focused, + &.cdk-program-focused { + .mat-button-focus-overlay { + opacity: 1 !important; + } + } + + @media (hover: none) { + &:hover { + .mat-button-focus-overlay { + opacity: 0 !important; + } + } + } + + &.mat-button-disabled { + .mat-button-focus-overlay { + opacity: 0 !important; + } + } +} + +/* Stroked buttons */ +.mat-stroked-button { + /* Border color */ + &:not(.mat-button-disabled) { + @apply border-gray-300 dark:border-gray-500 #{'!important'}; + } + + &.mat-button-disabled { + @apply border-gray-200 dark:border-gray-600 #{'!important'}; + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Button Toggle +/* ----------------------------------------------------------------------------------------------------- */ +.mat-button-toggle-group { + border: none !important; + @apply space-x-1; + + &.mat-button-toggle-group-appearance-standard { + .mat-button-toggle + .mat-button-toggle { + background-clip: padding-box; + } + } + + .mat-button-toggle { + border-radius: 9999px; + overflow: hidden; + border: none !important; + font-weight: 500; + + &.mat-button-toggle-checked { + .mat-button-toggle-label-content { + @apply text-default #{'!important'}; + } + } + + .mat-button-toggle-label-content { + padding: 0 20px; + @apply text-secondary; + } + + .mat-ripple { + border-radius: 9999px; + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Checkbox +/* ----------------------------------------------------------------------------------------------------- */ +.mat-checkbox { + display: inline-flex; + + /* Allow multiline text */ + .mat-checkbox-layout { + white-space: normal; + + .mat-checkbox-inner-container { + display: inline-flex; + align-items: center; + margin: 0 8px 0 0; + + /* Add a zero-width space character to trick the container */ + /* into being the same height as a single line of the label */ + &:after { + content: "\200b"; + } + } + + .mat-checkbox-label { + line-height: inherit; + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Chip +/* ----------------------------------------------------------------------------------------------------- */ +.mat-chip { + font-weight: 500 !important; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Dialog +/* ----------------------------------------------------------------------------------------------------- */ +.mat-dialog-container { + border-radius: 16px !important; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Drawer +/* ----------------------------------------------------------------------------------------------------- */ +.mat-drawer-backdrop.mat-drawer-shown { + background-color: rgba(0, 0, 0, 0.6) !important; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Form fields +/* ----------------------------------------------------------------------------------------------------- */ + +/* Fuse only uses 'fill' style form fields and therefore */ +/* only provides fixes and tweaks for that style */ +.mat-form-field.mat-form-field-appearance-fill { + /* Disabled */ + &.mat-form-field-disabled { + opacity: 0.7 !important; + } + + /* Invalid */ + &.mat-form-field-invalid { + .mat-form-field-wrapper { + /* Border color */ + .mat-form-field-flex { + @apply border-warn dark:border-warn #{'!important'}; + } + } + } + + /* Focused */ + &.mat-focused { + .mat-form-field-wrapper { + /* Background color */ + .mat-form-field-flex { + @apply bg-card dark:bg-card #{'!important'}; + } + } + } + + /* Focused and valid fields */ + &.mat-focused:not(.mat-form-field-invalid) { + .mat-form-field-wrapper { + /* Border color */ + .mat-form-field-flex { + @apply border-primary dark:border-primary #{'!important'}; + } + } + } + + /* Disable floating mat-label */ + &.mat-form-field-has-label.mat-form-field-can-float.mat-form-field-should-float { + .mat-form-field-label-wrapper { + .mat-form-field-label { + width: 100% !important; + transform: none !important; + } + } + } + + /* Remove the default arrow for native select */ + &.mat-form-field-type-mat-native-select { + .mat-form-field-infix { + select { + top: auto; + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-right: 18px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%2364748B' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5H7z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right -7px center; + background-size: 24px; + + .dark & { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%2397a6ba' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5H7z'/%3E%3C/svg%3E"); + } + } + + &:after { + display: none; + } + } + } + + /* Adjustments for mat-label */ + &.mat-form-field-has-label { + .mat-form-field-wrapper { + margin-top: 24px; + } + } + + /* Default style tweaks and enhancements */ + .mat-form-field-wrapper { + margin-bottom: 16px; + padding-bottom: 0; + + .mat-form-field-flex { + position: relative; + display: flex; + align-items: stretch; + min-height: 48px; + border-radius: 6px; + padding: 0 16px; + border-width: 1px; + @apply shadow-sm bg-white border-gray-300 dark:bg-black dark:bg-opacity-5 dark:border-gray-500 #{'!important'}; + + .mat-form-field-prefix { + > .mat-icon { + margin-right: 12px; + } + + > .mat-icon-button { + margin: 0 4px 0 -10px; + } + + > .mat-select { + margin-right: 10px; + } + + > .mat-datepicker-toggle { + margin-left: -8px; + } + + > *:not(.mat-icon):not(.mat-icon-button):not(.mat-select):not( + .mat-datepicker-toggle + ) { + margin-right: 12px; + } + } + + .mat-form-field-suffix { + > .mat-icon { + margin-left: 12px; + } + + > .mat-icon-button { + margin: 0 -10px 0 4px; + } + + > .mat-select { + margin-left: 10px; + } + + > .mat-datepicker-toggle { + margin-right: -8px; + } + } + + .mat-form-field-prefix, + .mat-form-field-suffix { + display: inline-flex; + align-items: center; + justify-content: center; + @apply text-hint #{'!important'}; + + .mat-icon-button { + width: 40px; + min-width: 40px; + height: 40px; + min-height: 40px; + } + + .mat-icon, + .mat-icon-button:not(.mat-button-disabled), + .mat-select-value { + @apply text-hint; + } + + /* Remove the margins from the mat-icon if it's inside a button */ + /* Force the icon size to 24 */ + .mat-button, + .mat-raised-button, + .mat-icon-button, + .mat-stroked-button, + .mat-flat-button, + .mat-fab, + .mat-mini-fab { + .mat-icon { + margin: 0 !important; + @apply icon-size-6; + } + } + + /* Datepicker default icon size */ + .mat-datepicker-toggle-default-icon { + @apply icon-size-6; + } + + /* Make mat-select usable as prefix and suffix */ + .mat-select { + display: flex; + align-items: center; + + &:focus { + .mat-select-trigger { + .mat-select-value { + @apply text-primary #{'!important'}; + } + + .mat-select-arrow-wrapper { + .mat-select-arrow { + border-top-color: var(--fuse-primary) !important; + } + } + } + } + + .mat-select-trigger { + display: flex; + align-items: center; + + .mat-select-value { + display: flex; + max-width: none; + + mat-select-trigger { + .mat-icon { + margin: 0 !important; + } + } + } + + .mat-select-arrow-wrapper { + display: flex; + align-items: center; + transform: none; + margin-left: 4px; + + .mat-select-arrow { + min-height: 0; + @apply text-gray-500 dark:text-gray-400 #{'!important'}; + } + } + } + } + } + + .mat-form-field-infix { + position: static; + display: flex; + align-items: center; + width: 88px; + padding: 0; + border: 0; + + .mat-input-element { + padding: 14px 0; + margin-top: 0; + } + + /* Textarea */ + textarea.mat-input-element { + display: flex; + align-self: stretch; + min-height: 36px; + height: auto; + margin: 14px 0; + padding: 0 6px 0 0; + transform: none; + } + + /* Select */ + .mat-select { + display: inline-flex; + + .mat-select-trigger { + display: inline-flex; + align-items: center; + width: 100%; + + .mat-select-value { + display: flex; + position: relative; + max-width: none; + + .mat-select-value-text { + display: inline-flex; + + > * { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + } + + .mat-select-arrow-wrapper { + transform: translateY(0); + + .mat-select-arrow { + margin: 0 0 0 8px; + } + } + } + + /* Chips */ + .mat-chip-list { + width: 100%; + margin: 0 -8px; + + .mat-chip-input { + margin: 0 0 0 8px; + } + } + + .mat-form-field-label-wrapper { + top: -25px; + height: auto; + padding-top: 0; + overflow: visible; + pointer-events: auto; + + .mat-form-field-label { + position: relative; + top: 0; + margin-top: 0; + backface-visibility: hidden; + transition: none; + font-weight: 500; + @apply text-default #{'!important'}; + } + } + } + } + + /* Remove the underline */ + .mat-form-field-underline { + display: none; + } + + /* Subscript tweaks */ + .mat-form-field-subscript-wrapper { + position: relative; + top: auto; + padding: 0; + margin-top: 0; + font-size: 12px; + font-weight: 500; + line-height: 1; + + > div { + display: contents; /* Remove the div from flow to stop the subscript animation */ + } + + .mat-error, + .mat-hint { + display: block; + margin-top: 4px; + } + + .mat-hint { + @apply text-hint #{'!important'}; + } + } + } + + /* Adds better alignment for textarea inputs */ + &.fuse-mat-textarea { + .mat-form-field-wrapper { + .mat-form-field-flex { + .mat-form-field-prefix, + .mat-form-field-suffix { + align-items: flex-start; + } + + .mat-form-field-prefix { + padding-top: 12px; + } + + .mat-form-field-suffix { + padding-top: 12px; + } + } + } + } + + /* Removes subscript space */ + &.fuse-mat-no-subscript { + .mat-form-field-wrapper { + padding-bottom: 0; + margin-bottom: 0; + + .mat-form-field-subscript-wrapper { + display: none !important; + height: 0 !important; + } + } + } + + /* Rounded */ + &.fuse-mat-rounded { + .mat-form-field-wrapper { + .mat-form-field-flex { + border-radius: 24px; + } + } + + /* Emphasized affix */ + &.fuse-mat-emphasized-affix { + .mat-form-field-wrapper { + .mat-form-field-flex { + .mat-form-field-prefix { + border-radius: 24px 0 0 24px; + + > .mat-icon { + margin-right: 12px; + } + + > .mat-icon-button { + margin-right: 2px; + } + + > .mat-select { + margin-right: 8px; + } + + > .mat-datepicker-toggle { + margin-right: 4px; + } + + > *:not(.mat-icon):not(.mat-icon-button):not(.mat-select):not( + .mat-datepicker-toggle + ) { + margin-right: 12px; + } + } + + .mat-form-field-suffix { + border-radius: 0 24px 24px 0; + + > .mat-icon { + margin-left: 12px !important; + } + + > .mat-icon-button { + margin-left: 2px !important; + } + + > .mat-select { + margin-left: 12px !important; + } + + > .mat-datepicker-toggle { + margin-left: 4px !important; + } + + > *:not(.mat-icon):not(.mat-icon-button):not(.mat-select):not( + .mat-datepicker-toggle + ) { + margin-left: 12px !important; + } + } + } + } + } + } + + /* Dense */ + &.fuse-mat-dense { + .mat-form-field-wrapper { + .mat-form-field-flex { + min-height: 40px; + + .mat-form-field-prefix, + .mat-form-field-suffix { + .mat-icon-button { + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + } + } + + .mat-form-field-prefix { + > .mat-icon-button { + margin-left: -6px; + margin-right: 12px; + } + } + + .mat-form-field-suffix { + > .mat-icon-button { + margin-left: 12px; + margin-right: -6px; + } + } + + .mat-form-field-infix { + .mat-input-element { + padding: 11px 0; + } + } + } + } + + /* Rounded */ + &.fuse-mat-rounded { + .mat-form-field-wrapper { + .mat-form-field-flex { + border-radius: 20px; + } + } + + /* Emphasized affix */ + &.fuse-mat-emphasized-affix { + .mat-form-field-wrapper { + .mat-form-field-flex { + .mat-form-field-prefix { + border-radius: 20px 0 0 20px !important; + } + + .mat-form-field-suffix { + border-radius: 0 20px 20px 0 !important; + } + } + } + } + } + } + + /* Emphasized affix */ + &.fuse-mat-emphasized-affix { + .mat-form-field-wrapper { + .mat-form-field-flex { + .mat-form-field-prefix { + margin: 0 16px 0 -16px; + padding-left: 16px; + border-radius: 6px 0 0 6px; + border-right-width: 1px; + + > .mat-icon { + margin-right: 16px; + } + + > .mat-icon-button { + margin: 0 6px 0 -10px; + } + + > .mat-select { + margin-right: 12px; + } + + > .mat-datepicker-toggle { + margin-right: 8px; + } + + > *:not(.mat-icon):not(.mat-icon-button):not(.mat-select):not( + .mat-datepicker-toggle + ) { + margin-right: 16px; + } + } + + .mat-form-field-suffix { + margin: 0 -16px 0 16px; + padding-right: 16px; + border-radius: 0 6px 6px 0; + border-left-width: 1px; + + > .mat-icon { + margin-left: 16px; + } + + > .mat-icon-button { + margin: 0 -10px 0 6px; + } + + > .mat-select { + margin: 0 -4px 0 16px; + } + + > .mat-datepicker-toggle { + margin-left: 8px; + } + + > *:not(.mat-icon):not(.mat-icon-button):not(.mat-select):not( + .mat-datepicker-toggle + ) { + margin-left: 16px; + } + } + + .mat-form-field-prefix, + .mat-form-field-suffix { + @apply bg-default border-gray-300 dark:border-gray-500 #{'!important'}; + } + } + } + } + + /* Bolder border width */ + &.fuse-mat-bold { + .mat-form-field-wrapper { + .mat-form-field-flex { + border-width: 2px !important; + } + } + } +} + +/* Fix the outline appearance */ +.mat-form-field.mat-form-field-appearance-outline { + .mat-form-field-wrapper { + .mat-form-field-flex { + .mat-form-field-outline { + @apply text-gray-300 dark:text-gray-500 #{'!important'}; + } + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Datepicker +/* ----------------------------------------------------------------------------------------------------- */ +/* Hover and active cell content background opacity */ +.mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover, +.cdk-keyboard-focused .mat-calendar-body-active, +.cdk-program-focused .mat-calendar-body-active { + & > .mat-calendar-body-cell-content { + &:not(.mat-calendar-body-selected):not( + .mat-calendar-body-comparison-identical + ) { + @apply bg-primary bg-opacity-30 #{'!important'}; + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Icon +/* ----------------------------------------------------------------------------------------------------- */ +.mat-icon { + display: inline-flex !important; + align-items: center; + justify-content: center; + width: 24px; + min-width: 24px; + height: 24px; + min-height: 24px; + font-size: 24px; + line-height: 24px; + -webkit-appearance: none !important; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Inputs +/* ----------------------------------------------------------------------------------------------------- */ +.mat-input-element { + &::placeholder { + transition: none !important; + @apply text-hint #{'!important'}; + } + + &::-moz-placeholder { + transition: none !important; + @apply text-hint #{'!important'}; + } + + &::-webkit-input-placeholder { + transition: none !important; + @apply text-hint #{'!important'}; + } + + &:-ms-input-placeholder { + transition: none !important; + @apply text-hint #{'!important'}; + } +} + +/* Invalid */ +.mat-form-field-invalid { + .mat-input-element { + /* Placeholder color */ + &::placeholder { + @apply text-warn #{'!important'}; + } + + &::-moz-placeholder { + @apply text-warn #{'!important'}; + } + + &::-webkit-input-placeholder { + @apply text-warn #{'!important'}; + } + + &:-ms-input-placeholder { + @apply text-warn #{'!important'}; + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Menu +/* ----------------------------------------------------------------------------------------------------- */ +.mat-menu-panel { + min-width: 144px !important; + + .mat-menu-content { + .mat-menu-item { + display: flex; + align-items: center; + + &.mat-menu-item-submenu-trigger { + padding-right: 40px; + } + + .mat-icon { + margin-right: 12px; + } + } + + /* Divider within mat-menu */ + mat-divider { + margin: 8px 0; + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Paginator +/* ----------------------------------------------------------------------------------------------------- */ +.mat-paginator { + .mat-paginator-container { + padding: 8px 16px; + justify-content: space-between; + + @screen sm { + justify-content: normal; + } + + /* Page size select */ + .mat-paginator-page-size { + align-items: center; + min-height: 40px; + margin: 8px; + + .mat-paginator-page-size-label { + display: none; + margin-right: 12px; + + @screen sm { + display: block; + } + } + + .mat-paginator-page-size-select { + margin: 0; + + .mat-form-field-wrapper { + margin-bottom: 0; + + .mat-form-field-flex { + min-height: 32px; + padding: 0 10px; + } + } + } + } + + /* Range actions */ + .mat-paginator-range-actions { + margin: 8px 0; + + .mat-paginator-range-label { + margin-right: 16px; + } + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Select +/* ----------------------------------------------------------------------------------------------------- */ +.mat-select { + display: inline-flex; + + .mat-select-placeholder { + transition: none !important; + @apply text-hint #{'!important'}; + } + + .mat-select-trigger { + display: inline-flex; + align-items: center; + width: 100%; + height: auto; + + .mat-select-value { + display: flex; + position: relative; + max-width: none; + + .mat-select-value-text { + display: inline-flex; + + > * { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + } + + .mat-select-arrow-wrapper { + transform: translateY(0); + + .mat-select-arrow { + margin: 0 4px 0 2px; + } + } +} + +/* Invalid */ +.mat-form-field-invalid { + .mat-select { + /* Placeholder color */ + .mat-select-placeholder { + @apply text-warn #{'!important'}; + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Slide Toggle +/* ----------------------------------------------------------------------------------------------------- */ +.mat-slide-toggle.mat-checked .mat-slide-toggle-bar { + background-color: rgba(var(--fuse-primary-rgb), 0.54) !important; +} + +.mat-slide-toggle.mat-checked .mat-slide-toggle-thumb { + background-color: var(--fuse-primary) !important; +} + +.mat-slide-toggle.mat-primary.mat-checked .mat-slide-toggle-bar { + background-color: rgba(var(--fuse-primary-500-rgb), 0.54) !important; +} + +.mat-slide-toggle.mat-warn.mat-checked .mat-slide-toggle-bar { + background-color: rgba(var(--fuse-warn-500-rgb), 0.54) !important; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Stepper +/* ----------------------------------------------------------------------------------------------------- */ +.mat-step-icon { + /* Do not override the mat-icon color */ + .mat-icon { + color: currentColor !important; + } +} + +.mat-step-label, +.mat-step-label-selected { + font-weight: 500 !important; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Tabs +/* ----------------------------------------------------------------------------------------------------- */ +.mat-tab-group { + /* No header */ + &.fuse-mat-no-header { + .mat-tab-header { + height: 0 !important; + max-height: 0 !important; + border: none !important; + visibility: hidden !important; + opacity: 0 !important; + } + } + + .mat-tab-header { + border-bottom: none !important; + + .mat-tab-label-container { + padding: 0 24px; + + .mat-tab-list { + .mat-tab-labels { + .mat-tab-label { + min-width: 0 !important; + height: 40px !important; + padding: 0 20px !important; + border-radius: 9999px !important; + @apply text-secondary; + + &.mat-tab-label-active { + @apply bg-gray-700 bg-opacity-10 dark:bg-gray-50 dark:bg-opacity-10 #{'!important'}; + @apply text-default #{'!important'}; + } + + + .mat-tab-label { + margin-left: 4px; + } + + .mat-tab-label-content { + line-height: 20px; + } + } + } + + .mat-ink-bar { + display: none !important; + } + } + } + } + + .mat-tab-body-content { + padding: 24px; + } +} + +.mat-tab-label { + opacity: 1 !important; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Textarea +/* ----------------------------------------------------------------------------------------------------- */ +textarea.mat-input-element { + box-sizing: content-box !important; +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Toolbar +/* ----------------------------------------------------------------------------------------------------- */ +.mat-toolbar { + /* Apply primary contrast color */ + &.mat-primary { + .mat-icon { + @apply text-on-primary #{'!important'}; + } + + .text-secondary { + @apply text-on-primary text-opacity-60 #{'!important'}; + } + + .text-hint { + @apply text-on-primary text-opacity-38 #{'!important'}; + } + + .text-disabled { + @apply text-on-primary text-opacity-38 #{'!important'}; + } + + .divider { + @apply text-on-primary text-opacity-12 #{'!important'}; + } + } + + /* Apply accent contrast color */ + &.mat-accent { + .mat-icon { + @apply text-on-accent #{'!important'}; + } + + .text-secondary { + @apply text-on-accent text-opacity-60 #{'!important'}; + } + + .text-hint { + @apply text-on-accent text-opacity-38 #{'!important'}; + } + + .text-disabled { + @apply text-on-accent text-opacity-38 #{'!important'}; + } + + .divider { + @apply text-on-accent text-opacity-12 #{'!important'}; + } + } + + /* Apply warn contrast color */ + &.mat-warn { + .mat-icon { + @apply text-on-warn #{'!important'}; + } + + .text-secondary { + @apply text-on-warn text-opacity-60 #{'!important'}; + } + + .text-hint { + @apply text-on-warn text-opacity-38 #{'!important'}; + } + + .text-disabled { + @apply text-on-warn text-opacity-38 #{'!important'}; + } + + .divider { + @apply text-on-warn text-opacity-12 #{'!important'}; + } + } +} + +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Snackbar +/* ----------------------------------------------------------------------------------------------------- */ +.mat-simple-snackbar-action { + @apply text-sky-600 #{'!important'}; +} diff --git a/transparency_dashboard_frontend/src/@fuse/styles/overrides/highlightjs.scss b/transparency_dashboard_frontend/src/@fuse/styles/overrides/highlightjs.scss new file mode 100644 index 0000000000000000000000000000000000000000..120ef830f24cdf1b0130f7b1facf9c63893cb951 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/overrides/highlightjs.scss @@ -0,0 +1,82 @@ +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Highlight.js overrides +/* ----------------------------------------------------------------------------------------------------- */ +code[class*='language-'], +pre[class*='language-'] { + + .hljs-comment, + .hljs-quote { + color: #8B9FC1; + font-style: italic; + } + + .hljs-doctag, + .hljs-keyword, + .hljs-formula { + color: #22D3EE; + } + + .hljs-name { + color: #E879F9; + } + + .hljs-tag { + color: #BAE6FD; + } + + .hljs-section, + .hljs-selector-tag, + .hljs-deletion, + .hljs-subst { + color: #F87F71; + } + + .hljs-literal { + color: #36BEFF; + } + + .hljs-string, + .hljs-regexp, + .hljs-addition, + .hljs-attribute, + .hljs-meta-string { + color: #BEF264; + } + + .hljs-built_in, + .hljs-class .hljs-title { + color: #FFD374; + } + + .hljs-attr, + .hljs-variable, + .hljs-template-variable, + .hljs-type, + .hljs-selector-class, + .hljs-selector-attr, + .hljs-selector-pseudo, + .hljs-number { + color: #22D3EE; + } + + .hljs-symbol, + .hljs-bullet, + .hljs-link, + .hljs-meta, + .hljs-selector-id, + .hljs-title { + color: #E879F9; + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: 700; + } + + .hljs-link { + text-decoration: underline; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/styles/overrides/perfect-scrollbar.scss b/transparency_dashboard_frontend/src/@fuse/styles/overrides/perfect-scrollbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..586b47dc90523c4185dc53c1b88bb8d0e7669233 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/overrides/perfect-scrollbar.scss @@ -0,0 +1,69 @@ +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Perfect scrollbar overrides +/* ----------------------------------------------------------------------------------------------------- */ +.ps { + position: relative; + + &:hover, + &.ps--focus, + &.ps--scrolling-x, + &.ps--scrolling-y { + + > .ps__rail-x, + > .ps__rail-y { + opacity: 1; + } + } + + > .ps__rail-x, + > .ps__rail-y { + z-index: 99999; + } + + > .ps__rail-x { + height: 14px; + background: transparent !important; + transition: none !important; + + &:hover, + &:focus, + &.ps--clicking { + opacity: 1; + + .ps__thumb-x { + height: 10px; + } + } + + .ps__thumb-x { + background: rgba(0, 0, 0, 0.5); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15); + height: 6px; + transition: height 225ms cubic-bezier(0.25, 0.8, 0.25, 1); + } + } + + > .ps__rail-y { + width: 14px; + background: transparent !important; + transition: none !important; + left: auto !important; + + &:hover, + &:focus, + &.ps--clicking { + opacity: 1; + + .ps__thumb-y { + width: 10px; + } + } + + .ps__thumb-y { + background: rgba(0, 0, 0, 0.5); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15); + width: 6px; + transition: width 225ms cubic-bezier(0.25, 0.8, 0.25, 1); + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/styles/overrides/quill.scss b/transparency_dashboard_frontend/src/@fuse/styles/overrides/quill.scss new file mode 100644 index 0000000000000000000000000000000000000000..61832818342fadeaf591e9e9801bb70116d875f4 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/overrides/quill.scss @@ -0,0 +1,105 @@ +/* ----------------------------------------------------------------------------------------------------- */ +/* @ Quill editor overrides +/* ----------------------------------------------------------------------------------------------------- */ +.ql-toolbar { + border-radius: 6px 6px 0 0; + padding: 0 !important; + @apply bg-gray-100; + @apply border-gray-300 border-opacity-100 #{'!important'}; + + .dark & { + background-color: rgba(0, 0, 0, 0.05); + @apply border-gray-500 #{'!important'}; + } + + .ql-formats { + margin: 11px 8px !important; + } + + .ql-picker { + + &.ql-expanded { + + .ql-picker-label { + @apply border-gray-300; + + .dark & { + @apply border-gray-500; + } + } + + .ql-picker-options { + z-index: 10 !important; + @apply border-gray-300 bg-card; + + .dark & { + @apply border-gray-500; + } + } + } + + .ql-picker-label { + @apply text-default; + } + + .ql-picker-options { + + .ql-picker-item { + @apply text-default; + } + } + } + + .ql-stroke, + .ql-stroke-mitter { + stroke: var(--fuse-icon); + } + + .ql-fill { + fill: var(--fuse-icon); + } + + button:hover, + button:focus, + button.ql-active, + .ql-picker-label:hover, + .ql-picker-label.ql-active, + .ql-picker-item:hover, + .ql-picker-item.ql-selected { + @apply text-primary #{'!important'}; + + .ql-stroke, + .ql-stroke-mitter { + stroke: var(--fuse-primary) !important; + } + + .ql-fill { + fill: var(--fuse-primary) !important; + } + } +} + +.ql-container { + overflow: hidden; + border-radius: 0 0 6px 6px; + @apply border-gray-300 border-opacity-100 shadow-sm #{'!important'}; + + .dark & { + @apply border-gray-500 #{'!important'}; + } + + .ql-editor { + min-height: 160px; + max-height: 160px; + height: 160px; + @apply bg-card; + + .dark & { + background-color: rgba(0, 0, 0, 0.05); + } + + &.ql-blank::before { + @apply text-hint; + } + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/styles/tailwind.scss b/transparency_dashboard_frontend/src/@fuse/styles/tailwind.scss new file mode 100644 index 0000000000000000000000000000000000000000..e4d823f94d47558eb0e41f2cd5154e8bb9c52286 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/tailwind.scss @@ -0,0 +1,98 @@ +/* This injects Tailwind's base styles and any base styles registered by plugins. */ +@tailwind base; + +/* This injects additional styles into Tailwind's base styles layer. */ +@layer base { + + * { + /* Text rendering */ + text-rendering: optimizeLegibility; + -o-text-rendering: optimizeLegibility; + -ms-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + -webkit-text-rendering: optimizeLegibility; + -webkit-tap-highlight-color: transparent; + + /* Remove the focus ring */ + &:focus { + outline: none !important; + } + } + + /* HTML and Body default styles */ + html, + body { + display: flex; + flex-direction: column; + flex: 1 1 auto; + width: 100%; + min-height: 100%; + -webkit-font-smoothing: auto; + -moz-osx-font-smoothing: auto; + } + + /* Font size */ + html { + font-size: 16px; + } + + body { + font-size: 0.875rem; + } + + /* Stylistic alternates for Inter */ + body { + font-feature-settings: 'salt'; + } + + /* Better spacing and border for horizontal rule */ + hr { + margin: 32px 0; + border-bottom-width: 1px; + } + + /* Make images and videos to take up all the available space */ + img { + width: 100%; + vertical-align: top; + } + + /* Fix: Disabled placeholder color is too faded on Safari */ + input[disabled] { + opacity: 1; + -webkit-text-fill-color: currentColor; + } + + body, .dark, .light { + @apply text-default bg-default #{'!important'}; + } + + *, *::before, *::after { + --tw-border-opacity: 1 !important; + border-color: rgba(var(--fuse-border-rgb), var(--tw-border-opacity)); + + .dark & { + --tw-border-opacity: 0.12 !important; + } + } + + [disabled] * { + @apply text-disabled #{'!important'}; + } + + /* Print styles */ + @media print { + + /* Make the base font size smaller for print so everything is scaled nicely */ + html { + font-size: 12px !important; + } + + body, .dark, .light { + background: none !important; + } + } +} + +/* This injects Tailwind's component classes and any component classes registered by plugins. */ +@tailwind components; diff --git a/transparency_dashboard_frontend/src/@fuse/styles/themes.scss b/transparency_dashboard_frontend/src/@fuse/styles/themes.scss new file mode 100644 index 0000000000000000000000000000000000000000..8aaa96d50694c2362b95fb77498238057d42b244 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/styles/themes.scss @@ -0,0 +1,167 @@ +@use '@angular/material' as mat; +@use "sass:map"; + +/* Include the core Angular Material styles */ +@include mat.core(); + +/* Create a base theme without color. + This will globally set the density and typography for all future color themes. */ +@include mat.all-component-themes(( + color: null, + density: -2, + typography: mat.define-typography-config( + $font-family: theme('fontFamily.sans'), + $title: mat.define-typography-level(1.25rem, 2rem, 600), + $body-2: mat.define-typography-level(0.875rem, 1.5rem, 600), + $button: mat.define-typography-level(0.875rem, 0.875rem, 500), + $input: mat.define-typography-level(0.875rem, 1.2857142857, 400) /* line-height: 20px */ + ) +)); + +/* Generate Primary, Accent and Warn palettes */ +$palettes: (); +@each $name in (primary, accent, warn) { + $palettes: map.merge($palettes, (#{$name}: ( + 50: var(--fuse-#{$name}-50), + 100: var(--fuse-#{$name}-100), + 200: var(--fuse-#{$name}-200), + 300: var(--fuse-#{$name}-300), + 400: var(--fuse-#{$name}-400), + 500: var(--fuse-#{$name}-500), + 600: var(--fuse-#{$name}-600), + 700: var(--fuse-#{$name}-700), + 800: var(--fuse-#{$name}-800), + 900: var(--fuse-#{$name}-900), + contrast: ( + 50: var(--fuse-on-#{$name}-50), + 100: var(--fuse-on-#{$name}-100), + 200: var(--fuse-on-#{$name}-200), + 300: var(--fuse-on-#{$name}-300), + 400: var(--fuse-on-#{$name}-400), + 500: var(--fuse-on-#{$name}-500), + 600: var(--fuse-on-#{$name}-600), + 700: var(--fuse-on-#{$name}-700), + 800: var(--fuse-on-#{$name}-800), + 900: var(--fuse-on-#{$name}-900) + ), + default: var(--fuse-#{$name}), + lighter: var(--fuse-#{$name}-100), + darker: var(--fuse-#{$name}-700), + text: var(--fuse-#{$name}), + default-contrast: var(--fuse-on-#{$name}), + lighter-contrast: var(--fuse-on-#{$name}-100), + darker-contrast: var(--fuse-on-#{$name}-700) + ))); +} + +/* Generate Angular Material themes. Since we are using CSS Custom Properties, + we don't have to generate a separate Angular Material theme for each color + set. We can just create one light and one dark theme and then switch the + CSS Custom Properties to dynamically switch the colors. */ +body.light, +body .light { + $base-light-theme: mat.define-light-theme(( + color: ($palettes) + )); + + $light-theme: ( + color: ( + primary: map.get(map.get($base-light-theme, color), primary), + accent: map.get(map.get($base-light-theme, color), accent), + warn: map.get(map.get($base-light-theme, color), warn), + is-dark: map.get(map.get($base-light-theme, color), is-dark), + foreground: ( + base: #000000, + divider: #E2E8F0, /* slate.200 */ + dividers: #E2E8F0, /* slate.200 */ + disabled: #94A3B8, /* slate.400 */ + disabled-button: #94A3B8, /* slate.400 */ + disabled-text: #94A3B8, /* slate.400 */ + elevation: #000000, + hint-text: #94A3B8, /* slate.400 */ + secondary-text: #64748B, /* slate.500 */ + icon: #64748B, /* slate.500 */ + icons: #64748B, /* slate.500 */ + mat-icon: #64748B, /* slate.500 */ + text: #1E293B, /* slate.800 */ + slider-min: #1E293B, /* slate.800 */ + slider-off: #CBD5E1, /* slate.300 */ + slider-off-active: #94A3B8 /* slate.400 */ + ), + background: ( + status-bar: #CBD5E1, /* slate.300 */ + app-bar: #FFFFFF, + background: #F1F5F9, /* slate.100 */ + hover: rgba(148, 163, 184, 0.12), /* slate.400 + opacity */ + card: #FFFFFF, + dialog: #FFFFFF, + disabled-button: rgba(148, 163, 184, 0.38), /* slate.400 + opacity */ + raised-button: #FFFFFF, + focused-button: #64748B, /* slate.500 */ + selected-button: #E2E8F0, /* slate.200 */ + selected-disabled-button: #E2E8F0, /* slate.200 */ + disabled-button-toggle: #CBD5E1, /* slate.300 */ + unselected-chip: #E2E8F0, /* slate.200 */ + disabled-list-option: #CBD5E1, /* slate.300 */ + tooltip: #1E293B /* slate.800 */ + ) + ) + ); + + /* Use all-component-colors to only generate the colors */ + @include mat.all-component-colors($light-theme); +} + +body.dark, +body .dark { + $base-dark-theme: mat.define-dark-theme(( + color: ($palettes) + )); + + $dark-theme: ( + color: ( + primary: map.get(map.get($base-dark-theme, color), primary), + accent: map.get(map.get($base-dark-theme, color), accent), + warn: map.get(map.get($base-dark-theme, color), warn), + is-dark: map.get(map.get($base-dark-theme, color), is-dark), + foreground: ( + base: #FFFFFF, + divider: rgba(241, 245, 249, 0.12), /* slate.100 + opacity */ + dividers: rgba(241, 245, 249, 0.12), /* slate.100 + opacity */ + disabled: #475569, /* slate.600 */ + disabled-button: #1E293B, /* slate.800 */ + disabled-text: #475569, /* slate.600 */ + elevation: #000000, + hint-text: #64748B, /* slate.500 */ + secondary-text: #94A3B8, /* slate.400 */ + icon: #F1F5F9, /* slate.100 */ + icons: #F1F5F9, /* slate.100 */ + mat-icon: #94A3B8, /* slate.400 */ + text: #FFFFFF, + slider-min: #FFFFFF, + slider-off: #64748B, /* slate.500 */ + slider-off-active: #94A3B8 /* slate.400 */ + ), + background: ( + status-bar: #0F172A, /* slate.900 */ + app-bar: #0F172A, /* slate.900 */ + background: #0F172A, /* slate.900 */ + hover: rgba(255, 255, 255, 0.05), + card: #1E293B, /* slate.800 */ + dialog: #1E293B, /* slate.800 */ + disabled-button: rgba(15, 23, 42, 0.38), /* slate.900 + opacity */ + raised-button: #0F172A, /* slate.900 */ + focused-button: #E2E8F0, /* slate.200 */ + selected-button: rgba(255, 255, 255, 0.05), + selected-disabled-button: #1E293B, /* slate.800 */ + disabled-button-toggle: #0F172A, /* slate.900 */ + unselected-chip: #475569, /* slate.600 */ + disabled-list-option: #E2E8F0, /* slate.200 */ + tooltip: #64748B /* slate.500 */ + ) + ) + ); + + /* Use all-component-colors to only generate the colors */ + @include mat.all-component-colors($dark-theme); +} diff --git a/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/icon-size.js b/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/icon-size.js new file mode 100644 index 0000000000000000000000000000000000000000..7933b5f1124d680658872bb46fcb2126c1045ffb --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/icon-size.js @@ -0,0 +1,50 @@ +const plugin = require('tailwindcss/plugin'); + +module.exports = plugin( + ({ + matchUtilities, + theme + }) => + { + matchUtilities( + { + 'icon-size': (value) => ({ + width : value, + height : value, + minWidth : value, + minHeight : value, + fontSize : value, + lineHeight: value, + [`svg`] : { + width : value, + height: value + } + }) + }, + { + values: theme('iconSize') + }); + }, + { + theme: { + iconSize: { + 3 : '0.75rem', + 3.5: '0.875rem', + 4 : '1rem', + 4.5: '1.125rem', + 5 : '1.25rem', + 6 : '1.5rem', + 7 : '1.75rem', + 8 : '2rem', + 10 : '2.5rem', + 12 : '3rem', + 14 : '3.5rem', + 16 : '4rem', + 18 : '4.5rem', + 20 : '5rem', + 22 : '5.5rem', + 24 : '6rem' + } + } + } +); diff --git a/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/theming.js b/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/theming.js new file mode 100644 index 0000000000000000000000000000000000000000..244b5acf20d78f8aff73e537c902cfd0a07c0444 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/theming.js @@ -0,0 +1,232 @@ +const chroma = require('chroma-js'); +const _ = require('lodash'); +const path = require('path'); +const colors = require('tailwindcss/colors'); +const plugin = require('tailwindcss/plugin'); +const flattenColorPalette = require('tailwindcss/lib/util/flattenColorPalette').default; +const generateContrasts = require(path.resolve(__dirname, ('../utils/generate-contrasts'))); + +// ----------------------------------------------------------------------------------------------------- +// @ Utilities +// ----------------------------------------------------------------------------------------------------- + +/** + * Normalize the provided theme + * + * @param theme + */ +const normalizeTheme = (theme) => +{ + return _.fromPairs(_.map(_.omitBy(theme, (palette, paletteName) => paletteName.startsWith('on') || _.isEmpty(palette)), + (palette, paletteName) => [ + paletteName, + { + ...palette, + DEFAULT: palette['DEFAULT'] || palette[500] + } + ] + )); +}; + +/** + * Generates variable colors for the 'colors' + * configuration from the provided theme + * + * @param theme + */ +const generateVariableColors = (theme) => +{ + // https://github.com/adamwathan/tailwind-css-variable-text-opacity-demo + const customPropertiesWithOpacity = (name) => ({ + opacityVariable, + opacityValue + }) => + { + if ( opacityValue ) + { + return `rgba(var(--fuse-${name}-rgb), ${opacityValue})`; + } + if ( opacityVariable ) + { + return `rgba(var(--fuse-${name}-rgb), var(${opacityVariable}, 1))`; + } + return `rgb(var(--fuse-${name}-rgb))`; + }; + + return _.fromPairs(_.flatten(_.map(_.keys(flattenColorPalette(normalizeTheme(theme))), (name) => [ + [name, customPropertiesWithOpacity(name)], + [`on-${name}`, customPropertiesWithOpacity(`on-${name}`)] + ]))); +}; + +/** + * Generate and return themes object with theme name and colors/ + * This is useful for accessing themes from Angular (Typescript). + * + * @param themes + * @returns {unknown[]} + */ +function generateThemesObject(themes) +{ + const normalizedDefaultTheme = normalizeTheme(themes.default); + return _.map(_.cloneDeep(themes), (value, key) => + { + const theme = normalizeTheme(value); + const primary = (theme && theme.primary && theme.primary.DEFAULT) ? theme.primary.DEFAULT : normalizedDefaultTheme.primary.DEFAULT; + const accent = (theme && theme.accent && theme.accent.DEFAULT) ? theme.accent.DEFAULT : normalizedDefaultTheme.accent.DEFAULT; + const warn = (theme && theme.warn && theme.warn.DEFAULT) ? theme.warn.DEFAULT : normalizedDefaultTheme.warn.DEFAULT; + + return _.fromPairs([ + [ + key, + { + primary, + accent, + warn + } + ] + ]); + }); +} + +// ----------------------------------------------------------------------------------------------------- +// @ FUSE TailwindCSS Main Plugin +// ----------------------------------------------------------------------------------------------------- +const theming = plugin.withOptions((options) => ({ + addComponents, + e, + theme + }) => + { + // ----------------------------------------------------------------------------------------------------- + // @ Map variable colors + // ----------------------------------------------------------------------------------------------------- + const mapVariableColors = _.fromPairs(_.map(options.themes, (theme, themeName) => [ + themeName === 'default' ? 'body, .theme-default' : `.theme-${e(themeName)}`, + _.fromPairs(_.flatten(_.map(flattenColorPalette(_.fromPairs(_.flatten(_.map(normalizeTheme(theme), (palette, paletteName) => [ + [ + e(paletteName), + palette + ], + [ + `on-${e(paletteName)}`, + _.fromPairs(_.map(generateContrasts(palette), (color, hue) => [hue, _.get(theme, [`on-${paletteName}`, hue]) || color])) + ] + ]) + ))), (value, key) => [[`--fuse-${e(key)}`, value], [`--fuse-${e(key)}-rgb`, chroma(value).rgb().join(',')]]))) + ])); + + addComponents(mapVariableColors); + + // ----------------------------------------------------------------------------------------------------- + // @ Generate scheme based css custom properties and utility classes + // ----------------------------------------------------------------------------------------------------- + const schemeCustomProps = _.map(['light', 'dark'], (colorScheme) => + { + const isDark = colorScheme === 'dark'; + const background = theme(`fuse.customProps.background.${colorScheme}`); + const foreground = theme(`fuse.customProps.foreground.${colorScheme}`); + const lightSchemeSelectors = 'body.light, .light, .dark .light'; + const darkSchemeSelectors = 'body.dark, .dark, .light .dark'; + + return { + [(isDark ? darkSchemeSelectors : lightSchemeSelectors)]: { + + /** + * If a custom property is not available, browsers will use + * the fallback value. In this case, we want to use '--is-dark' + * as the indicator of a dark theme so we can use it like this: + * background-color: var(--is-dark, red); + * + * If we set '--is-dark' as "true" on dark themes, the above rule + * won't work because of the said "fallback value" logic. Therefore, + * we set the '--is-dark' to "false" on light themes and not set it + * all on dark themes so that the fallback value can be used on + * dark themes. + * + * On light themes, since '--is-dark' exists, the above rule will be + * interpolated as: + * "background-color: false" + * + * On dark themes, since '--is-dark' doesn't exist, the fallback value + * will be used ('red' in this case) and the rule will be interpolated as: + * "background-color: red" + * + * It's easier to understand and remember like this. + */ + ...(!isDark ? {'--is-dark': 'false'} : {}), + + // Generate custom properties from customProps + ..._.fromPairs(_.flatten(_.map(background, (value, key) => [[`--fuse-${e(key)}`, value], [`--fuse-${e(key)}-rgb`, chroma(value).rgb().join(',')]]))), + ..._.fromPairs(_.flatten(_.map(foreground, (value, key) => [[`--fuse-${e(key)}`, value], [`--fuse-${e(key)}-rgb`, chroma(value).rgb().join(',')]]))) + } + }; + }); + + const schemeUtilities = (() => + { + // Generate general styles & utilities + return {}; + })(); + + addComponents(schemeCustomProps); + addComponents(schemeUtilities); + }, + (options) => + { + return { + theme : { + extend: { + colors: generateVariableColors(options.themes.default) + }, + fuse : { + customProps: { + background: { + light: { + 'bg-app-bar' : '#FFFFFF', + 'bg-card' : '#FFFFFF', + 'bg-default' : colors.slate[100], + 'bg-dialog' : '#FFFFFF', + 'bg-hover' : chroma(colors.slate[400]).alpha(0.12).css(), + 'bg-status-bar': colors.slate[300] + }, + dark : { + 'bg-app-bar' : colors.slate[900], + 'bg-card' : colors.slate[800], + 'bg-default' : colors.slate[900], + 'bg-dialog' : colors.slate[800], + 'bg-hover' : 'rgba(255, 255, 255, 0.05)', + 'bg-status-bar': colors.slate[900] + } + }, + foreground: { + light: { + 'text-default' : colors.slate[800], + 'text-secondary': colors.slate[500], + 'text-hint' : colors.slate[400], + 'text-disabled' : colors.slate[400], + 'border' : colors.slate[200], + 'divider' : colors.slate[200], + 'icon' : colors.slate[500], + 'mat-icon' : colors.slate[500] + }, + dark : { + 'text-default' : '#FFFFFF', + 'text-secondary': colors.slate[400], + 'text-hint' : colors.slate[500], + 'text-disabled' : colors.slate[600], + 'border' : chroma(colors.slate[100]).alpha(0.12).css(), + 'divider' : chroma(colors.slate[100]).alpha(0.12).css(), + 'icon' : colors.slate[400], + 'mat-icon' : colors.slate[400] + } + } + }, + themes : generateThemesObject(options.themes) + } + } + }; + } +); + +module.exports = theming; diff --git a/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/utilities.js b/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/utilities.js new file mode 100644 index 0000000000000000000000000000000000000000..4b1afb9dede3428e427b3ad3accae33de1860377 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/tailwind/plugins/utilities.js @@ -0,0 +1,67 @@ +const plugin = require('tailwindcss/plugin'); + +module.exports = plugin(({ + addComponents +}) => +{ + /* + * Add base components. These are very important for everything to look + * correct. We are adding these to the 'components' layer because they must + * be defined before pretty much everything else. + */ + addComponents( + { + '.mat-icon' : { + '--tw-text-opacity': '1', + color : 'rgba(var(--fuse-mat-icon-rgb), var(--tw-text-opacity))' + }, + '.text-default' : { + '--tw-text-opacity': '1 !important', + color : 'rgba(var(--fuse-text-default-rgb), var(--tw-text-opacity)) !important' + }, + '.text-secondary' : { + '--tw-text-opacity': '1 !important', + color : 'rgba(var(--fuse-text-secondary-rgb), var(--tw-text-opacity)) !important' + }, + '.text-hint' : { + '--tw-text-opacity': '1 !important', + color : 'rgba(var(--fuse-text-hint-rgb), var(--tw-text-opacity)) !important' + }, + '.text-disabled' : { + '--tw-text-opacity': '1 !important', + color : 'rgba(var(--fuse-text-disabled-rgb), var(--tw-text-opacity)) !important' + }, + '.divider' : { + color: 'var(--fuse-divider) !important' + }, + '.bg-card' : { + '--tw-bg-opacity': '1 !important', + backgroundColor : 'rgba(var(--fuse-bg-card-rgb), var(--tw-bg-opacity)) !important' + }, + '.bg-default' : { + '--tw-bg-opacity': '1 !important', + backgroundColor : 'rgba(var(--fuse-bg-default-rgb), var(--tw-bg-opacity)) !important' + }, + '.bg-dialog' : { + '--tw-bg-opacity': '1 !important', + backgroundColor : 'rgba(var(--fuse-bg-dialog-rgb), var(--tw-bg-opacity)) !important' + }, + '.ring-bg-default': { + '--tw-ring-opacity': '1 !important', + '--tw-ring-color' : 'rgba(var(--fuse-bg-default-rgb), var(--tw-ring-opacity)) !important' + }, + '.ring-bg-card' : { + '--tw-ring-opacity': '1 !important', + '--tw-ring-color' : 'rgba(var(--fuse-bg-card-rgb), var(--tw-ring-opacity)) !important' + } + } + ); + + addComponents( + { + '.bg-hover': { + backgroundColor: 'var(--fuse-bg-hover) !important' + } + } + ); +}); diff --git a/transparency_dashboard_frontend/src/@fuse/tailwind/utils/generate-contrasts.js b/transparency_dashboard_frontend/src/@fuse/tailwind/utils/generate-contrasts.js new file mode 100644 index 0000000000000000000000000000000000000000..fd98c3c82815d29f86bfa8fca05c12530431459a --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/tailwind/utils/generate-contrasts.js @@ -0,0 +1,31 @@ +const chroma = require('chroma-js'); +const _ = require('lodash'); + +/** + * Generates contrasting counterparts of the given palette. + * The provided palette must be in the same format with + * default Tailwind color palettes. + * + * @param palette + * @private + */ +const generateContrasts = (palette) => +{ + const lightColor = '#FFFFFF'; + let darkColor = '#FFFFFF'; + + // Iterate through the palette to find the darkest color + _.forEach(palette, ((color) => + { + darkColor = chroma.contrast(color, '#FFFFFF') > chroma.contrast(darkColor, '#FFFFFF') ? color : darkColor; + })); + + // Generate the contrasting colors + return _.fromPairs(_.map(palette, ((color, hue) => [ + hue, + chroma.contrast(color, darkColor) > chroma.contrast(color, lightColor) ? darkColor : lightColor + ] + ))); +}; + +module.exports = generateContrasts; diff --git a/transparency_dashboard_frontend/src/@fuse/tailwind/utils/generate-palette.js b/transparency_dashboard_frontend/src/@fuse/tailwind/utils/generate-palette.js new file mode 100644 index 0000000000000000000000000000000000000000..e2a8c90c106ccd2382e8fec1e785586903f4cce3 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/tailwind/utils/generate-palette.js @@ -0,0 +1,100 @@ +const chroma = require('chroma-js'); +const _ = require('lodash'); + +/** + * Generates palettes from the provided configuration. + * Accepts a single color string or a Tailwind-like + * color object. If provided Tailwind-like color object, + * it must have a 500 hue level. + * + * @param config + */ +const generatePalette = (config) => +{ + // Prepare an empty palette + const palette = { + 50 : null, + 100: null, + 200: null, + 300: null, + 400: null, + 500: null, + 600: null, + 700: null, + 800: null, + 900: null + }; + + // If a single color is provided, + // assign it to the 500 + if ( _.isString(config) ) + { + palette[500] = chroma.valid(config) ? config : null; + } + + // If a partial palette is provided, + // assign the values + if ( _.isPlainObject(config) ) + { + if ( !chroma.valid(config[500]) ) + { + throw new Error('You must have a 500 hue in your palette configuration! Make sure the main color of your palette is marked as 500.'); + } + + // Remove everything that is not a hue/color entry + config = _.pick(config, Object.keys(palette)); + + // Merge the values + _.mergeWith(palette, config, (objValue, srcValue) => chroma.valid(srcValue) ? srcValue : null); + } + + // Prepare the colors array + const colors = Object.values(palette).filter((color) => color); + + // Generate a very dark and a very light versions of the + // default color to use them as the boundary colors rather + // than using pure white and pure black. This will stop + // in between colors' hue values to slipping into the grays. + colors.unshift( + chroma.scale(['white', palette[500]]) + .domain([0, 1]) + .mode("lrgb") + .colors(50)[1] + ); + colors.push( + chroma.scale(['black', palette[500]]) + .domain([0, 1]) + .mode("lrgb") + .colors(10)[1] + ); + + // Prepare the domains array + const domain = [ + 0, + ...Object.entries(palette) + .filter(([key, value]) => value) + .map(([key]) => parseInt(key) / 1000), + 1 + ]; + + // Generate the color scale + const scale = chroma.scale(colors) + .domain(domain) + .mode('lrgb'); + + // Build and return the final palette + return { + 50 : scale(0.05).hex(), + 100: scale(0.1).hex(), + 200: scale(0.2).hex(), + 300: scale(0.3).hex(), + 400: scale(0.4).hex(), + 500: scale(0.5).hex(), + 600: scale(0.6).hex(), + 700: scale(0.7).hex(), + 800: scale(0.8).hex(), + 900: scale(0.9).hex() + }; +}; + +module.exports = generatePalette; diff --git a/transparency_dashboard_frontend/src/@fuse/validators/index.ts b/transparency_dashboard_frontend/src/@fuse/validators/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e4748d357a80e8e14daae72d9e62c5179080047 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/validators/index.ts @@ -0,0 +1 @@ +export * from '@fuse/validators/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/validators/public-api.ts b/transparency_dashboard_frontend/src/@fuse/validators/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc16900a369fb1c83ca0f961d335f3d50d9791db --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/validators/public-api.ts @@ -0,0 +1 @@ +export * from '@fuse/validators/validators'; diff --git a/transparency_dashboard_frontend/src/@fuse/validators/validators.ts b/transparency_dashboard_frontend/src/@fuse/validators/validators.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fb1f60fad276dd096fab16c0582bd716ca8cc14 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/validators/validators.ts @@ -0,0 +1,59 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export class FuseValidators +{ + /** + * Check for empty (optional fields) values + * + * @param value + */ + static isEmptyInputValue(value: any): boolean + { + return value == null || value.length === 0; + } + + /** + * Must match validator + * + * @param controlPath A dot-delimited string values that define the path to the control. + * @param matchingControlPath A dot-delimited string values that define the path to the matching control. + */ + static mustMatch(controlPath: string, matchingControlPath: string): ValidatorFn + { + return (formGroup: AbstractControl): ValidationErrors | null => { + + // Get the control and matching control + const control = formGroup.get(controlPath); + const matchingControl = formGroup.get(matchingControlPath); + + // Return if control or matching control doesn't exist + if ( !control || !matchingControl ) + { + return null; + } + + // Delete the mustMatch error to reset the error on the matching control + if ( matchingControl.hasError('mustMatch') ) + { + delete matchingControl.errors.mustMatch; + matchingControl.updateValueAndValidity(); + } + + // Don't validate empty values on the matching control + // Don't validate if values are matching + if ( this.isEmptyInputValue(matchingControl.value) || control.value === matchingControl.value ) + { + return null; + } + + // Prepare the validation errors + const errors = {mustMatch: true}; + + // Set the validation error on the matching control + matchingControl.setErrors(errors); + + // Return the errors + return errors; + }; + } +} diff --git a/transparency_dashboard_frontend/src/@fuse/version/fuse-version.ts b/transparency_dashboard_frontend/src/@fuse/version/fuse-version.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a485a4a642b1e7ee9705b0d562bc2a943032b7e --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/version/fuse-version.ts @@ -0,0 +1,3 @@ +import { Version } from '@fuse/version/version'; + +export const FUSE_VERSION = new Version('14.2.0').full; diff --git a/transparency_dashboard_frontend/src/@fuse/version/index.ts b/transparency_dashboard_frontend/src/@fuse/version/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..aaf271872310c4770d1432bda6b89c5376a9a59b --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/version/index.ts @@ -0,0 +1 @@ +export * from '@fuse/version/public-api'; diff --git a/transparency_dashboard_frontend/src/@fuse/version/public-api.ts b/transparency_dashboard_frontend/src/@fuse/version/public-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..8645bbfcf0a456ccf7dae0966d8014708c401ebd --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/version/public-api.ts @@ -0,0 +1,2 @@ +export * from '@fuse/version/fuse-version'; +export * from '@fuse/version/version'; diff --git a/transparency_dashboard_frontend/src/@fuse/version/version.ts b/transparency_dashboard_frontend/src/@fuse/version/version.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e7a8d4b83269286bbcb855de48d459d5e00a003 --- /dev/null +++ b/transparency_dashboard_frontend/src/@fuse/version/version.ts @@ -0,0 +1,21 @@ +/** + * Derived from Angular's version class + */ +export class Version +{ + public readonly full: string; + public readonly major: string; + public readonly minor: string; + public readonly patch: string; + + /** + * Constructor + */ + constructor(public version: string) + { + this.full = version; + this.major = version.split('.')[0]; + this.minor = version.split('.')[1]; + this.patch = version.split('.').slice(2).join('.'); + } +}