@sneat/core 0.1.0
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/eslint.config.js +7 -0
- package/ng-package.json +7 -0
- package/package.json +14 -0
- package/project.json +38 -0
- package/src/index.ts +1 -0
- package/src/lib/analytics.interface.ts +34 -0
- package/src/lib/animations/form-animations.spec.ts +26 -0
- package/src/lib/animations/form-animations.ts +11 -0
- package/src/lib/animations/index.ts +2 -0
- package/src/lib/animations/list-animations.spec.ts +50 -0
- package/src/lib/animations/list-animations.ts +44 -0
- package/src/lib/app.service.ts +33 -0
- package/src/lib/constants.spec.ts +20 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/core-models.ts +12 -0
- package/src/lib/directives/index.ts +1 -0
- package/src/lib/directives/sneat-select-all-on-focus.directive.spec.ts +142 -0
- package/src/lib/directives/sneat-select-all-on-focus.directive.ts +36 -0
- package/src/lib/environment-config.ts +54 -0
- package/src/lib/eq.spec.ts +24 -0
- package/src/lib/eq.ts +1 -0
- package/src/lib/exclude-undefined.spec.ts +165 -0
- package/src/lib/exclude-undefined.ts +47 -0
- package/src/lib/form-field.ts +5 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/interfaces.spec.ts +116 -0
- package/src/lib/interfaces.ts +85 -0
- package/src/lib/location-href.spec.ts +53 -0
- package/src/lib/location-href.ts +9 -0
- package/src/lib/logging/interfaces.ts +19 -0
- package/src/lib/logging.spec.ts +132 -0
- package/src/lib/logging.ts +33 -0
- package/src/lib/nav/index.ts +2 -0
- package/src/lib/nav/nav-context.ts +16 -0
- package/src/lib/nav/routing-state.spec.ts +65 -0
- package/src/lib/nav/routing-state.ts +26 -0
- package/src/lib/services/index.ts +3 -0
- package/src/lib/services/ng-module-preloader.service.spec.ts +72 -0
- package/src/lib/services/ng-module-preloader.service.ts +125 -0
- package/src/lib/services/sneat-nav.service.spec.ts +95 -0
- package/src/lib/services/sneat-nav.service.ts +46 -0
- package/src/lib/services/top-menu.service.spec.ts +42 -0
- package/src/lib/services/top-menu.service.ts +19 -0
- package/src/lib/sneat-enum-keys.ts +2 -0
- package/src/lib/sneat-extensions.spec.ts +127 -0
- package/src/lib/sneat-extensions.ts +49 -0
- package/src/lib/store.spec.ts +156 -0
- package/src/lib/store.ts +54 -0
- package/src/lib/team-type.spec.ts +8 -0
- package/src/lib/team-type.ts +13 -0
- package/src/lib/testing/base-test-setup.ts +247 -0
- package/src/lib/testing/test-setup-light.ts +1 -0
- package/src/lib/testing/test-setup.ts +70 -0
- package/src/lib/types/age-group.ts +1 -0
- package/src/lib/types/gender.spec.ts +42 -0
- package/src/lib/types/gender.ts +12 -0
- package/src/lib/types/index.ts +2 -0
- package/src/lib/utils/datetimes.spec.ts +144 -0
- package/src/lib/utils/datetimes.ts +51 -0
- package/src/lib/utils/index.ts +1 -0
- package/src/test-setup.ts +3 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.lib.prod.json +7 -0
- package/tsconfig.spec.json +31 -0
- package/vite.config.mts +10 -0
package/eslint.config.js
ADDED
package/ng-package.json
ADDED
package/package.json
ADDED
package/project.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "core",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"projectType": "library",
|
|
5
|
+
"sourceRoot": "libs/core/src",
|
|
6
|
+
"prefix": "sneat",
|
|
7
|
+
"targets": {
|
|
8
|
+
"build": {
|
|
9
|
+
"executor": "@nx/angular:ng-packagr-lite",
|
|
10
|
+
"outputs": [
|
|
11
|
+
"{workspaceRoot}/dist/libs/core"
|
|
12
|
+
],
|
|
13
|
+
"options": {
|
|
14
|
+
"project": "libs/core/ng-package.json",
|
|
15
|
+
"tsConfig": "libs/core/tsconfig.lib.json"
|
|
16
|
+
},
|
|
17
|
+
"configurations": {
|
|
18
|
+
"production": {
|
|
19
|
+
"tsConfig": "libs/core/tsconfig.lib.prod.json"
|
|
20
|
+
},
|
|
21
|
+
"development": {}
|
|
22
|
+
},
|
|
23
|
+
"defaultConfiguration": "production"
|
|
24
|
+
},
|
|
25
|
+
"test": {
|
|
26
|
+
"executor": "@nx/vitest:test",
|
|
27
|
+
"outputs": [
|
|
28
|
+
"{workspaceRoot}/coverage/libs/core"
|
|
29
|
+
],
|
|
30
|
+
"options": {
|
|
31
|
+
"tsConfig": "libs/core/tsconfig.spec.json"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"lint": {
|
|
35
|
+
"executor": "@nx/eslint:lint"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export interface IAnalyticsCallOptions {
|
|
4
|
+
// If true, this config or event call applies globally to all analytics properties on the page.
|
|
5
|
+
// This is from Firebase Analytics https://firebase.google.com/docs/reference/js/analytics.md#logevent_d5f1743
|
|
6
|
+
global: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type UserProperties = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* An interface for analytics service
|
|
13
|
+
*/
|
|
14
|
+
export interface IAnalyticsService {
|
|
15
|
+
logEvent(
|
|
16
|
+
eventName: string,
|
|
17
|
+
eventParams?: Readonly<Record<string, unknown>>,
|
|
18
|
+
options?: IAnalyticsCallOptions,
|
|
19
|
+
): void;
|
|
20
|
+
|
|
21
|
+
identify(
|
|
22
|
+
userID: string,
|
|
23
|
+
userPropertiesToSet?: UserProperties,
|
|
24
|
+
userPropertiesToSetOnce?: UserProperties,
|
|
25
|
+
): void;
|
|
26
|
+
|
|
27
|
+
loggedOut(): void;
|
|
28
|
+
|
|
29
|
+
setCurrentScreen(screenName: string, options?: IAnalyticsCallOptions): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const AnalyticsService = new InjectionToken<IAnalyticsService>(
|
|
33
|
+
'IAnalyticsService',
|
|
34
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formNexInAnimation } from './form-animations';
|
|
3
|
+
|
|
4
|
+
describe('form-animations', () => {
|
|
5
|
+
describe('formNexInAnimation', () => {
|
|
6
|
+
it('should be defined', () => {
|
|
7
|
+
expect(formNexInAnimation).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should have trigger name "formNextIn"', () => {
|
|
11
|
+
// Angular animation triggers have a name property
|
|
12
|
+
// @ts-expect-error - accessing internal property for testing
|
|
13
|
+
expect(formNexInAnimation.name).toBe('formNextIn');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should have definitions array', () => {
|
|
17
|
+
// @ts-expect-error - accessing internal property for testing
|
|
18
|
+
expect(Array.isArray(formNexInAnimation.definitions)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should have at least one definition', () => {
|
|
22
|
+
// @ts-expect-error - accessing internal property for testing
|
|
23
|
+
expect(formNexInAnimation.definitions.length).toBeGreaterThan(0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { animate, style, transition, trigger } from '@angular/animations';
|
|
2
|
+
|
|
3
|
+
export const formNexInAnimation = trigger('formNextIn', [
|
|
4
|
+
transition(':enter', [
|
|
5
|
+
style({ opacity: 0 }), // initial styles
|
|
6
|
+
animate(
|
|
7
|
+
'250ms',
|
|
8
|
+
style({ opacity: 1 }), // final style after the transition has finished
|
|
9
|
+
),
|
|
10
|
+
]),
|
|
11
|
+
]);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { listItemAnimations, listAddRemoveAnimation } from './list-animations';
|
|
3
|
+
|
|
4
|
+
describe('list-animations', () => {
|
|
5
|
+
describe('listItemAnimations', () => {
|
|
6
|
+
it('should be defined', () => {
|
|
7
|
+
expect(listItemAnimations).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should have trigger name "listItem"', () => {
|
|
11
|
+
// Angular animation triggers have a name property
|
|
12
|
+
// @ts-expect-error - accessing internal property for testing
|
|
13
|
+
expect(listItemAnimations.name).toBe('listItem');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should have definitions array', () => {
|
|
17
|
+
// @ts-expect-error - accessing internal property for testing
|
|
18
|
+
expect(Array.isArray(listItemAnimations.definitions)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should have multiple definitions for add and remove', () => {
|
|
22
|
+
// @ts-expect-error - accessing internal property for testing
|
|
23
|
+
expect(listItemAnimations.definitions.length).toBeGreaterThan(0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('listAddRemoveAnimation', () => {
|
|
28
|
+
it('should be defined', () => {
|
|
29
|
+
expect(listAddRemoveAnimation).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should be an array', () => {
|
|
33
|
+
expect(Array.isArray(listAddRemoveAnimation)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should have at least one animation trigger', () => {
|
|
37
|
+
expect(listAddRemoveAnimation.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should have trigger name "addRemove"', () => {
|
|
41
|
+
// @ts-expect-error - accessing internal property for testing
|
|
42
|
+
expect(listAddRemoveAnimation[0].name).toBe('addRemove');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should have definitions array in the first trigger', () => {
|
|
46
|
+
// @ts-expect-error - accessing internal property for testing
|
|
47
|
+
expect(Array.isArray(listAddRemoveAnimation[0].definitions)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
animate,
|
|
3
|
+
AnimationTriggerMetadata,
|
|
4
|
+
style,
|
|
5
|
+
transition,
|
|
6
|
+
trigger,
|
|
7
|
+
} from '@angular/animations';
|
|
8
|
+
|
|
9
|
+
const removeListItemAnimation = transition('* => void', [
|
|
10
|
+
animate(
|
|
11
|
+
'0.2s ease-in-out',
|
|
12
|
+
style({
|
|
13
|
+
height: 0,
|
|
14
|
+
opacity: 0,
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const addListItemAnimation = transition('void => added', [
|
|
20
|
+
style({
|
|
21
|
+
height: 0,
|
|
22
|
+
opacity: 0,
|
|
23
|
+
}),
|
|
24
|
+
animate(
|
|
25
|
+
'0.5s ease-in',
|
|
26
|
+
style({
|
|
27
|
+
height: '*',
|
|
28
|
+
opacity: 1,
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// TODO: How it is different from listAddRemoveAnimation ?
|
|
34
|
+
export const listItemAnimations = trigger('listItem', [
|
|
35
|
+
removeListItemAnimation,
|
|
36
|
+
addListItemAnimation,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// TODO: How it is different from listItemAnimations ?
|
|
40
|
+
export const listAddRemoveAnimation: AnimationTriggerMetadata[] = [
|
|
41
|
+
trigger('addRemove', [
|
|
42
|
+
transition(':leave', [animate('0.2s ease-in-out', style({ height: 0 }))]),
|
|
43
|
+
]),
|
|
44
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
import { SpaceType } from './team-type';
|
|
3
|
+
|
|
4
|
+
export type SneatApp =
|
|
5
|
+
| 'sneat'
|
|
6
|
+
| 'aaproject'
|
|
7
|
+
| 'agendum'
|
|
8
|
+
| 'class'
|
|
9
|
+
| 'contactus'
|
|
10
|
+
| 'creche'
|
|
11
|
+
| 'debtus'
|
|
12
|
+
| 'docus'
|
|
13
|
+
| 'dream7'
|
|
14
|
+
| 'feis'
|
|
15
|
+
| 'logist'
|
|
16
|
+
| 'listus'
|
|
17
|
+
| 'neighbours'
|
|
18
|
+
| 'parish'
|
|
19
|
+
| 'renterra'
|
|
20
|
+
| 'rsvp'
|
|
21
|
+
| 'sizechart'
|
|
22
|
+
| 'splitus'
|
|
23
|
+
| 'sportclubs'
|
|
24
|
+
| 'tournament'
|
|
25
|
+
| 'datatug';
|
|
26
|
+
|
|
27
|
+
export interface IAppInfo {
|
|
28
|
+
readonly appId: SneatApp;
|
|
29
|
+
readonly appTitle: string;
|
|
30
|
+
readonly requiredSpaceType?: SpaceType;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const APP_INFO = new InjectionToken<IAppInfo>('app_info');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CurrencyList } from './constants';
|
|
3
|
+
|
|
4
|
+
describe('CurrencyList', () => {
|
|
5
|
+
it('should contain USD', () => {
|
|
6
|
+
expect(CurrencyList).toContain('USD');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should contain EUR', () => {
|
|
10
|
+
expect(CurrencyList).toContain('EUR');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should have exactly 2 currencies', () => {
|
|
14
|
+
expect(CurrencyList).toHaveLength(2);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should be an array', () => {
|
|
18
|
+
expect(Array.isArray(CurrencyList)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CurrencyList = ['USD', 'EUR'];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SneatSelectAllOnFocusDirective } from './sneat-select-all-on-focus.directive';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SneatSelectAllOnFocusDirective } from './sneat-select-all-on-focus.directive';
|
|
4
|
+
import { By } from '@angular/platform-browser';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
template: `
|
|
8
|
+
<input id="directInput" sneatSelectAllOnFocus value="test" />
|
|
9
|
+
<div id="container" sneatSelectAllOnFocus>
|
|
10
|
+
<input id="nestedInput" value="nested test" />
|
|
11
|
+
</div>
|
|
12
|
+
<textarea id="directTextarea" sneatSelectAllOnFocus>textarea test</textarea>
|
|
13
|
+
<div id="textareaContainer" sneatSelectAllOnFocus>
|
|
14
|
+
<textarea id="nestedTextarea">nested textarea</textarea>
|
|
15
|
+
</div>
|
|
16
|
+
`,
|
|
17
|
+
standalone: true,
|
|
18
|
+
imports: [SneatSelectAllOnFocusDirective],
|
|
19
|
+
})
|
|
20
|
+
class TestHostComponent {}
|
|
21
|
+
|
|
22
|
+
describe('SneatSelectAllOnFocusDirective', () => {
|
|
23
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
await TestBed.configureTestingModule({
|
|
27
|
+
imports: [TestHostComponent],
|
|
28
|
+
}).compileComponents();
|
|
29
|
+
|
|
30
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
31
|
+
fixture.detectChanges();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should create', () => {
|
|
35
|
+
expect(fixture.componentInstance).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should select text on focus of input element', () => {
|
|
39
|
+
const inputEl = fixture.debugElement.query(By.css('#directInput'));
|
|
40
|
+
const directive = inputEl.injector.get(SneatSelectAllOnFocusDirective);
|
|
41
|
+
const input = inputEl.nativeElement as HTMLInputElement;
|
|
42
|
+
input.setSelectionRange = vi.fn();
|
|
43
|
+
|
|
44
|
+
directive.selectAll();
|
|
45
|
+
|
|
46
|
+
expect(input.setSelectionRange).toHaveBeenCalledWith(0, input.value.length);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should select text on focus of container element', () => {
|
|
50
|
+
const containerEl = fixture.debugElement.query(By.css('#container'));
|
|
51
|
+
const directive = containerEl.injector.get(SneatSelectAllOnFocusDirective);
|
|
52
|
+
const nestedInput = containerEl.nativeElement.querySelector(
|
|
53
|
+
'input',
|
|
54
|
+
) as HTMLInputElement;
|
|
55
|
+
nestedInput.setSelectionRange = vi.fn();
|
|
56
|
+
|
|
57
|
+
directive.selectAll();
|
|
58
|
+
|
|
59
|
+
expect(nestedInput.setSelectionRange).toHaveBeenCalledWith(
|
|
60
|
+
0,
|
|
61
|
+
nestedInput.value.length,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle setSelectionRange failure and fallback to select()', () => {
|
|
66
|
+
const inputEl = fixture.debugElement.query(By.css('#directInput'));
|
|
67
|
+
const directive = inputEl.injector.get(SneatSelectAllOnFocusDirective);
|
|
68
|
+
const input = inputEl.nativeElement as HTMLInputElement;
|
|
69
|
+
const consoleWarnSpy = vi
|
|
70
|
+
.spyOn(console, 'warn')
|
|
71
|
+
.mockImplementation(() => undefined);
|
|
72
|
+
|
|
73
|
+
// Mock setSelectionRange to throw error
|
|
74
|
+
input.setSelectionRange = vi.fn().mockImplementation(() => {
|
|
75
|
+
throw new Error('setSelectionRange not supported');
|
|
76
|
+
});
|
|
77
|
+
input.select = vi.fn();
|
|
78
|
+
|
|
79
|
+
directive.selectAll();
|
|
80
|
+
|
|
81
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
82
|
+
'Element does not support setSelectionRange',
|
|
83
|
+
);
|
|
84
|
+
expect(input.select).toHaveBeenCalled();
|
|
85
|
+
consoleWarnSpy.mockRestore();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should select text on focus of textarea element', () => {
|
|
89
|
+
const textareaEl = fixture.debugElement.query(By.css('#directTextarea'));
|
|
90
|
+
const directive = textareaEl.injector.get(SneatSelectAllOnFocusDirective);
|
|
91
|
+
const textarea = textareaEl.nativeElement as HTMLTextAreaElement;
|
|
92
|
+
textarea.setSelectionRange = vi.fn();
|
|
93
|
+
|
|
94
|
+
directive.selectAll();
|
|
95
|
+
|
|
96
|
+
expect(textarea.setSelectionRange).toHaveBeenCalledWith(
|
|
97
|
+
0,
|
|
98
|
+
textarea.value.length,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should select text on focus of container with nested textarea', () => {
|
|
103
|
+
const containerEl = fixture.debugElement.query(
|
|
104
|
+
By.css('#textareaContainer'),
|
|
105
|
+
);
|
|
106
|
+
const directive = containerEl.injector.get(SneatSelectAllOnFocusDirective);
|
|
107
|
+
const nestedTextarea = containerEl.nativeElement.querySelector(
|
|
108
|
+
'textarea',
|
|
109
|
+
) as HTMLTextAreaElement;
|
|
110
|
+
nestedTextarea.setSelectionRange = vi.fn();
|
|
111
|
+
|
|
112
|
+
directive.selectAll();
|
|
113
|
+
|
|
114
|
+
expect(nestedTextarea.setSelectionRange).toHaveBeenCalledWith(
|
|
115
|
+
0,
|
|
116
|
+
nestedTextarea.value.length,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle textarea when setSelectionRange fails', () => {
|
|
121
|
+
const textareaEl = fixture.debugElement.query(By.css('#directTextarea'));
|
|
122
|
+
const directive = textareaEl.injector.get(SneatSelectAllOnFocusDirective);
|
|
123
|
+
const textarea = textareaEl.nativeElement as HTMLTextAreaElement;
|
|
124
|
+
const consoleWarnSpy = vi
|
|
125
|
+
.spyOn(console, 'warn')
|
|
126
|
+
.mockImplementation(() => undefined);
|
|
127
|
+
|
|
128
|
+
// Mock setSelectionRange to throw error
|
|
129
|
+
textarea.setSelectionRange = vi.fn().mockImplementation(() => {
|
|
130
|
+
throw new Error('setSelectionRange not supported');
|
|
131
|
+
});
|
|
132
|
+
textarea.select = vi.fn();
|
|
133
|
+
|
|
134
|
+
directive.selectAll();
|
|
135
|
+
|
|
136
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
137
|
+
'Element does not support setSelectionRange',
|
|
138
|
+
);
|
|
139
|
+
expect(textarea.select).toHaveBeenCalled();
|
|
140
|
+
consoleWarnSpy.mockRestore();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Directive, ElementRef, HostListener, inject } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[sneatSelectAllOnFocus]',
|
|
5
|
+
})
|
|
6
|
+
export class SneatSelectAllOnFocusDirective {
|
|
7
|
+
private readonly el = inject(ElementRef);
|
|
8
|
+
|
|
9
|
+
@HostListener('focus', ['$event.target'])
|
|
10
|
+
public selectAll(_target?: EventTarget | null) {
|
|
11
|
+
const nativeElement = this.el.nativeElement;
|
|
12
|
+
const input =
|
|
13
|
+
nativeElement instanceof HTMLInputElement ||
|
|
14
|
+
nativeElement instanceof HTMLTextAreaElement
|
|
15
|
+
? nativeElement
|
|
16
|
+
: nativeElement.querySelector('input') ||
|
|
17
|
+
nativeElement.querySelector('textarea');
|
|
18
|
+
|
|
19
|
+
if (input) {
|
|
20
|
+
if ('setSelectionRange' in input) {
|
|
21
|
+
try {
|
|
22
|
+
input.setSelectionRange(0, (input as HTMLInputElement).value.length);
|
|
23
|
+
return;
|
|
24
|
+
} catch {
|
|
25
|
+
console.warn('Element does not support setSelectionRange');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
input instanceof HTMLInputElement ||
|
|
30
|
+
input instanceof HTMLTextAreaElement
|
|
31
|
+
) {
|
|
32
|
+
input.select();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
import { BrowserOptions } from '@sentry/browser';
|
|
3
|
+
import { PostHogConfig } from 'posthog-js';
|
|
4
|
+
|
|
5
|
+
export interface IFirebaseEmulatorConfig {
|
|
6
|
+
authPort: number;
|
|
7
|
+
authHost?: string;
|
|
8
|
+
firestorePort: number;
|
|
9
|
+
firestoreHost?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IFirebaseConfig {
|
|
13
|
+
emulator?: IFirebaseEmulatorConfig;
|
|
14
|
+
//
|
|
15
|
+
projectId: string;
|
|
16
|
+
appId: string;
|
|
17
|
+
measurementId?: string;
|
|
18
|
+
messagingSenderId?: string;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
authDomain: string;
|
|
21
|
+
databaseURL?: string;
|
|
22
|
+
storageBucket?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type OnlyValidKeys<T, Shape> = T extends Shape
|
|
26
|
+
? Exclude<keyof T, keyof Shape> extends never
|
|
27
|
+
? T
|
|
28
|
+
: never
|
|
29
|
+
: never;
|
|
30
|
+
|
|
31
|
+
export interface IPosthogSettings {
|
|
32
|
+
readonly token: string;
|
|
33
|
+
readonly config?: OnlyValidKeys<
|
|
34
|
+
Partial<PostHogConfig>,
|
|
35
|
+
Partial<PostHogConfig>
|
|
36
|
+
>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IEnvironmentConfig {
|
|
40
|
+
production: boolean;
|
|
41
|
+
useNgrok?: boolean;
|
|
42
|
+
posthog?: IPosthogSettings;
|
|
43
|
+
sentry?: BrowserOptions;
|
|
44
|
+
agents: Record<string, string>;
|
|
45
|
+
firebaseConfig: IFirebaseConfig;
|
|
46
|
+
firebaseBaseUrl?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const FirebaseConfigToken = new InjectionToken<IFirebaseConfig>(
|
|
50
|
+
'firebaseConfig',
|
|
51
|
+
);
|
|
52
|
+
export const EnvConfigToken = new InjectionToken<IEnvironmentConfig>(
|
|
53
|
+
'envConfig',
|
|
54
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { eq } from './eq';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('eq', () => {
|
|
5
|
+
it('should return true if both are undefined', () => {
|
|
6
|
+
expect(eq(undefined, undefined)).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should return true if both are null', () => {
|
|
10
|
+
expect(eq(null as unknown, null as unknown)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return true if both are same string', () => {
|
|
14
|
+
expect(eq('a', 'a')).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return false if strings differ', () => {
|
|
18
|
+
expect(eq('a', 'b')).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should return true if one is null and other is undefined', () => {
|
|
22
|
+
expect(eq(null as unknown, undefined)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/lib/eq.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const eq = <T = string | number>(x?: T, y?: T) => (!x && !y) || x === y;
|