@lukfel/ng-scaffold 20.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +37 -0
- package/README.md +382 -0
- package/ng-package.json +8 -0
- package/package.json +28 -0
- package/src/lib/components/bottom-bar/bottom-bar.component.html +29 -0
- package/src/lib/components/bottom-bar/bottom-bar.component.scss +33 -0
- package/src/lib/components/bottom-bar/bottom-bar.component.spec.ts +24 -0
- package/src/lib/components/bottom-bar/bottom-bar.component.ts +31 -0
- package/src/lib/components/content-title-card/content-title-card.component.html +25 -0
- package/src/lib/components/content-title-card/content-title-card.component.scss +17 -0
- package/src/lib/components/content-title-card/content-title-card.component.spec.ts +108 -0
- package/src/lib/components/content-title-card/content-title-card.component.ts +24 -0
- package/src/lib/components/drawer/drawer.component.html +33 -0
- package/src/lib/components/drawer/drawer.component.scss +10 -0
- package/src/lib/components/drawer/drawer.component.spec.ts +52 -0
- package/src/lib/components/drawer/drawer.component.ts +30 -0
- package/src/lib/components/floating-button/floating-button.component.html +32 -0
- package/src/lib/components/floating-button/floating-button.component.scss +20 -0
- package/src/lib/components/floating-button/floating-button.component.spec.ts +84 -0
- package/src/lib/components/floating-button/floating-button.component.ts +57 -0
- package/src/lib/components/footer/footer.component.html +38 -0
- package/src/lib/components/footer/footer.component.scss +39 -0
- package/src/lib/components/footer/footer.component.spec.ts +118 -0
- package/src/lib/components/footer/footer.component.ts +14 -0
- package/src/lib/components/header/header.component.html +170 -0
- package/src/lib/components/header/header.component.scss +102 -0
- package/src/lib/components/header/header.component.spec.ts +134 -0
- package/src/lib/components/header/header.component.ts +53 -0
- package/src/lib/components/loading-overlay/loading-overlay.component.html +3 -0
- package/src/lib/components/loading-overlay/loading-overlay.component.scss +16 -0
- package/src/lib/components/loading-overlay/loading-overlay.component.spec.ts +24 -0
- package/src/lib/components/loading-overlay/loading-overlay.component.ts +10 -0
- package/src/lib/components/navbar/navbar.component.html +43 -0
- package/src/lib/components/navbar/navbar.component.scss +71 -0
- package/src/lib/components/navbar/navbar.component.spec.ts +43 -0
- package/src/lib/components/navbar/navbar.component.ts +35 -0
- package/src/lib/components/scaffold/scaffold.component.html +74 -0
- package/src/lib/components/scaffold/scaffold.component.scss +48 -0
- package/src/lib/components/scaffold/scaffold.component.spec.ts +119 -0
- package/src/lib/components/scaffold/scaffold.component.ts +191 -0
- package/src/lib/interceptors/loading.interceptor.ts +51 -0
- package/src/lib/models/bottom-bar-config.model.ts +8 -0
- package/src/lib/models/confirm-dialog-config.model.ts +6 -0
- package/src/lib/models/content-title-card-config.model.ts +6 -0
- package/src/lib/models/drawer-config.model.ts +6 -0
- package/src/lib/models/floating-button-config.model.ts +13 -0
- package/src/lib/models/footer-config.model.ts +10 -0
- package/src/lib/models/header-config.model.ts +26 -0
- package/src/lib/models/index.ts +15 -0
- package/src/lib/models/library-config.model.ts +4 -0
- package/src/lib/models/menu-button.model.ts +10 -0
- package/src/lib/models/navbar-config.model.ts +8 -0
- package/src/lib/models/navigation-link.model.ts +6 -0
- package/src/lib/models/placeholder-config.model.ts +7 -0
- package/src/lib/models/scaffold-config.model.ts +21 -0
- package/src/lib/models/seo-config.model.ts +6 -0
- package/src/lib/scaffold.module.ts +54 -0
- package/src/lib/services/breakpoint.service.spec.ts +15 -0
- package/src/lib/services/breakpoint.service.ts +16 -0
- package/src/lib/services/dialog.service.spec.ts +18 -0
- package/src/lib/services/dialog.service.ts +58 -0
- package/src/lib/services/index.ts +9 -0
- package/src/lib/services/local-storage.service.spec.ts +15 -0
- package/src/lib/services/local-storage.service.ts +125 -0
- package/src/lib/services/logger.service.spec.ts +15 -0
- package/src/lib/services/logger.service.ts +46 -0
- package/src/lib/services/router.service.spec.ts +15 -0
- package/src/lib/services/router.service.ts +91 -0
- package/src/lib/services/scaffold.service.spec.ts +15 -0
- package/src/lib/services/scaffold.service.ts +77 -0
- package/src/lib/services/seo.service.spec.ts +15 -0
- package/src/lib/services/seo.service.ts +75 -0
- package/src/lib/services/snackbar.service.spec.ts +18 -0
- package/src/lib/services/snackbar.service.ts +38 -0
- package/src/lib/services/theme.service.spec.ts +20 -0
- package/src/lib/services/theme.service.ts +71 -0
- package/src/lib/shared/components/dialogs/confirm-dialog/confirm-dialog.component.html +24 -0
- package/src/lib/shared/components/dialogs/confirm-dialog/confirm-dialog.component.scss +0 -0
- package/src/lib/shared/components/dialogs/confirm-dialog/confirm-dialog.component.spec.ts +85 -0
- package/src/lib/shared/components/dialogs/confirm-dialog/confirm-dialog.component.ts +14 -0
- package/src/lib/shared/components/file-upload/file-upload.component.html +21 -0
- package/src/lib/shared/components/file-upload/file-upload.component.scss +5 -0
- package/src/lib/shared/components/file-upload/file-upload.component.spec.ts +25 -0
- package/src/lib/shared/components/file-upload/file-upload.component.ts +43 -0
- package/src/lib/shared/components/icon/icon.component.html +17 -0
- package/src/lib/shared/components/icon/icon.component.scss +9 -0
- package/src/lib/shared/components/icon/icon.component.spec.ts +22 -0
- package/src/lib/shared/components/icon/icon.component.ts +17 -0
- package/src/lib/shared/components/input/input.component.html +38 -0
- package/src/lib/shared/components/input/input.component.scss +31 -0
- package/src/lib/shared/components/input/input.component.spec.ts +62 -0
- package/src/lib/shared/components/input/input.component.ts +72 -0
- package/src/lib/shared/components/placeholder/placeholder.component.html +21 -0
- package/src/lib/shared/components/placeholder/placeholder.component.scss +30 -0
- package/src/lib/shared/components/placeholder/placeholder.component.spec.ts +24 -0
- package/src/lib/shared/components/placeholder/placeholder.component.ts +16 -0
- package/src/lib/shared/modules/material.module.ts +77 -0
- package/src/lib/shared/shared.module.ts +18 -0
- package/src/public-api.ts +16 -0
- package/styles/_classes.scss +34 -0
- package/styles/_theme.scss +97 -0
- package/styles/_variables.scss +50 -0
- package/styles/fonts/icons/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 +0 -0
- package/styles/fonts/icons/gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUce.woff2 +0 -0
- package/styles/fonts/material-icons.scss +48 -0
- package/styles/fonts/roboto/roboto-v30-latin-100.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-100.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-100italic.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-100italic.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-300.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-300.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-300italic.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-300italic.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-500.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-500.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-500italic.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-500italic.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-700.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-700.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-700italic.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-700italic.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-italic.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-italic.woff2 +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-regular.woff +0 -0
- package/styles/fonts/roboto/roboto-v30-latin-regular.woff2 +0 -0
- package/styles/fonts/roboto-font.scss +109 -0
- package/styles/style.scss +54 -0
- package/tsconfig.lib.json +14 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +14 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
import { DOCUMENT, inject, Injectable } from '@angular/core';
|
|
3
|
+
import { Meta, Title } from '@angular/platform-browser';
|
|
4
|
+
import { SeoConfig } from '../models';
|
|
5
|
+
import { Logger } from './logger.service';
|
|
6
|
+
|
|
7
|
+
@Injectable({
|
|
8
|
+
providedIn: 'root'
|
|
9
|
+
})
|
|
10
|
+
export class SeoService {
|
|
11
|
+
private metaTitle = inject(Title);
|
|
12
|
+
private metaTags = inject(Meta);
|
|
13
|
+
private document = inject<Document>(DOCUMENT);
|
|
14
|
+
private logger = inject(Logger);
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pass a configuration to set meta tags such as title, description and image for search results and social media
|
|
19
|
+
*
|
|
20
|
+
* @param seoConfig config that contains all the meta information
|
|
21
|
+
*
|
|
22
|
+
*/
|
|
23
|
+
public setMetaTags(seoConfig: SeoConfig): void {
|
|
24
|
+
const autoTrim: boolean = seoConfig.autoTrim || false;
|
|
25
|
+
const title: string = seoConfig.metaPageTitle || '';
|
|
26
|
+
const description: string = seoConfig.metaPageDescription || '';
|
|
27
|
+
const imagePath: string = seoConfig.metaImagePath || '';
|
|
28
|
+
const titleLimit: number = 60;
|
|
29
|
+
const descriptionLimit: number = 160;
|
|
30
|
+
|
|
31
|
+
// Set meta title
|
|
32
|
+
if (title) {
|
|
33
|
+
if (autoTrim && title.length > titleLimit) {
|
|
34
|
+
this.logger.error(`[SeoService] The set meta title is too long. Recommended length is ${titleLimit}. The title will be trimmed.`);
|
|
35
|
+
}
|
|
36
|
+
const titleTrim: string = (title.length > titleLimit) ? title.substring(0, titleLimit - 3) + '...' : title;
|
|
37
|
+
this._setMetaTitle(autoTrim ? titleTrim : title);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set meta description
|
|
41
|
+
if (description) {
|
|
42
|
+
if (autoTrim && description.length > descriptionLimit) {
|
|
43
|
+
this.logger.error(`[SeoService] The set meta description is too long. Recommended length is ${descriptionLimit}. The description will be trimmed.`);
|
|
44
|
+
}
|
|
45
|
+
const descriptionTrim: string = (description.length > descriptionLimit) ? description.substring(0, descriptionLimit - 3) + '...' : description;
|
|
46
|
+
this._setMetaDescription(autoTrim ? descriptionTrim : description);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Set meta image
|
|
50
|
+
const host: string = this.document.location.origin;
|
|
51
|
+
if (imagePath) {
|
|
52
|
+
this._setMetaImage(`${host}/${imagePath}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Set all meta titles
|
|
57
|
+
private _setMetaTitle(title: string): void {
|
|
58
|
+
this.metaTitle.setTitle(title);
|
|
59
|
+
this.metaTags.updateTag({ property: 'og:title', content: title });
|
|
60
|
+
this.metaTags.updateTag({ name: 'twitter:title', content: title });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Set all meta descriptions
|
|
64
|
+
private _setMetaDescription(description: string): void {
|
|
65
|
+
this.metaTags.updateTag({ name: 'description', content: description });
|
|
66
|
+
this.metaTags.updateTag({ property: 'og:description', content: description });
|
|
67
|
+
this.metaTags.updateTag({ name: 'twitter:description', content: description });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Set all meta images
|
|
71
|
+
private _setMetaImage(image: string): void {
|
|
72
|
+
this.metaTags.updateTag({ property: 'og:image', content: image });
|
|
73
|
+
this.metaTags.updateTag({ name: 'twitter:image', content: image });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { SharedModule } from '../shared/shared.module';
|
|
3
|
+
import { SnackbarService } from './snackbar.service';
|
|
4
|
+
|
|
5
|
+
describe('SnackbarService', () => {
|
|
6
|
+
let service: SnackbarService;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
TestBed.configureTestingModule({
|
|
10
|
+
imports: [SharedModule]
|
|
11
|
+
});
|
|
12
|
+
service = TestBed.inject(SnackbarService);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should be created', () => {
|
|
16
|
+
expect(service).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Injectable, inject } from '@angular/core';
|
|
2
|
+
import { MatSnackBar, MatSnackBarConfig, MatSnackBarHorizontalPosition, MatSnackBarVerticalPosition } from '@angular/material/snack-bar';
|
|
3
|
+
import { firstValueFrom } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
@Injectable({
|
|
6
|
+
providedIn: 'root'
|
|
7
|
+
})
|
|
8
|
+
export class SnackbarService {
|
|
9
|
+
private snackbar = inject(MatSnackBar);
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
private readonly SNACKBAR_DURATION: number = 5000;
|
|
13
|
+
private readonly SNACKBAR_POSITION_HORIZONTAL: MatSnackBarHorizontalPosition = 'center';
|
|
14
|
+
private readonly SNACKBAR_POSITION_VERTICAL: MatSnackBarVerticalPosition = 'bottom';
|
|
15
|
+
|
|
16
|
+
private readonly actionConfig: MatSnackBarConfig = {
|
|
17
|
+
horizontalPosition: this.SNACKBAR_POSITION_HORIZONTAL,
|
|
18
|
+
verticalPosition: this.SNACKBAR_POSITION_VERTICAL
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
private readonly defaultConfig: MatSnackBarConfig = {
|
|
22
|
+
duration: this.SNACKBAR_DURATION,
|
|
23
|
+
horizontalPosition: this.SNACKBAR_POSITION_HORIZONTAL,
|
|
24
|
+
verticalPosition: this.SNACKBAR_POSITION_VERTICAL
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Opens a snackbar with an action to wait for
|
|
28
|
+
public openSnackbarWithAction(message: string, action: string, config?: MatSnackBarConfig): Promise<void> {
|
|
29
|
+
const snackbarRef = this.snackbar.open(message, action, config ? config : this.actionConfig);
|
|
30
|
+
return firstValueFrom(snackbarRef.onAction());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Opens a generic snackbar with a message
|
|
34
|
+
public openSnackbar(message: string, config?: MatSnackBarConfig): void {
|
|
35
|
+
this.snackbar.open(message, '', config ? config : this.defaultConfig);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { ThemeService } from './theme.service';
|
|
3
|
+
|
|
4
|
+
describe('ThemeService', () => {
|
|
5
|
+
let service: ThemeService;
|
|
6
|
+
// let mockDocument: any;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
TestBed.configureTestingModule({
|
|
10
|
+
// providers: [
|
|
11
|
+
// { provide: DOCUMENT, useValue: mockDocument }
|
|
12
|
+
// ]
|
|
13
|
+
});
|
|
14
|
+
service = TestBed.inject(ThemeService);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should be created', () => {
|
|
18
|
+
expect(service).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
import { Injectable, DOCUMENT, inject } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject, Observable } from 'rxjs';
|
|
4
|
+
import { LocalStorageService } from './local-storage.service';
|
|
5
|
+
|
|
6
|
+
@Injectable({
|
|
7
|
+
providedIn: 'root'
|
|
8
|
+
})
|
|
9
|
+
export class ThemeService {
|
|
10
|
+
private storageService = inject(LocalStorageService);
|
|
11
|
+
private document = inject<Document>(DOCUMENT);
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
private readonly THEME_KEY: string = 'THEME';
|
|
15
|
+
|
|
16
|
+
private _currentTheme$: BehaviorSubject<string> = new BehaviorSubject<string>('');
|
|
17
|
+
|
|
18
|
+
get currentTheme$(): Observable<string> {
|
|
19
|
+
return this._currentTheme$.asObservable();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this.loadTheme();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Set one of the defined themes by passing its class name
|
|
28
|
+
*
|
|
29
|
+
* @param newTheme class name of the theme (pass empty string for default theme)
|
|
30
|
+
* @param useLocalStorage persist the current theme in the LocalStorage
|
|
31
|
+
*/
|
|
32
|
+
public setTheme(newTheme: string, useLocalStorage?: boolean): void {
|
|
33
|
+
const currentTheme: string = this._currentTheme$.value;
|
|
34
|
+
|
|
35
|
+
if (newTheme === currentTheme) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (currentTheme) {
|
|
40
|
+
this.document.body.classList.remove(currentTheme);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this._currentTheme$.next(newTheme);
|
|
44
|
+
|
|
45
|
+
if (newTheme) {
|
|
46
|
+
this.document.body.classList.add(newTheme);
|
|
47
|
+
|
|
48
|
+
if (useLocalStorage) {
|
|
49
|
+
this.storageService.setItem(this.THEME_KEY, JSON.stringify(newTheme));
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
if (useLocalStorage) {
|
|
53
|
+
this.storageService.removeItem(this.THEME_KEY);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Loads the current theme stored in the local storage if available
|
|
60
|
+
*
|
|
61
|
+
*/
|
|
62
|
+
private loadTheme(): void {
|
|
63
|
+
const theme: string | null = this.storageService.getItem(this.THEME_KEY);
|
|
64
|
+
|
|
65
|
+
if (!theme) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.setTheme(theme.replace(/"/g, ''));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@if (config) {
|
|
2
|
+
@if (config.title) {
|
|
3
|
+
<h2 mat-dialog-title>{{ config.title }}</h2>
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
<mat-dialog-content>
|
|
7
|
+
@if (config.message) {
|
|
8
|
+
<p>{{ config.message }}</p>
|
|
9
|
+
}
|
|
10
|
+
</mat-dialog-content>
|
|
11
|
+
|
|
12
|
+
<mat-dialog-actions align="end">
|
|
13
|
+
@if (config.closeLabel) {
|
|
14
|
+
<button mat-button [mat-dialog-close]="false">
|
|
15
|
+
{{ config.closeLabel }}
|
|
16
|
+
</button>
|
|
17
|
+
}
|
|
18
|
+
@if (config.confirmLabel) {
|
|
19
|
+
<button mat-button color="primary" [mat-dialog-close]="true">
|
|
20
|
+
{{ config.confirmLabel }}
|
|
21
|
+
</button>
|
|
22
|
+
}
|
|
23
|
+
</mat-dialog-actions>
|
|
24
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
3
|
+
import { By } from '@angular/platform-browser';
|
|
4
|
+
import { SharedModule } from '../../../shared.module';
|
|
5
|
+
import { ConfirmDialogComponent } from './confirm-dialog.component';
|
|
6
|
+
|
|
7
|
+
describe('ConfirmDialogComponent', () => {
|
|
8
|
+
let component: ConfirmDialogComponent;
|
|
9
|
+
let fixture: ComponentFixture<ConfirmDialogComponent>;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
await TestBed.configureTestingModule({
|
|
13
|
+
declarations: [ConfirmDialogComponent],
|
|
14
|
+
imports: [SharedModule],
|
|
15
|
+
providers: [
|
|
16
|
+
{ provide: MAT_DIALOG_DATA, useValue: {} }
|
|
17
|
+
]
|
|
18
|
+
})
|
|
19
|
+
.compileComponents();
|
|
20
|
+
|
|
21
|
+
fixture = TestBed.createComponent(ConfirmDialogComponent);
|
|
22
|
+
component = fixture.componentInstance;
|
|
23
|
+
fixture.detectChanges();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should create', () => {
|
|
27
|
+
expect(component).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should display the title if provided', () => {
|
|
31
|
+
component.config = { title: 'Test Title' };
|
|
32
|
+
fixture.detectChanges();
|
|
33
|
+
const titleEl = fixture.debugElement.query(By.css('h2'));
|
|
34
|
+
expect(titleEl.nativeElement.textContent.trim()).toEqual('Test Title');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should not display the title if not provided', () => {
|
|
38
|
+
component.config = {};
|
|
39
|
+
fixture.detectChanges();
|
|
40
|
+
const titleEl = fixture.debugElement.query(By.css('h2'));
|
|
41
|
+
expect(titleEl).toBeFalsy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should display the message if provided', () => {
|
|
45
|
+
component.config = { message: 'Test Message' };
|
|
46
|
+
fixture.detectChanges();
|
|
47
|
+
const messageEl = fixture.debugElement.query(By.css('mat-dialog-content p'));
|
|
48
|
+
expect(messageEl.nativeElement.textContent.trim()).toEqual('Test Message');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should not display the message if not provided', () => {
|
|
52
|
+
component.config = {};
|
|
53
|
+
fixture.detectChanges();
|
|
54
|
+
const messageEl = fixture.debugElement.query(By.css('mat-dialog-content p'));
|
|
55
|
+
expect(messageEl).toBeFalsy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should display the close button label if provided', () => {
|
|
59
|
+
component.config = { closeLabel: 'Close' };
|
|
60
|
+
fixture.detectChanges();
|
|
61
|
+
const closeButtonEl = fixture.debugElement.query(By.css('mat-dialog-actions button:first-child'));
|
|
62
|
+
expect(closeButtonEl.nativeElement.textContent.trim()).toEqual('Close');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not display the close button if not provided', () => {
|
|
66
|
+
component.config = {};
|
|
67
|
+
fixture.detectChanges();
|
|
68
|
+
const closeButtonEl = fixture.debugElement.query(By.css('mat-dialog-actions button:first-child'));
|
|
69
|
+
expect(closeButtonEl).toBeFalsy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should display the confirm button label if provided', () => {
|
|
73
|
+
component.config = { confirmLabel: 'Confirm' };
|
|
74
|
+
fixture.detectChanges();
|
|
75
|
+
const confirmButtonEl = fixture.debugElement.query(By.css('mat-dialog-actions button:last-child'));
|
|
76
|
+
expect(confirmButtonEl.nativeElement.textContent.trim()).toEqual('Confirm');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should not display the confirm button if not provided', () => {
|
|
80
|
+
component.config = {};
|
|
81
|
+
fixture.detectChanges();
|
|
82
|
+
const confirmButtonEl = fixture.debugElement.query(By.css('mat-dialog-actions button:last-child'));
|
|
83
|
+
expect(confirmButtonEl).toBeFalsy();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Component, inject } from '@angular/core';
|
|
2
|
+
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
3
|
+
import { ConfirmDialogConfig } from '../../../../models';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'lf-confirm-dialog',
|
|
7
|
+
templateUrl: './confirm-dialog.component.html',
|
|
8
|
+
styleUrls: ['./confirm-dialog.component.scss'],
|
|
9
|
+
standalone: false
|
|
10
|
+
})
|
|
11
|
+
export class ConfirmDialogComponent {
|
|
12
|
+
public config = inject<ConfirmDialogConfig>(MAT_DIALOG_DATA);
|
|
13
|
+
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<input
|
|
2
|
+
hidden
|
|
3
|
+
#file
|
|
4
|
+
type="file"
|
|
5
|
+
[accept]="accept"
|
|
6
|
+
onclick="this.value=null"
|
|
7
|
+
(change)="selectFile($event)" />
|
|
8
|
+
|
|
9
|
+
<button
|
|
10
|
+
mat-flat-button
|
|
11
|
+
class="lf-button"
|
|
12
|
+
type="button"
|
|
13
|
+
[color]="color"
|
|
14
|
+
[disabled]="disabled"
|
|
15
|
+
(click)="triggerInput()"
|
|
16
|
+
[matTooltip]="tooltip">
|
|
17
|
+
@if (matIcon) {
|
|
18
|
+
<mat-icon>{{ matIcon }}</mat-icon>
|
|
19
|
+
}
|
|
20
|
+
{{ label }}
|
|
21
|
+
</button>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { SharedModule } from '../../shared.module';
|
|
3
|
+
import { FileUploadComponent } from './file-upload.component';
|
|
4
|
+
|
|
5
|
+
describe('FileUploadComponent', () => {
|
|
6
|
+
let component: FileUploadComponent;
|
|
7
|
+
let fixture: ComponentFixture<FileUploadComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [
|
|
12
|
+
FileUploadComponent,
|
|
13
|
+
SharedModule
|
|
14
|
+
]
|
|
15
|
+
}).compileComponents();
|
|
16
|
+
|
|
17
|
+
fixture = TestBed.createComponent(FileUploadComponent);
|
|
18
|
+
component = fixture.componentInstance;
|
|
19
|
+
fixture.detectChanges();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should create', () => {
|
|
23
|
+
expect(component).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Component, ElementRef, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core';
|
|
2
|
+
import { Logger } from '../../../services';
|
|
3
|
+
import { SharedModule } from '../../shared.module';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'lf-file-upload',
|
|
7
|
+
templateUrl: './file-upload.component.html',
|
|
8
|
+
styleUrls: ['./file-upload.component.scss'],
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [SharedModule]
|
|
11
|
+
})
|
|
12
|
+
export class FileUploadComponent {
|
|
13
|
+
|
|
14
|
+
private logger: Logger = inject(Logger);
|
|
15
|
+
|
|
16
|
+
@ViewChild('file') public fileElement: ElementRef;
|
|
17
|
+
|
|
18
|
+
@Input() public color: 'primary' | 'accent' | 'warn' = 'primary';
|
|
19
|
+
@Input() public label: string;
|
|
20
|
+
@Input() public matIcon: string;
|
|
21
|
+
@Input() public disabled: boolean = false;
|
|
22
|
+
@Input() public accept: string;
|
|
23
|
+
@Input() public tooltip: string;
|
|
24
|
+
|
|
25
|
+
@Output() public fileChange: EventEmitter<File> = new EventEmitter<File>();
|
|
26
|
+
|
|
27
|
+
public selectFile(event: Event): void {
|
|
28
|
+
const input: HTMLInputElement = event.target as HTMLInputElement;
|
|
29
|
+
|
|
30
|
+
if (!input || !input.files) return;
|
|
31
|
+
const file: File = input.files[0];
|
|
32
|
+
|
|
33
|
+
if (!file) return;
|
|
34
|
+
|
|
35
|
+
this.logger.log('[FileUploadComponent]', file);
|
|
36
|
+
this.fileChange.emit(file);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public triggerInput(): void {
|
|
40
|
+
this.fileElement.nativeElement.click();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- mat icon -->
|
|
2
|
+
@if (matIcon) {
|
|
3
|
+
<mat-icon
|
|
4
|
+
class="lf-icon"
|
|
5
|
+
[class.material-icons-outlined]="outlineIcon"
|
|
6
|
+
[class.lf-icon-align-middle]="alignMiddle">
|
|
7
|
+
{{ matIcon }}
|
|
8
|
+
</mat-icon>
|
|
9
|
+
} @else {
|
|
10
|
+
@if (svgIcon) {
|
|
11
|
+
<mat-icon
|
|
12
|
+
class="lf-icon"
|
|
13
|
+
[svgIcon]="svgIcon"
|
|
14
|
+
[class.lf-icon-align-middle]="alignMiddle"></mat-icon>
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
<!-- svg icon -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { SharedModule } from '../../shared.module';
|
|
3
|
+
import { IconComponent } from './icon.component';
|
|
4
|
+
|
|
5
|
+
describe('IconComponent', () => {
|
|
6
|
+
let component: IconComponent;
|
|
7
|
+
let fixture: ComponentFixture<IconComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [SharedModule]
|
|
12
|
+
}).compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(IconComponent);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
fixture.detectChanges();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Component, Input } from '@angular/core';
|
|
2
|
+
import { SharedModule } from '../../shared.module';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'lf-icon',
|
|
6
|
+
templateUrl: './icon.component.html',
|
|
7
|
+
styleUrls: ['./icon.component.scss'],
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [SharedModule]
|
|
10
|
+
})
|
|
11
|
+
export class IconComponent {
|
|
12
|
+
|
|
13
|
+
@Input() public matIcon: string;
|
|
14
|
+
@Input() public svgIcon: string;
|
|
15
|
+
@Input() public outlineIcon: boolean = false;
|
|
16
|
+
@Input() public alignMiddle: boolean = false;
|
|
17
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@if (inputConfig) {
|
|
2
|
+
<mat-form-field class="lf-input" appearance="fill" [class.lf-hide-hint]="!inputConfig.hint">
|
|
3
|
+
<mat-label>{{ inputConfig.label || "Search" }}</mat-label>
|
|
4
|
+
@if (inputConfig.hint) {
|
|
5
|
+
<mat-hint>{{ inputConfig.hint }}</mat-hint>
|
|
6
|
+
}
|
|
7
|
+
<input
|
|
8
|
+
matInput
|
|
9
|
+
type="text"
|
|
10
|
+
[(ngModel)]="inputValue"
|
|
11
|
+
[disabled]="inputConfig.disabled!"
|
|
12
|
+
(keyup.enter)="inputSubmitted(inputValue)"
|
|
13
|
+
(ngModelChange)="inputChanged($event)"
|
|
14
|
+
#input />
|
|
15
|
+
@if (isMobile) {
|
|
16
|
+
<button mat-icon-button matPrefix color="accent" (click)="inputPrefixAction()">
|
|
17
|
+
<mat-icon>arrow_back</mat-icon>
|
|
18
|
+
</button>
|
|
19
|
+
}
|
|
20
|
+
@if (inputValue) {
|
|
21
|
+
<button class="lf-input-close-button" mat-icon-button matSuffix (click)="clearInput()">
|
|
22
|
+
<mat-icon>close</mat-icon>
|
|
23
|
+
</button>
|
|
24
|
+
}
|
|
25
|
+
@if (inputConfig.matIconSubmit) {
|
|
26
|
+
<button
|
|
27
|
+
mat-icon-button
|
|
28
|
+
matSuffix
|
|
29
|
+
color="accent"
|
|
30
|
+
type="submit"
|
|
31
|
+
(click)="inputSubmitted(inputValue)">
|
|
32
|
+
<mat-icon>
|
|
33
|
+
{{ inputConfig.matIconSubmit }}
|
|
34
|
+
</mat-icon>
|
|
35
|
+
</button>
|
|
36
|
+
}
|
|
37
|
+
</mat-form-field>
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@use '../../../../../styles/variables' as *;
|
|
2
|
+
|
|
3
|
+
.lf-input {
|
|
4
|
+
width: 100%;
|
|
5
|
+
|
|
6
|
+
::ng-deep .mdc-text-field {
|
|
7
|
+
border-radius: $border-radius !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
::ng-deep .mdc-line-ripple {
|
|
11
|
+
&::after {
|
|
12
|
+
border-bottom-width: 0 !important;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&::before {
|
|
16
|
+
border-bottom-width: 0 !important;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.lf-input-close-button {
|
|
21
|
+
color: $color-gray;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&.lf-hide-hint{
|
|
25
|
+
::ng-deep {
|
|
26
|
+
.mat-mdc-form-field-subscript-wrapper {
|
|
27
|
+
display: none !important;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { By } from '@angular/platform-browser';
|
|
3
|
+
import { SharedModule } from '../../shared.module';
|
|
4
|
+
import { InputComponent } from './input.component';
|
|
5
|
+
|
|
6
|
+
describe('InputComponent', () => {
|
|
7
|
+
let component: InputComponent;
|
|
8
|
+
let fixture: ComponentFixture<InputComponent>;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await TestBed.configureTestingModule({
|
|
12
|
+
imports: [SharedModule]
|
|
13
|
+
})
|
|
14
|
+
.compileComponents();
|
|
15
|
+
|
|
16
|
+
fixture = TestBed.createComponent(InputComponent);
|
|
17
|
+
component = fixture.componentInstance;
|
|
18
|
+
fixture.detectChanges();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should create', () => {
|
|
22
|
+
expect(component).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should set the label to "Search" if no label is provided', () => {
|
|
26
|
+
const labelElement = fixture.debugElement.query(By.css('mat-label')).nativeElement;
|
|
27
|
+
expect(labelElement.textContent.trim()).toBe('Search');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should set the label to the provided value if a label is provided', () => {
|
|
31
|
+
component.inputConfig.label = 'Test';
|
|
32
|
+
fixture.detectChanges();
|
|
33
|
+
const labelElement = fixture.debugElement.query(By.css('mat-label')).nativeElement;
|
|
34
|
+
expect(labelElement.textContent.trim()).toBe('Test');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should display the hint if a hint is provided', () => {
|
|
38
|
+
component.inputConfig.hint = 'Test hint';
|
|
39
|
+
fixture.detectChanges();
|
|
40
|
+
const hintElement = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
|
|
41
|
+
expect(hintElement.textContent.trim()).toBe('Test hint');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should hide the hint if no hint is provided', () => {
|
|
45
|
+
const hintElement = fixture.debugElement.query(By.css('mat-hint'));
|
|
46
|
+
expect(hintElement).toBeFalsy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should enable the input if not disabled', () => {
|
|
50
|
+
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
|
|
51
|
+
expect(inputElement.disabled).toBeFalsy();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should call inputChanged when input value is changed', () => {
|
|
55
|
+
spyOn(component, 'inputChanged');
|
|
56
|
+
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
|
|
57
|
+
inputElement.value = 'test';
|
|
58
|
+
inputElement.dispatchEvent(new Event('input'));
|
|
59
|
+
fixture.detectChanges();
|
|
60
|
+
expect(component.inputChanged).toHaveBeenCalledWith('test');
|
|
61
|
+
});
|
|
62
|
+
});
|