@websolutespa/bom-mixer-hooks 0.2.0 → 0.2.2
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/CHANGELOG.md +12 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +7 -5
- package/src/index.ts +60 -0
- package/src/useApi/useApi.test.tsx +193 -0
- package/src/useApi/useApi.ts +58 -0
- package/src/useBodyScroll/useBodyScroll.test.ts +112 -0
- package/src/useBodyScroll/useBodyScroll.ts +89 -0
- package/src/useBreakpoint/useBreakpoint.test.tsx +45 -0
- package/src/useBreakpoint/useBreakpoint.ts +44 -0
- package/src/useCart/useCart.test.tsx +89 -0
- package/src/useCart/useCart.tsx +132 -0
- package/src/useCheckout/useCheckout.test.tsx +53 -0
- package/src/useCheckout/useCheckout.tsx +95 -0
- package/src/useClickAnyWhere/useClickAnyWhere.test.ts +15 -0
- package/src/useClickAnyWhere/useClickAnyWhere.ts +9 -0
- package/src/useClickOut/useClickOut.test.tsx +78 -0
- package/src/useClickOut/useClickOut.ts +25 -0
- package/src/useCurrency/useCurrency.test.tsx +33 -0
- package/src/useCurrency/useCurrency.ts +42 -0
- package/src/useCurrentState/useCurrentState.test.tsx +56 -0
- package/src/useCurrentState/useCurrentState.ts +22 -0
- package/src/useDateTimeFormat/useDateTimeFormat.test.tsx +47 -0
- package/src/useDateTimeFormat/useDateTimeFormat.ts +22 -0
- package/src/useDebounce/useDebounce.test.ts +49 -0
- package/src/useDebounce/useDebounce.ts +15 -0
- package/src/useDebounceCallback/useDebounceCallback.test.ts +34 -0
- package/src/useDebounceCallback/useDebounceCallback.ts +102 -0
- package/src/useDebugChangedProps/useDebugChangedProps.test.tsx +21 -0
- package/src/useDebugChangedProps/useDebugChangedProps.ts +17 -0
- package/src/useDomObserver/useDomObserver.test.ts +22 -0
- package/src/useDomObserver/useDomObserver.ts +26 -0
- package/src/useDrawer/useDrawer.test.ts +17 -0
- package/src/useDrawer/useDrawer.ts +16 -0
- package/src/useElementSize/useElementSize.test.ts +108 -0
- package/src/useElementSize/useElementSize.ts +34 -0
- package/src/useEventListener/useEventListener.test.ts +128 -0
- package/src/useEventListener/useEventListener.ts +43 -0
- package/src/useFilters/filter.service.ts +86 -0
- package/src/useFilters/filter.ts +152 -0
- package/src/useFilters/useFilters.test.ts +48 -0
- package/src/useFilters/useFilters.ts +44 -0
- package/src/useGenericContext/useGenericContext.test.tsx +19 -0
- package/src/useGenericContext/useGenericContext.ts +18 -0
- package/src/useGeolocation/geolocation.service.ts +66 -0
- package/src/useGeolocation/useGeolocation.test.ts +29 -0
- package/src/useGeolocation/useGeolocation.ts +56 -0
- package/src/useHttp/useHttp.test.tsx +192 -0
- package/src/useHttp/useHttp.ts +52 -0
- package/src/useIdle/useIdle.test.ts +31 -0
- package/src/useIdle/useIdle.ts +79 -0
- package/src/useInfiniteLoader/useInfiniteLoader.test.ts +41 -0
- package/src/useInfiniteLoader/useInfiniteLoader.ts +46 -0
- package/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts +3 -0
- package/src/useKeyboard/codes.ts +94 -0
- package/src/useKeyboard/helper.ts +31 -0
- package/src/useKeyboard/useKeyboard.test.tsx +163 -0
- package/src/useKeyboard/useKeyboard.ts +91 -0
- package/src/useLabel/useLabel.test.tsx +68 -0
- package/src/useLabel/useLabel.tsx +43 -0
- package/src/useLayout/useLayout.test.tsx +32 -0
- package/src/useLayout/useLayout.tsx +19 -0
- package/src/useModal/useModal.test.ts +19 -0
- package/src/useModal/useModal.ts +16 -0
- package/src/useMounted/useMounted.test.tsx +34 -0
- package/src/useMounted/useMounted.ts +25 -0
- package/src/useNumber/useNumber.test.tsx +36 -0
- package/src/useNumber/useNumber.ts +15 -0
- package/src/usePage/usePage.test.tsx +31 -0
- package/src/usePage/usePage.tsx +19 -0
- package/src/usePagination/pagination.service.ts +23 -0
- package/src/usePagination/usePagination.test.ts +64 -0
- package/src/usePagination/usePagination.ts +47 -0
- package/src/usePortal/usePortal.test.ts +24 -0
- package/src/usePortal/usePortal.ts +34 -0
- package/src/usePrevious/usePrevious.test.ts +21 -0
- package/src/usePrevious/usePrevious.ts +9 -0
- package/src/useRealSize/useRealSize.ts +41 -0
- package/src/useResize/useResize.test.ts +31 -0
- package/src/useResize/useResize.ts +18 -0
- package/src/useSSR/useSSR.test.tsx +16 -0
- package/src/useSSR/useSSR.ts +23 -0
- package/src/useScroll/useScroll.test.tsx +25 -0
- package/src/useScroll/useScroll.ts +30 -0
- package/src/useScrollTo/useScrollTo.test.tsx +105 -0
- package/src/useScrollTo/useScrollTo.ts +41 -0
- package/src/useSearchParams/useSearchParams.test.ts +59 -0
- package/src/useSearchParams/useSearchParams.ts +123 -0
- package/src/useSearchParamsEncoded/useSearchParamsEncoded.test.ts +58 -0
- package/src/useSearchParamsEncoded/useSearchParamsEncoded.ts +202 -0
- package/src/useSorting/useSorting.test.ts +40 -0
- package/src/useSorting/useSorting.ts +99 -0
- package/src/useSwiperIndex/useSwiperIndex.ts +30 -0
- package/src/useSyncedRef/useSyncedRef.test.ts +16 -0
- package/src/useSyncedRef/useSyncedRef.ts +18 -0
- package/src/useUI/useUI.test.ts +16 -0
- package/src/useUI/useUI.ts +44 -0
- package/src/useUnit/useUnit.test.tsx +33 -0
- package/src/useUnit/useUnit.ts +5 -0
- package/src/useUnmountEffect/useUnmountEffect.test.ts +15 -0
- package/src/useUnmountEffect/useUnmountEffect.ts +16 -0
- package/src/useUser/useUser.test.ts +22 -0
- package/src/useUser/useUser.ts +20 -0
- package/src/useWindowSize/useWindowSize.test.ts +48 -0
- package/src/useWindowSize/useWindowSize.ts +31 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { RefObject, useEffect, useRef } from 'react';
|
|
2
|
+
import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
|
|
3
|
+
|
|
4
|
+
// Window Event based useEventListener interface
|
|
5
|
+
function useEventListener_<K extends keyof WindowEventMap>(eventName: K, handler: (event: WindowEventMap[K]) => void, element?: undefined, options?: boolean | AddEventListenerOptions): void;
|
|
6
|
+
// Element Event based useEventListener interface
|
|
7
|
+
function useEventListener_<K extends keyof HTMLElementEventMap, T extends HTMLElement = HTMLDivElement>(eventName: K, handler: (event: HTMLElementEventMap[K]) => void, element: RefObject<T>, options?: boolean | AddEventListenerOptions): void;
|
|
8
|
+
// Document Event based useEventListener interface
|
|
9
|
+
function useEventListener_<K extends keyof DocumentEventMap>(eventName: K, handler: (event: DocumentEventMap[K]) => void, element: RefObject<Document>, options?: boolean | AddEventListenerOptions): void;
|
|
10
|
+
function useEventListener_<KW extends keyof WindowEventMap, KH extends keyof HTMLElementEventMap, T extends HTMLElement | void = void>(
|
|
11
|
+
eventName: KW | KH,
|
|
12
|
+
handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
|
|
13
|
+
element?: RefObject<T>,
|
|
14
|
+
options?: boolean | AddEventListenerOptions
|
|
15
|
+
) {
|
|
16
|
+
// Create a ref that stores handler
|
|
17
|
+
const savedHandler = useRef(handler);
|
|
18
|
+
|
|
19
|
+
useIsomorphicLayoutEffect(() => {
|
|
20
|
+
savedHandler.current = handler;
|
|
21
|
+
}, [handler]);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
// Define the listening target
|
|
25
|
+
const targetElement: T | Window = element?.current || window;
|
|
26
|
+
|
|
27
|
+
if (!(targetElement && targetElement.addEventListener)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create event listener that calls handler function stored in ref
|
|
32
|
+
const eventListener: typeof handler = event => savedHandler.current(event);
|
|
33
|
+
|
|
34
|
+
targetElement.addEventListener(eventName, eventListener, options);
|
|
35
|
+
|
|
36
|
+
// Remove event listener on cleanup
|
|
37
|
+
return () => {
|
|
38
|
+
targetElement.removeEventListener(eventName, eventListener);
|
|
39
|
+
};
|
|
40
|
+
}, [eventName, element, options]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useEventListener = useEventListener_;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { IEquatable } from '@websolutespa/bom-core';
|
|
2
|
+
import { IFeatureType } from '@websolutespa/bom-mixer-models';
|
|
3
|
+
import { Filter, FilterMode, FilterParams } from './filter';
|
|
4
|
+
|
|
5
|
+
export function getFilters<T>(items: T[], featureTypes: IFeatureType[], filterMap: (key: string, item: T, value: IEquatable) => boolean, params?: FilterParams | null): Filter<T>[] {
|
|
6
|
+
return featureTypes.map(featureType => {
|
|
7
|
+
const filter = Filter.fromFeatureType<T>(featureType, (featureType.mode as FilterMode) || FilterMode.OR);
|
|
8
|
+
filter.filter = (item, value) => {
|
|
9
|
+
if (typeof filterMap === 'function') {
|
|
10
|
+
return filterMap(featureType.id, item, value);
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
filter.removeInvalidOptions(items);
|
|
15
|
+
if (params && params[filter.id] != null) {
|
|
16
|
+
filter.values = params[filter.id] as IEquatable[];
|
|
17
|
+
}
|
|
18
|
+
return filter;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getFilteredItems<T>(items: T[], selectedFilters: Filter<T>[]): T[] {
|
|
23
|
+
const filteredItems = items.filter(item => {
|
|
24
|
+
let has = true;
|
|
25
|
+
selectedFilters.forEach(filter => {
|
|
26
|
+
has = has && filter.match(item);
|
|
27
|
+
});
|
|
28
|
+
return has;
|
|
29
|
+
});
|
|
30
|
+
return filteredItems;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setFilters<T>(items: T[], filters: Filter<T>[], filter?: Filter<T>, values?: IEquatable[]): T[] {
|
|
34
|
+
|
|
35
|
+
// if passed featureType and values with set filter values
|
|
36
|
+
if (filter) {
|
|
37
|
+
// console.log('setFilter', filter.id, values);
|
|
38
|
+
filter.values = values || [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// selecting all filters with values
|
|
42
|
+
const selectedFilters = filters.filter(x => x.values.length > 0);
|
|
43
|
+
|
|
44
|
+
// filtering items
|
|
45
|
+
const filteredItems = getFilteredItems<T>(items, selectedFilters);
|
|
46
|
+
|
|
47
|
+
// updating filter options
|
|
48
|
+
filters.forEach(filter => {
|
|
49
|
+
const otherFilters = selectedFilters.filter(x => x !== filter);
|
|
50
|
+
const filteredItems = getFilteredItems<T>(items, otherFilters);
|
|
51
|
+
if (filter.options) {
|
|
52
|
+
filter.options.forEach(option => {
|
|
53
|
+
let count = 0;
|
|
54
|
+
if (option.id) {
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (i < filteredItems.length) {
|
|
57
|
+
const item = filteredItems[i];
|
|
58
|
+
if (filter.filter(item, option.id)) {
|
|
59
|
+
count++;
|
|
60
|
+
}
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
count = filteredItems.length;
|
|
65
|
+
}
|
|
66
|
+
option.count = count;
|
|
67
|
+
option.disabled = count === 0;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return filteredItems;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function filtersToParams<T>(filters: Filter<T>[]): FilterParams | null {
|
|
76
|
+
const params: FilterParams = {};
|
|
77
|
+
let any = false;
|
|
78
|
+
filters.filter(x => x.hasAny()).forEach(filter => {
|
|
79
|
+
params[filter.id] = filter.values;
|
|
80
|
+
any = true;
|
|
81
|
+
});
|
|
82
|
+
if (!any) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return params;
|
|
86
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { IEquatable } from '@websolutespa/bom-core';
|
|
2
|
+
import { IFeatureType } from '@websolutespa/bom-mixer-models';
|
|
3
|
+
|
|
4
|
+
export type FilterParams = { [key: string]: IEquatable[] | null };
|
|
5
|
+
export type FilterValues = IEquatable[][];
|
|
6
|
+
|
|
7
|
+
export enum FilterMode {
|
|
8
|
+
SELECT = 'select',
|
|
9
|
+
AND = 'and',
|
|
10
|
+
OR = 'or',
|
|
11
|
+
QUERY = 'query',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type IFilterOption = {
|
|
15
|
+
id: IEquatable;
|
|
16
|
+
title: string;
|
|
17
|
+
count?: number;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type IFilter = {
|
|
22
|
+
id?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
mode?: FilterMode;
|
|
25
|
+
options?: IFilterOption[];
|
|
26
|
+
values?: IEquatable[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class Filter<T> implements IFilter {
|
|
30
|
+
id: string = 'filter';
|
|
31
|
+
title: string = 'Filter';
|
|
32
|
+
mode: FilterMode = FilterMode.OR;
|
|
33
|
+
options: IFilterOption[] = [];
|
|
34
|
+
values: IEquatable[] = [];
|
|
35
|
+
|
|
36
|
+
constructor(options?: IFilter) {
|
|
37
|
+
if (options) {
|
|
38
|
+
// console.log(options.values);
|
|
39
|
+
Object.assign(this, options);
|
|
40
|
+
}
|
|
41
|
+
if (this.mode === FilterMode.SELECT) {
|
|
42
|
+
this.options.unshift({
|
|
43
|
+
id: -1, // !!! should be undefined
|
|
44
|
+
title: 'select',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static fromFeatureType<T>(featureType: IFeatureType, mode: FilterMode = FilterMode.AND): Filter<T> {
|
|
50
|
+
return new Filter<T>({
|
|
51
|
+
id: featureType.id,
|
|
52
|
+
title: featureType.title,
|
|
53
|
+
mode,
|
|
54
|
+
options: featureType.features,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
filter(item: T, value: IEquatable): boolean {
|
|
59
|
+
console.warn('filter not defined for', item, value);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
match(item: T): boolean {
|
|
64
|
+
let match: boolean;
|
|
65
|
+
if (this.mode === FilterMode.OR) {
|
|
66
|
+
// OR: if item has any of the selected values
|
|
67
|
+
match = this.values.length ? false : true;
|
|
68
|
+
this.values.forEach(value => {
|
|
69
|
+
match = match || this.filter(item, value);
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
// AND: item must have all selected values
|
|
73
|
+
match = true;
|
|
74
|
+
this.values.forEach(value => {
|
|
75
|
+
match = match && this.filter(item, value);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return match;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
removeInvalidOptions(items: T[]): void {
|
|
82
|
+
if (this.options) {
|
|
83
|
+
this.options = this.options.filter(option => {
|
|
84
|
+
const hasItemWithOption = items.reduce((p, item) => {
|
|
85
|
+
return p || this.filter(item, option.id);
|
|
86
|
+
}, false);
|
|
87
|
+
return hasItemWithOption;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getLabel(): string | null {
|
|
93
|
+
if (this.hasAny()) {
|
|
94
|
+
return this.options.filter(x => x.id && this.values.indexOf(x.id) !== -1).map(x => x.title).join(', ');
|
|
95
|
+
} else {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
hasAny(): boolean {
|
|
101
|
+
return this.values.length > 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getCount(): number {
|
|
105
|
+
return this.options.reduce((p, option) => {
|
|
106
|
+
return p + (option.count || 0);
|
|
107
|
+
}, 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
hasResults(): boolean {
|
|
111
|
+
return this.getCount() > 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
has(option: IFilterOption): boolean {
|
|
115
|
+
return this.values.indexOf(option.id) !== -1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
set(option: IFilterOption): void {
|
|
119
|
+
// console.log(this.mode, FilterMode.SELECT);
|
|
120
|
+
if (this.mode === FilterMode.QUERY) {
|
|
121
|
+
this.values = option ? [option.toString()] : [];
|
|
122
|
+
} else if (this.mode === FilterMode.SELECT) {
|
|
123
|
+
this.values = option.id != null ? [option.id] : [];
|
|
124
|
+
} else {
|
|
125
|
+
const index = this.values.indexOf(option.id);
|
|
126
|
+
if (index === -1) {
|
|
127
|
+
if (option.id != null) {
|
|
128
|
+
this.values.push(option.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
remove(option: IFilterOption): void {
|
|
135
|
+
const index = this.values.indexOf(option.id);
|
|
136
|
+
if (index !== -1) {
|
|
137
|
+
this.values.splice(index, 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
toggle(option: IFilterOption): void {
|
|
142
|
+
if (this.has(option)) {
|
|
143
|
+
this.remove(option);
|
|
144
|
+
} else {
|
|
145
|
+
this.set(option);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
clear(): void {
|
|
150
|
+
this.values = [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// readme https://testing-library.com/
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { IEquatable } from '@websolutespa/bom-core';
|
|
4
|
+
import { act } from 'react-dom/test-utils';
|
|
5
|
+
import { useFilters } from './useFilters';
|
|
6
|
+
|
|
7
|
+
type Item = {
|
|
8
|
+
category: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('useFilters', () => {
|
|
12
|
+
|
|
13
|
+
const items = [{ category: 1 }, { category: 2 }];
|
|
14
|
+
|
|
15
|
+
const featureTypes = [{
|
|
16
|
+
id: 'category',
|
|
17
|
+
title: 'category',
|
|
18
|
+
mode: 'or',
|
|
19
|
+
features: [{
|
|
20
|
+
id: 1,
|
|
21
|
+
title: 'feature',
|
|
22
|
+
}]
|
|
23
|
+
}];
|
|
24
|
+
|
|
25
|
+
const filterItem = jest.fn((key: string, item: Item, value: IEquatable) => {
|
|
26
|
+
switch (key) {
|
|
27
|
+
case 'category':
|
|
28
|
+
return item.category === value;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should work correctly', async () => {
|
|
34
|
+
const params = {};
|
|
35
|
+
const { result } = renderHook(() => useFilters<Item>(items, featureTypes, filterItem, params));
|
|
36
|
+
expect(result.current.filteredItems).toEqual(items);
|
|
37
|
+
act(() => result.current.setFilter(result.current.filters[0], [1]));
|
|
38
|
+
expect(result.current.filteredItems).toEqual(items.filter(x => filterItem('category', x, 1)));
|
|
39
|
+
const omittedItems = result.current.itemsWithOmittedKeys('category');
|
|
40
|
+
expect(omittedItems).toEqual(items);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should work with params', async () => {
|
|
44
|
+
const params = { category: [1] };
|
|
45
|
+
const { result } = renderHook(() => useFilters<Item>(items, featureTypes, filterItem, params));
|
|
46
|
+
expect(result.current.filteredItems).toEqual(items.filter(x => filterItem('category', x, 1)));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { IEquatable } from '@websolutespa/bom-core';
|
|
2
|
+
import { IFeatureType } from '@websolutespa/bom-mixer-models';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { Filter, FilterParams } from './filter';
|
|
5
|
+
import { getFilters, setFilters } from './filter.service';
|
|
6
|
+
|
|
7
|
+
export function useFilters<T>(items: T[], featureTypes: IFeatureType[], filterItem: (key: string, item: T, value: IEquatable) => boolean, initialValues?: FilterParams | null): UseFiltersResultType<T> {
|
|
8
|
+
// console.log('useFilters', params);
|
|
9
|
+
|
|
10
|
+
// creating filters with useMemo because is an heavy operation
|
|
11
|
+
const filters = useMemo(() => {
|
|
12
|
+
return getFilters<T>(items, featureTypes, filterItem, initialValues);
|
|
13
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
+
}, [items, featureTypes, filterItem]);
|
|
15
|
+
|
|
16
|
+
const [filteredItems, setFilteredItems] = useState<T[]>(() => setFilters(items, filters));
|
|
17
|
+
|
|
18
|
+
const itemsWithOmittedKeys = useCallback((...keys: string[]) => {
|
|
19
|
+
const filtersWithOmittedKeys = filters.filter(x => !keys.includes(x.id));
|
|
20
|
+
const filteredItems = setFilters(items, filtersWithOmittedKeys);
|
|
21
|
+
return filteredItems;
|
|
22
|
+
}, [items, filters]);
|
|
23
|
+
|
|
24
|
+
// setFilter is called when user select a filter value
|
|
25
|
+
const setFilter = useCallback((filter?: Filter<T>, values?: IEquatable[]) => {
|
|
26
|
+
const filteredItems = setFilters(items, filters, filter, values);
|
|
27
|
+
setFilteredItems(filteredItems);
|
|
28
|
+
}, [items, filters]);
|
|
29
|
+
|
|
30
|
+
// initial call to setFilter
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setFilter();
|
|
33
|
+
return () => { };
|
|
34
|
+
}, [setFilter]);
|
|
35
|
+
|
|
36
|
+
return { filteredItems, filters, setFilter, itemsWithOmittedKeys };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type UseFiltersResultType<T> = {
|
|
40
|
+
filteredItems: T[];
|
|
41
|
+
filters: Filter<T>[];
|
|
42
|
+
setFilter: (filter?: Filter<T>, values?: IEquatable[]) => void;
|
|
43
|
+
itemsWithOmittedKeys: (...keys: string[]) => T[];
|
|
44
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { createGenericContext } from './useGenericContext';
|
|
3
|
+
|
|
4
|
+
describe('useGenericContext', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('should create context', async () => {
|
|
10
|
+
const value = { message: 'test' };
|
|
11
|
+
const [useContext, ContextProvider] = createGenericContext<typeof value>();
|
|
12
|
+
const wrapper = (props: { children: React.ReactNode }) => (
|
|
13
|
+
<ContextProvider value={value}>{props.children}</ContextProvider>
|
|
14
|
+
);
|
|
15
|
+
const { result } = renderHook(() => useContext(), { wrapper });
|
|
16
|
+
expect(result.current).toEqual(value);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export const createGenericContext = <T>(defaultContext: T | null = null) => {
|
|
4
|
+
// Create a context with a generic parameter or undefined
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
6
|
+
const genericContext = createContext<T | undefined>(defaultContext!);
|
|
7
|
+
|
|
8
|
+
// Check if the value provided to the context is defined or throw an error
|
|
9
|
+
const useGenericContext = () => {
|
|
10
|
+
const context = useContext(genericContext);
|
|
11
|
+
if (!context) {
|
|
12
|
+
throw new Error('useGenericContext must be used within a Provider');
|
|
13
|
+
}
|
|
14
|
+
return context;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return [useGenericContext, genericContext.Provider] as const;
|
|
18
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @desc Made compatible with {GeolocationPositionError} and {PositionError} cause
|
|
4
|
+
* PositionError been renamed to GeolocationPositionError in typescript 4.1.x and making
|
|
5
|
+
* own compatible interface is most easiest way to avoid errors.
|
|
6
|
+
*/
|
|
7
|
+
export type IGeolocationPositionError = {
|
|
8
|
+
readonly code: number;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly PERMISSION_DENIED: number;
|
|
11
|
+
readonly POSITION_UNAVAILABLE: number;
|
|
12
|
+
readonly TIMEOUT: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type GeoLocationSensorState = {
|
|
16
|
+
loading: boolean;
|
|
17
|
+
accuracy: number | null;
|
|
18
|
+
altitude: number | null;
|
|
19
|
+
altitudeAccuracy: number | null;
|
|
20
|
+
heading: number | null;
|
|
21
|
+
latitude: number | null;
|
|
22
|
+
longitude: number | null;
|
|
23
|
+
speed: number | null;
|
|
24
|
+
timestamp: number | null;
|
|
25
|
+
error?: Error | IGeolocationPositionError;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function getCurrentPosition(options?: PositionOptions): Promise<GeoLocationSensorState> {
|
|
29
|
+
return new Promise<GeoLocationSensorState>((resolve, reject) => {
|
|
30
|
+
if (!hasGeolocation()) {
|
|
31
|
+
reject('geolocation not available');
|
|
32
|
+
}
|
|
33
|
+
navigator.geolocation.getCurrentPosition((event: GeolocationPosition) => {
|
|
34
|
+
const result: GeoLocationSensorState = {
|
|
35
|
+
loading: false,
|
|
36
|
+
accuracy: event.coords.accuracy,
|
|
37
|
+
altitude: event.coords.altitude,
|
|
38
|
+
altitudeAccuracy: event.coords.altitudeAccuracy,
|
|
39
|
+
heading: event.coords.heading,
|
|
40
|
+
latitude: event.coords.latitude,
|
|
41
|
+
longitude: event.coords.longitude,
|
|
42
|
+
speed: event.coords.speed,
|
|
43
|
+
timestamp: event.timestamp,
|
|
44
|
+
};
|
|
45
|
+
resolve(result);
|
|
46
|
+
}, (error: IGeolocationPositionError) => {
|
|
47
|
+
const sensorError: GeoLocationSensorState = {
|
|
48
|
+
loading: false,
|
|
49
|
+
error,
|
|
50
|
+
accuracy: null,
|
|
51
|
+
altitude: null,
|
|
52
|
+
altitudeAccuracy: null,
|
|
53
|
+
heading: null,
|
|
54
|
+
latitude: null,
|
|
55
|
+
longitude: null,
|
|
56
|
+
speed: null,
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
};
|
|
59
|
+
reject(sensorError);
|
|
60
|
+
}, options);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function hasGeolocation(): boolean {
|
|
65
|
+
return Boolean(navigator && navigator.geolocation);
|
|
66
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// readme https://testing-library.com/
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { useGeolocation } from './useGeolocation';
|
|
4
|
+
|
|
5
|
+
const getCurrentPosition = jest.fn();
|
|
6
|
+
const watchPosition = jest.fn();
|
|
7
|
+
const clearWatch = jest.fn();
|
|
8
|
+
|
|
9
|
+
(global.navigator as any).geolocation = {
|
|
10
|
+
getCurrentPosition,
|
|
11
|
+
watchPosition,
|
|
12
|
+
clearWatch,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('useGeolocation', () => {
|
|
16
|
+
|
|
17
|
+
it('should work correctly', () => {
|
|
18
|
+
getCurrentPosition.mockImplementationOnce((success: (result: any) => Promise<any>) => Promise.resolve(success({
|
|
19
|
+
coords: {
|
|
20
|
+
latitude: 50.0,
|
|
21
|
+
longitude: 40.0
|
|
22
|
+
}
|
|
23
|
+
})));
|
|
24
|
+
const { result } = renderHook(() => useGeolocation());
|
|
25
|
+
expect(result.current.latitude).toBe(50.0);
|
|
26
|
+
expect(result.current.longitude).toBe(40.0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { GeoLocationSensorState, hasGeolocation, IGeolocationPositionError } from './geolocation.service';
|
|
3
|
+
|
|
4
|
+
export function useGeolocation(options?: PositionOptions): GeoLocationSensorState {
|
|
5
|
+
|
|
6
|
+
const [state, setState] = useState<GeoLocationSensorState>({
|
|
7
|
+
loading: true,
|
|
8
|
+
accuracy: null,
|
|
9
|
+
altitude: null,
|
|
10
|
+
altitudeAccuracy: null,
|
|
11
|
+
heading: null,
|
|
12
|
+
latitude: null,
|
|
13
|
+
longitude: null,
|
|
14
|
+
speed: null,
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let mounted = true;
|
|
19
|
+
let watchId: number;
|
|
20
|
+
|
|
21
|
+
const onEvent: PositionCallback = (event: GeolocationPosition) => {
|
|
22
|
+
if (mounted) {
|
|
23
|
+
setState({
|
|
24
|
+
loading: false,
|
|
25
|
+
accuracy: event.coords.accuracy,
|
|
26
|
+
altitude: event.coords.altitude,
|
|
27
|
+
altitudeAccuracy: event.coords.altitudeAccuracy,
|
|
28
|
+
heading: event.coords.heading,
|
|
29
|
+
latitude: event.coords.latitude,
|
|
30
|
+
longitude: event.coords.longitude,
|
|
31
|
+
speed: event.coords.speed,
|
|
32
|
+
timestamp: event.timestamp,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const onEventError = (error: IGeolocationPositionError) =>
|
|
38
|
+
mounted && setState((oldState) => ({ ...oldState, loading: false, error }));
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!hasGeolocation()) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
navigator.geolocation.getCurrentPosition(onEvent, onEventError, options);
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
watchId = navigator.geolocation.watchPosition(onEvent, onEventError, options);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
+
mounted = false;
|
|
51
|
+
navigator.geolocation.clearWatch(watchId);
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return state;
|
|
56
|
+
}
|