@sneat/ui 0.1.3 → 0.1.4

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.
Files changed (74) hide show
  1. package/esm2022/index.js +4 -0
  2. package/esm2022/index.js.map +1 -0
  3. package/esm2022/lib/components/index.js +3 -0
  4. package/esm2022/lib/components/index.js.map +1 -0
  5. package/esm2022/lib/components/sneat-base-modal.component.js +24 -0
  6. package/esm2022/lib/components/sneat-base-modal.component.js.map +1 -0
  7. package/esm2022/lib/components/sneat-base.component.js +58 -0
  8. package/esm2022/lib/components/sneat-base.component.js.map +1 -0
  9. package/esm2022/lib/focus.js +17 -0
  10. package/esm2022/lib/focus.js.map +1 -0
  11. package/esm2022/lib/selector/index.js +7 -0
  12. package/esm2022/lib/selector/index.js.map +1 -0
  13. package/esm2022/lib/selector/multi-selector/index.js +3 -0
  14. package/esm2022/lib/selector/multi-selector/index.js.map +1 -0
  15. package/esm2022/lib/selector/multi-selector/multi-selector.component.js +70 -0
  16. package/esm2022/lib/selector/multi-selector/multi-selector.component.js.map +1 -0
  17. package/esm2022/lib/selector/multi-selector/multi-selector.service.js +49 -0
  18. package/esm2022/lib/selector/multi-selector/multi-selector.service.js.map +1 -0
  19. package/esm2022/lib/selector/select-from-list/index.js +2 -0
  20. package/esm2022/lib/selector/select-from-list/index.js.map +1 -0
  21. package/esm2022/lib/selector/select-from-list/select-from-list.component.js +234 -0
  22. package/esm2022/lib/selector/select-from-list/select-from-list.component.js.map +1 -0
  23. package/esm2022/lib/selector/selector-base.component.js +36 -0
  24. package/esm2022/lib/selector/selector-base.component.js.map +1 -0
  25. package/esm2022/lib/selector/selector-base.service.js +49 -0
  26. package/esm2022/lib/selector/selector-base.service.js.map +1 -0
  27. package/esm2022/lib/selector/selector-interfaces.js +2 -0
  28. package/esm2022/lib/selector/selector-interfaces.js.map +1 -0
  29. package/esm2022/lib/selector/selector-options.js +2 -0
  30. package/esm2022/lib/selector/selector-options.js.map +1 -0
  31. package/esm2022/sneat-ui.js +5 -0
  32. package/esm2022/sneat-ui.js.map +1 -0
  33. package/lib/components/sneat-base-modal.component.d.ts +9 -0
  34. package/lib/components/sneat-base.component.d.ts +28 -0
  35. package/lib/focus.d.ts +3 -0
  36. package/lib/selector/multi-selector/multi-selector.component.d.ts +17 -0
  37. package/lib/selector/multi-selector/multi-selector.service.d.ts +9 -0
  38. package/lib/selector/select-from-list/select-from-list.component.d.ts +60 -0
  39. package/lib/selector/selector-base.component.d.ts +18 -0
  40. package/lib/selector/selector-base.service.d.ts +11 -0
  41. package/lib/selector/selector-interfaces.d.ts +17 -0
  42. package/lib/selector/selector-options.d.ts +15 -0
  43. package/package.json +14 -2
  44. package/sneat-ui.d.ts +5 -0
  45. package/eslint.config.js +0 -7
  46. package/ng-package.json +0 -7
  47. package/project.json +0 -38
  48. package/src/lib/components/sneat-base-modal.component.ts +0 -22
  49. package/src/lib/components/sneat-base.component.spec.ts +0 -71
  50. package/src/lib/components/sneat-base.component.ts +0 -78
  51. package/src/lib/focus.ts +0 -19
  52. package/src/lib/selector/multi-selector/multi-selector.component.html +0 -26
  53. package/src/lib/selector/multi-selector/multi-selector.component.spec.ts +0 -147
  54. package/src/lib/selector/multi-selector/multi-selector.component.ts +0 -79
  55. package/src/lib/selector/multi-selector/multi-selector.service.spec.ts +0 -91
  56. package/src/lib/selector/multi-selector/multi-selector.service.ts +0 -49
  57. package/src/lib/selector/select-from-list/select-from-list.component.html +0 -210
  58. package/src/lib/selector/select-from-list/select-from-list.component.spec.ts +0 -297
  59. package/src/lib/selector/select-from-list/select-from-list.component.ts +0 -283
  60. package/src/lib/selector/selector-base.component.ts +0 -43
  61. package/src/lib/selector/selector-base.service.ts +0 -62
  62. package/src/lib/selector/selector-interfaces.ts +0 -28
  63. package/src/lib/selector/selector-options.ts +0 -18
  64. package/src/test-setup.ts +0 -3
  65. package/tsconfig.json +0 -13
  66. package/tsconfig.lib.json +0 -19
  67. package/tsconfig.lib.prod.json +0 -7
  68. package/tsconfig.spec.json +0 -31
  69. package/vite.config.mts +0 -10
  70. /package/{src/index.ts → index.d.ts} +0 -0
  71. /package/{src/lib/components/index.ts → lib/components/index.d.ts} +0 -0
  72. /package/{src/lib/selector/index.ts → lib/selector/index.d.ts} +0 -0
  73. /package/{src/lib/selector/multi-selector/index.ts → lib/selector/multi-selector/index.d.ts} +0 -0
  74. /package/{src/lib/selector/select-from-list/index.ts → lib/selector/select-from-list/index.d.ts} +0 -0
@@ -1,78 +0,0 @@
1
- import { inject, Injectable, InjectionToken, OnDestroy } from '@angular/core';
2
- import { createSetFocusToInput } from '../focus';
3
- import { ErrorLogger } from '@sneat/core';
4
- import { MonoTypeOperatorFunction, Subject, Subscription } from 'rxjs';
5
- import { takeUntil } from 'rxjs/operators';
6
-
7
- export interface IConsole {
8
- log(...data: unknown[]): void;
9
-
10
- warn(...data: unknown[]): void;
11
-
12
- trace(...data: unknown[]): void;
13
-
14
- error(...data: unknown[]): void;
15
- }
16
-
17
- export const ClassName = new InjectionToken<string>('className');
18
-
19
- @Injectable()
20
- export abstract class SneatBaseComponent implements OnDestroy {
21
- // protected $isInitialized = signal(false);
22
- //
23
- // // eslint-disable-next-line @angular-eslint/contextual-lifecycle
24
- // public ngOnInit() {
25
- // console.info(`${this.className}.SneatBaseComponent.ngOnInit()`);
26
- // // $isInitialized is for workaround for https://angular.dev/errors/NG0950
27
- // // Required input is accessed before a value is set.
28
- // // Might be excessive and should be removed if we can find a better way.
29
- // this.$isInitialized.set(true);
30
- // }
31
-
32
- private readonly destroyed = new Subject<void>();
33
- // Signals that the component is destroyed and should not be used anymore
34
- protected readonly destroyed$ = this.destroyed.asObservable();
35
-
36
- // All active subscriptions of a component. Will be unsubscribed on destroy
37
- protected readonly subs = new Subscription();
38
-
39
- protected readonly console: IConsole = console;
40
-
41
- protected readonly errorLogger = inject(ErrorLogger);
42
-
43
- protected readonly logError = this.errorLogger.logError;
44
- protected readonly logErrorHandler = this.errorLogger.logErrorHandler;
45
-
46
- // Passes focus to the input element
47
- protected readonly setFocusToInput = createSetFocusToInput(this.errorLogger);
48
-
49
- protected readonly className = inject(ClassName);
50
-
51
- public constructor() {
52
- this.log(`${this.className}.SneatBaseComponent.constructor()`);
53
- }
54
-
55
- protected log(msg: string, ...data: unknown[]): void {
56
- this.console.log(msg, ...data);
57
- }
58
-
59
- // protected readonly takeUntilDestroyed = <T>() =>
60
- // takeUntil<T>(this.destroyed$);
61
- protected takeUntilDestroyed<T>(): MonoTypeOperatorFunction<T> {
62
- return takeUntil(this.destroyed$);
63
- }
64
-
65
- public ngOnDestroy(): void {
66
- this.log(`${this.className}.SneatBaseComponent.ngOnDestroy()`);
67
- this.unsubscribe(`${this.className}.SneatBaseComponent.ngOnDestroy()`);
68
- this.destroyed?.next();
69
- this.destroyed?.complete();
70
- }
71
-
72
- protected unsubscribe(reason?: string): void {
73
- this.log(
74
- `${this.className}.SneatBaseComponent.unsubscribe(reason: ${reason})`,
75
- );
76
- this.subs.unsubscribe();
77
- }
78
- }
package/src/lib/focus.ts DELETED
@@ -1,19 +0,0 @@
1
- import { IonInput, IonTextarea } from '@ionic/angular/standalone';
2
- import { IErrorLogger } from '@sneat/core';
3
-
4
- export function createSetFocusToInput(errorLogger: IErrorLogger) {
5
- return (input?: IonInput | IonTextarea, delay = 100): void => {
6
- if (!input) {
7
- console.error('can not set focus to undefined input');
8
- return;
9
- }
10
- setTimeout(() => {
11
- requestAnimationFrame(() => {
12
- // input.getInputElement().then(el => el.focus()).catch(errorLogger.logErrorHandler('failed to set focus to input'));
13
- input
14
- .setFocus()
15
- .catch(errorLogger.logErrorHandler('failed to set focus to input'));
16
- });
17
- }, delay);
18
- };
19
- }
@@ -1,26 +0,0 @@
1
- <ion-card>
2
- <ion-item-divider color="light">
3
- <ion-label color="medium">{{ title }}</ion-label>
4
- <ion-buttons slot="end">
5
- <ion-button color="medium">
6
- <ion-icon name="add-outline" />
7
- <ion-label>Add</ion-label>
8
- </ion-button>
9
- </ion-buttons>
10
- </ion-item-divider>
11
-
12
- <ion-list>
13
- @for (item of selectedItems; track item.id) {
14
- <ion-item>
15
- <ion-label>{{ item.title }}</ion-label>
16
- @if (canRemove) {
17
- <ion-buttons slot="end">
18
- <ion-button title="Remove" (click)="removeItem($event, item)">
19
- <ion-icon name="close-outline" />
20
- </ion-button>
21
- </ion-buttons>
22
- }
23
- </ion-item>
24
- }
25
- </ion-list>
26
- </ion-card>
@@ -1,147 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
3
- import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
4
- import { ErrorLogger } from '@sneat/core';
5
- import { ClassName } from '../../components';
6
- import { OverlayController } from '../selector-base.component';
7
- import { MultiSelectorComponent } from './multi-selector.component';
8
- import { ISelectItem } from '../selector-interfaces';
9
-
10
- // Suppress Ionic icon warnings - mock console methods to filter Ionic-specific errors
11
- const originalWarn = console.warn;
12
- const originalError = console.error;
13
-
14
- vi.spyOn(console, 'error').mockImplementation((...args: any[]) => {
15
- const message = args[0]?.toString() || '';
16
- // Only suppress Ionic/Stencil icon loading errors
17
- if (
18
- message.includes('Ionicons') ||
19
- message.includes('@ionic/core/components/icon') ||
20
- message.includes('getAssetPath')
21
- ) {
22
- return;
23
- }
24
- // Let other errors through
25
- originalError(...args);
26
- });
27
-
28
- vi.spyOn(console, 'warn').mockImplementation((...args: any[]) => {
29
- const message = args[0]?.toString() || '';
30
- // Only suppress Ionic icon warnings
31
- if (message.includes('Ionicons') || message.includes('Could not load icon')) {
32
- return;
33
- }
34
- // Let other warnings through
35
- originalWarn(...args);
36
- });
37
-
38
- describe('MultiSelectorComponent', () => {
39
- let component: MultiSelectorComponent;
40
- let fixture: ComponentFixture<MultiSelectorComponent>;
41
-
42
- const mockItems: ISelectItem[] = [
43
- { id: '1', title: 'Item 1' },
44
- { id: '2', title: 'Item 2' },
45
- ];
46
-
47
- beforeEach(waitForAsync(async () => {
48
- await TestBed.configureTestingModule({
49
- imports: [MultiSelectorComponent],
50
- providers: [
51
- { provide: ClassName, useValue: 'TestComponent' },
52
- {
53
- provide: ErrorLogger,
54
- useValue: { logError: vi.fn(), logErrorHandler: () => vi.fn() },
55
- },
56
- {
57
- provide: OverlayController,
58
- useValue: { dismiss: vi.fn().mockResolvedValue(undefined) },
59
- },
60
- ],
61
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
62
- })
63
- .overrideComponent(MultiSelectorComponent, {
64
- set: {
65
- imports: [],
66
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
67
- providers: [
68
- { provide: ClassName, useValue: 'TestComponent' },
69
- {
70
- provide: OverlayController,
71
- useValue: { dismiss: vi.fn().mockResolvedValue(undefined) },
72
- },
73
- ],
74
- },
75
- })
76
- .compileComponents();
77
- fixture = TestBed.createComponent(MultiSelectorComponent);
78
- component = fixture.componentInstance;
79
- fixture.detectChanges();
80
- }));
81
-
82
- it('should create', () => {
83
- expect(component).toBeTruthy();
84
- });
85
-
86
- describe('ngOnChanges', () => {
87
- it('should set selectedItems when allItems changes', () => {
88
- component.allItems = mockItems;
89
- component.selectedIDs = ['1'];
90
- component.ngOnChanges({
91
- allItems: new SimpleChange(undefined, mockItems, true),
92
- });
93
- // @ts-expect-error accessing protected member
94
- expect(component.selectedItems).toEqual([mockItems[0]]);
95
- });
96
- });
97
-
98
- describe('removeItem', () => {
99
- it('should remove item and emit event', () => {
100
- component.allItems = mockItems;
101
- component.selectedIDs = ['1', '2'];
102
- component.ngOnChanges({
103
- allItems: new SimpleChange(undefined, mockItems, true),
104
- });
105
-
106
- const removeSpy = vi.spyOn(component.removeItems, 'emit');
107
- const event = { stopPropagation: vi.fn() };
108
- // @ts-expect-error accessing protected member
109
- component.removeItem(event as any, mockItems[0]);
110
-
111
- expect(event.stopPropagation).toHaveBeenCalled();
112
- // @ts-expect-error accessing protected member
113
- expect(component.selectedItems).toEqual([mockItems[1]]);
114
- expect(removeSpy).toHaveBeenCalledWith([{ event, item: mockItems[0] }]);
115
- });
116
- });
117
- describe('SelectorBaseComponent coverage', () => {
118
- it('should close and dismiss overlay', () => {
119
- // @ts-expect-error accessing protected member
120
- const dismissSpy = vi.spyOn(component.overlayController, 'dismiss');
121
- const event = { stopPropagation: vi.fn(), preventDefault: vi.fn() };
122
- // @ts-expect-error accessing protected member
123
- component.close(event as any);
124
- expect(event.stopPropagation).toHaveBeenCalled();
125
- expect(event.preventDefault).toHaveBeenCalled();
126
- expect(dismissSpy).toHaveBeenCalled();
127
- });
128
-
129
- it('should close without event and dismiss overlay', () => {
130
- // @ts-expect-error accessing protected member
131
- const dismissSpy = vi.spyOn(component.overlayController, 'dismiss');
132
- // @ts-expect-error accessing protected member
133
- component.close();
134
- expect(dismissSpy).toHaveBeenCalled();
135
- });
136
-
137
- it('should ignore ngOnChanges if allItems not changed', () => {
138
- // @ts-expect-error accessing protected member
139
- component.selectedItems = undefined;
140
- component.ngOnChanges({
141
- other: new SimpleChange(undefined, 'value', true),
142
- });
143
- // @ts-expect-error accessing protected member
144
- expect(component.selectedItems).toBeUndefined();
145
- });
146
- });
147
- });
@@ -1,79 +0,0 @@
1
- import {
2
- Component,
3
- EventEmitter,
4
- Input,
5
- OnChanges,
6
- Output,
7
- SimpleChanges,
8
- } from '@angular/core';
9
- import {
10
- IonButton,
11
- IonButtons,
12
- IonCard,
13
- IonIcon,
14
- IonItem,
15
- IonItemDivider,
16
- IonLabel,
17
- IonList,
18
- ModalController,
19
- } from '@ionic/angular/standalone';
20
- import { ClassName } from '../../components';
21
- import { ISelectItem, ISelectItemEvent } from '../selector-interfaces';
22
- import {
23
- OverlayController,
24
- SelectorBaseComponent,
25
- } from '../selector-base.component';
26
-
27
- @Component({
28
- selector: 'sneat-multi-selector',
29
- templateUrl: './multi-selector.component.html',
30
- imports: [
31
- IonCard,
32
- IonItemDivider,
33
- IonLabel,
34
- IonButtons,
35
- IonButton,
36
- IonItem,
37
- IonIcon,
38
- IonList,
39
- ],
40
- providers: [
41
- {
42
- provide: ClassName,
43
- useValue: 'MultiSelectorComponent',
44
- },
45
- {
46
- provide: OverlayController,
47
- useClass: ModalController,
48
- },
49
- ],
50
- })
51
- export class MultiSelectorComponent<T = ISelectItem>
52
- extends SelectorBaseComponent<T>
53
- implements OnChanges
54
- {
55
- @Input() title = 'Select';
56
-
57
- @Input() canRemove = false;
58
- @Input() public allItems?: ISelectItem[];
59
- @Input() public selectedIDs?: readonly string[];
60
-
61
- @Output() readonly removeItems = new EventEmitter<ISelectItemEvent[]>();
62
- @Output() readonly addItems = new EventEmitter<ISelectItemEvent[]>();
63
-
64
- protected selectedItems?: ISelectItem[];
65
-
66
- ngOnChanges(changes: SimpleChanges): void {
67
- if (changes['allItems']) {
68
- this.selectedItems = this.allItems?.filter((item) =>
69
- this.selectedIDs?.includes(item.id),
70
- );
71
- }
72
- }
73
-
74
- protected removeItem(event: Event, item: ISelectItem): void {
75
- event.stopPropagation();
76
- this.selectedItems = this.selectedItems?.filter((i) => i.id !== item.id);
77
- this.removeItems.emit([{ event, item }]);
78
- }
79
- }
@@ -1,91 +0,0 @@
1
- import { MultiSelectorService } from './multi-selector.service';
2
- import { ISelectItem } from '../selector-interfaces';
3
- import { ErrorLogger } from '@sneat/core';
4
- import { ModalController } from '@ionic/angular/standalone';
5
-
6
- describe('MultiSelectorService', () => {
7
- let service: MultiSelectorService;
8
- let errorLoggerMock: ErrorLogger;
9
- let modalControllerMock: ModalController;
10
-
11
- beforeEach(() => {
12
- errorLoggerMock = {
13
- logError: vi.fn(),
14
- logErrorHandler: vi.fn(() => vi.fn()),
15
- };
16
- modalControllerMock = {
17
- create: vi.fn(),
18
- };
19
- service = new MultiSelectorService(errorLoggerMock, modalControllerMock);
20
- });
21
-
22
- it('should create', () => {
23
- expect(service).toBeTruthy();
24
- });
25
-
26
- describe('selectMultiple', () => {
27
- const items: ISelectItem[] = [{ id: '1', title: 'test' }];
28
-
29
- it('should resolve with selected items on success', async () => {
30
- const modalMock = {
31
- present: vi.fn().mockReturnValue(Promise.resolve()),
32
- onDidDismiss: vi.fn().mockReturnValue(
33
- Promise.resolve({ data: { selectedItems: items } }),
34
- ),
35
- };
36
- modalControllerMock.create.mockReturnValue(Promise.resolve(modalMock));
37
-
38
- const result = await service.selectMultiple(items, []);
39
- expect(result).toEqual(items);
40
- expect(modalControllerMock.create).toHaveBeenCalled();
41
- expect(modalMock.present).toHaveBeenCalled();
42
- });
43
-
44
- it('should reject and log error if modal creation fails', async () => {
45
- modalControllerMock.create.mockReturnValue(Promise.reject('fail'));
46
- await expect(service.selectMultiple([], [])).rejects.toBe('fail');
47
- expect(errorLoggerMock.logError).toHaveBeenCalledWith(
48
- 'fail',
49
- 'Failed to create modal',
50
- );
51
- });
52
-
53
- it('should reject and log error if modal presentation fails', async () => {
54
- const modalMock = {
55
- present: vi.fn().mockReturnValue(Promise.reject('present-fail')),
56
- onDidDismiss: vi.fn().mockReturnValue(new Promise(() => { /* never resolves */ })),
57
- };
58
- modalControllerMock.create.mockReturnValue(Promise.resolve(modalMock));
59
- await expect(service.selectMultiple([], [])).rejects.toBe('present-fail');
60
- expect(errorLoggerMock.logError).toHaveBeenCalledWith(
61
- 'Failed to present modal',
62
- );
63
- });
64
-
65
- it('should reject and log error if modal dismiss fails', async () => {
66
- const modalMock = {
67
- present: vi.fn().mockReturnValue(Promise.resolve()),
68
- onDidDismiss: vi.fn().mockReturnValue(Promise.reject('dismiss-fail')),
69
- };
70
- modalControllerMock.create.mockReturnValue(Promise.resolve(modalMock));
71
- await expect(service.selectMultiple([], [])).rejects.toBe('dismiss-fail');
72
- expect(errorLoggerMock.logError).toHaveBeenCalledWith(
73
- 'dismiss-fail',
74
- 'Failed to handle modal dismiss',
75
- );
76
- });
77
-
78
- it('should resolve with empty array if selectedItems is missing in response', async () => {
79
- const modalMock = {
80
- present: vi.fn().mockReturnValue(Promise.resolve()),
81
- onDidDismiss: vi.fn().mockReturnValue(
82
- Promise.resolve({ data: {} }),
83
- ),
84
- };
85
- modalControllerMock.create.mockReturnValue(Promise.resolve(modalMock));
86
-
87
- const result = await service.selectMultiple([], []);
88
- expect(result).toEqual([]);
89
- });
90
- });
91
- });
@@ -1,49 +0,0 @@
1
- import { Inject } from '@angular/core';
2
- import { ModalController, ModalOptions } from '@ionic/angular/standalone';
3
- import { ErrorLogger, IErrorLogger } from '@sneat/core';
4
- import { ISelectItem } from '../selector-interfaces';
5
- import { MultiSelectorComponent } from './multi-selector.component';
6
-
7
- export class MultiSelectorService {
8
- constructor(
9
- @Inject(ErrorLogger) private readonly errorLogger: IErrorLogger,
10
- private readonly modalController: ModalController,
11
- ) {}
12
-
13
- selectMultiple(
14
- items: ISelectItem[],
15
- selectedItems: ISelectItem[],
16
- ): Promise<ISelectItem[]> {
17
- const result = new Promise<ISelectItem[]>((resolve, reject) => {
18
- const modalOptions: ModalOptions = {
19
- component: MultiSelectorComponent,
20
- componentProps: {
21
- items,
22
- selectedItems,
23
- },
24
- keyboardClose: true,
25
- };
26
- this.modalController
27
- .create(modalOptions)
28
- .then((modal) => {
29
- modal
30
- .onDidDismiss()
31
- .then((res) => resolve(res.data?.selectedItems || []))
32
- .catch((err) => {
33
- this.errorLogger.logError(err, 'Failed to handle modal dismiss');
34
- reject(err);
35
- });
36
- modal.present().catch((err) => {
37
- reject(err);
38
- this.errorLogger.logError('Failed to present modal');
39
- });
40
- })
41
- .catch((err) => {
42
- this.errorLogger.logError(err, 'Failed to create modal');
43
- reject(err);
44
- });
45
- });
46
-
47
- return result;
48
- }
49
- }