@snack-uikit/utils 3.6.0 → 3.7.1-preview-e840ab81.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/CHANGELOG.md +11 -0
- package/README.md +21 -0
- package/dist/cjs/hooks/index.d.ts +3 -0
- package/dist/cjs/hooks/index.js +4 -1
- package/dist/cjs/hooks/useDynamicList.d.ts +18 -0
- package/dist/cjs/hooks/useDynamicList.js +110 -0
- package/dist/cjs/hooks/useSaveFilterState.d.ts +13 -0
- package/dist/cjs/hooks/useSaveFilterState.js +54 -0
- package/dist/cjs/hooks/useSaveState/index.d.ts +2 -0
- package/dist/cjs/hooks/useSaveState/index.js +26 -0
- package/dist/cjs/hooks/useSaveState/sources/baseSource.d.ts +11 -0
- package/dist/cjs/hooks/useSaveState/sources/baseSource.js +31 -0
- package/dist/cjs/hooks/useSaveState/sources/index.d.ts +3 -0
- package/dist/cjs/hooks/useSaveState/sources/index.js +27 -0
- package/dist/cjs/hooks/useSaveState/sources/localStorageSource.d.ts +5 -0
- package/dist/cjs/hooks/useSaveState/sources/localStorageSource.js +22 -0
- package/dist/cjs/hooks/useSaveState/sources/queryParamSource.d.ts +5 -0
- package/dist/cjs/hooks/useSaveState/sources/queryParamSource.js +24 -0
- package/dist/cjs/hooks/useSaveState/useSaveState.d.ts +7 -0
- package/dist/cjs/hooks/useSaveState/useSaveState.js +27 -0
- package/dist/esm/hooks/index.d.ts +3 -0
- package/dist/esm/hooks/index.js +3 -0
- package/dist/esm/hooks/useDynamicList.d.ts +18 -0
- package/dist/esm/hooks/useDynamicList.js +94 -0
- package/dist/esm/hooks/useSaveFilterState.d.ts +13 -0
- package/dist/esm/hooks/useSaveFilterState.js +30 -0
- package/dist/esm/hooks/useSaveState/index.d.ts +2 -0
- package/dist/esm/hooks/useSaveState/index.js +2 -0
- package/dist/esm/hooks/useSaveState/sources/baseSource.d.ts +11 -0
- package/dist/esm/hooks/useSaveState/sources/baseSource.js +24 -0
- package/dist/esm/hooks/useSaveState/sources/index.d.ts +3 -0
- package/dist/esm/hooks/useSaveState/sources/index.js +3 -0
- package/dist/esm/hooks/useSaveState/sources/localStorageSource.d.ts +5 -0
- package/dist/esm/hooks/useSaveState/sources/localStorageSource.js +15 -0
- package/dist/esm/hooks/useSaveState/sources/queryParamSource.d.ts +5 -0
- package/dist/esm/hooks/useSaveState/sources/queryParamSource.js +17 -0
- package/dist/esm/hooks/useSaveState/useSaveState.d.ts +7 -0
- package/dist/esm/hooks/useSaveState/useSaveState.js +15 -0
- package/package.json +2 -2
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useDynamicList.ts +135 -0
- package/src/hooks/useSaveFilterState.ts +55 -0
- package/src/hooks/useSaveState/index.ts +2 -0
- package/src/hooks/useSaveState/sources/baseSource.ts +32 -0
- package/src/hooks/useSaveState/sources/index.ts +3 -0
- package/src/hooks/useSaveState/sources/localStorageSource.ts +17 -0
- package/src/hooks/useSaveState/sources/queryParamSource.ts +19 -0
- package/src/hooks/useSaveState/useSaveState.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.7.0 (2025-01-23)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **PDS-926:** add useDynamicList hook ([1d95c09](https://github.com/cloud-ru-tech/snack-uikit/commit/1d95c0969ef96e7b891e927543a38b0929edc760))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# 3.6.0 (2024-12-12)
|
|
7
18
|
|
|
8
19
|
|
package/README.md
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
Хук задерживает выполнение функции или обновление значения до тех пор,
|
|
20
20
|
пока не пройдет определенный период времени без новых вызовов.
|
|
21
|
+
## useDynamicList
|
|
22
|
+
`React hook`
|
|
23
|
+
|
|
24
|
+
Хук позволяет распределять элементы списка на две группы: видимые и невидимые,
|
|
25
|
+
в зависимости от ширины контейнера
|
|
21
26
|
## useEventHandler
|
|
22
27
|
`React hook`
|
|
23
28
|
|
|
@@ -66,6 +71,22 @@
|
|
|
66
71
|
| value | `TValue` | - | Значение состояния |
|
|
67
72
|
| defaultValue | `TValue` | - | Значение по умолчанию |
|
|
68
73
|
| onChange | `(value: TValue) => void` | - | Колбек, вызываемый на смену состояния |
|
|
74
|
+
## useSaveState
|
|
75
|
+
### Props
|
|
76
|
+
| name | type | default value | description |
|
|
77
|
+
|------|------|---------------|-------------|
|
|
78
|
+
| onLoadFilter* | `(filter: T) => void` | - | |
|
|
79
|
+
| filter* | `T` | - | |
|
|
80
|
+
| source | `BaseSource<T>` | - | |
|
|
81
|
+
## useSaveFilterState
|
|
82
|
+
### Props
|
|
83
|
+
| name | type | default value | description |
|
|
84
|
+
|------|------|---------------|-------------|
|
|
85
|
+
| onLoadFilter* | `(filter: TFilter) => void` | - | |
|
|
86
|
+
| filter* | `TFilter` | - | |
|
|
87
|
+
| options | `FilterStateOptions<TFilter>` | - | |
|
|
88
|
+
| parser | `(jsonFilter: string) => TFilter` | - | |
|
|
89
|
+
| encoder | `(filter: TFilter) => string` | - | |
|
|
69
90
|
## ThemeProvider
|
|
70
91
|
Провайдер, предназначенный для работы с темами
|
|
71
92
|
### Props
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from './useDebounce';
|
|
2
|
+
export * from './useDynamicList';
|
|
2
3
|
export * from './useEventHandler';
|
|
3
4
|
export * from './useIsomorphicLayoutEffect';
|
|
4
5
|
export * from './useSwipeable';
|
|
5
6
|
export * from './useThemeConfig';
|
|
6
7
|
export * from './useValueControl';
|
|
8
|
+
export * from './useSaveState';
|
|
9
|
+
export * from './useSaveFilterState';
|
package/dist/cjs/hooks/index.js
CHANGED
|
@@ -23,8 +23,11 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
23
23
|
value: true
|
|
24
24
|
});
|
|
25
25
|
__exportStar(require("./useDebounce"), exports);
|
|
26
|
+
__exportStar(require("./useDynamicList"), exports);
|
|
26
27
|
__exportStar(require("./useEventHandler"), exports);
|
|
27
28
|
__exportStar(require("./useIsomorphicLayoutEffect"), exports);
|
|
28
29
|
__exportStar(require("./useSwipeable"), exports);
|
|
29
30
|
__exportStar(require("./useThemeConfig"), exports);
|
|
30
|
-
__exportStar(require("./useValueControl"), exports);
|
|
31
|
+
__exportStar(require("./useValueControl"), exports);
|
|
32
|
+
__exportStar(require("./useSaveState"), exports);
|
|
33
|
+
__exportStar(require("./useSaveFilterState"), exports);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
type UseDynamicListProps<T extends object> = {
|
|
3
|
+
items: T[];
|
|
4
|
+
resizingContainerRef?: RefObject<HTMLDivElement>;
|
|
5
|
+
parentContainerRef: RefObject<HTMLDivElement>;
|
|
6
|
+
maxVisibleItems?: number;
|
|
7
|
+
};
|
|
8
|
+
type UseDynamicListReturnType<T extends object> = {
|
|
9
|
+
visibleItems: T[];
|
|
10
|
+
hiddenItems: T[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Хук позволяет распределять элементы списка на две группы: видимые и невидимые,
|
|
14
|
+
* в зависимости от ширины контейнера
|
|
15
|
+
* @function React hook
|
|
16
|
+
*/
|
|
17
|
+
export declare function useDynamicList<T extends object>({ parentContainerRef, resizingContainerRef, items, maxVisibleItems, }: UseDynamicListProps<T>): UseDynamicListReturnType<T>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useDynamicList = useDynamicList;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const useEventHandler_1 = require("./useEventHandler");
|
|
9
|
+
const useIsomorphicLayoutEffect_1 = require("./useIsomorphicLayoutEffect");
|
|
10
|
+
/**
|
|
11
|
+
* Хук позволяет распределять элементы списка на две группы: видимые и невидимые,
|
|
12
|
+
* в зависимости от ширины контейнера
|
|
13
|
+
* @function React hook
|
|
14
|
+
*/
|
|
15
|
+
function useDynamicList(_ref) {
|
|
16
|
+
let {
|
|
17
|
+
parentContainerRef,
|
|
18
|
+
resizingContainerRef = parentContainerRef,
|
|
19
|
+
items,
|
|
20
|
+
maxVisibleItems = Infinity
|
|
21
|
+
} = _ref;
|
|
22
|
+
const adjustAmount = value => Math.min(maxVisibleItems, value);
|
|
23
|
+
const [visibleItemsAmount, setVisibleItemsAmount] = (0, react_1.useState)(adjustAmount(items.length));
|
|
24
|
+
const [width, setWidth] = (0, react_1.useState)(Infinity);
|
|
25
|
+
const widthRef = (0, react_1.useRef)(width);
|
|
26
|
+
const handleVisibleItemsAmount = param => {
|
|
27
|
+
if (typeof param === 'number') {
|
|
28
|
+
setVisibleItemsAmount(adjustAmount(param));
|
|
29
|
+
} else {
|
|
30
|
+
setVisibleItemsAmount(v => param(adjustAmount(v)));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const tryHidingItem = (0, useEventHandler_1.useEventHandler)(() => {
|
|
34
|
+
const container = parentContainerRef.current;
|
|
35
|
+
if (container && container.scrollWidth - container.offsetWidth > 0) {
|
|
36
|
+
const itemToHide = items[visibleItemsAmount - 1];
|
|
37
|
+
if (itemToHide) {
|
|
38
|
+
handleVisibleItemsAmount(value => value - 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const tryShowingItem = (0, useEventHandler_1.useEventHandler)(() => {
|
|
43
|
+
const itemToShow = items[visibleItemsAmount];
|
|
44
|
+
if (itemToShow) {
|
|
45
|
+
handleVisibleItemsAmount(value => value + 1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const toggleItemWidth = (0, useEventHandler_1.useEventHandler)(_ref2 => {
|
|
49
|
+
let {
|
|
50
|
+
changedWidth,
|
|
51
|
+
initialWidth
|
|
52
|
+
} = _ref2;
|
|
53
|
+
if (changedWidth > initialWidth) {
|
|
54
|
+
//try to add extra item
|
|
55
|
+
if (visibleItemsAmount < maxVisibleItems) {
|
|
56
|
+
tryShowingItem();
|
|
57
|
+
}
|
|
58
|
+
} else if (changedWidth < initialWidth) {
|
|
59
|
+
// check if item should be hidden
|
|
60
|
+
tryHidingItem();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
(0, react_1.useEffect)(() => {
|
|
64
|
+
const listener = () => {
|
|
65
|
+
tryHidingItem();
|
|
66
|
+
if (parentContainerRef.current) {
|
|
67
|
+
setWidth(parentContainerRef.current.scrollWidth);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
document.fonts.addEventListener('loadingdone', listener);
|
|
71
|
+
return () => document.fonts.removeEventListener('loadingdone', listener);
|
|
72
|
+
}, [parentContainerRef, tryHidingItem]);
|
|
73
|
+
(0, react_1.useEffect)(() => {
|
|
74
|
+
const container = resizingContainerRef.current;
|
|
75
|
+
if (container) {
|
|
76
|
+
const observer = new ResizeObserver(entities => entities.forEach(entity => {
|
|
77
|
+
if (entity.target === container) {
|
|
78
|
+
const [{
|
|
79
|
+
inlineSize: newWidth
|
|
80
|
+
}] = entity.contentBoxSize;
|
|
81
|
+
setWidth(Math.floor(newWidth));
|
|
82
|
+
}
|
|
83
|
+
}));
|
|
84
|
+
observer.observe(container);
|
|
85
|
+
return () => observer.disconnect();
|
|
86
|
+
}
|
|
87
|
+
}, [resizingContainerRef]);
|
|
88
|
+
(0, useIsomorphicLayoutEffect_1.useLayoutEffect)(() => {
|
|
89
|
+
if (parentContainerRef.current) {
|
|
90
|
+
toggleItemWidth({
|
|
91
|
+
initialWidth: parentContainerRef.current.scrollWidth,
|
|
92
|
+
changedWidth: widthRef.current
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}, [items, parentContainerRef, toggleItemWidth]);
|
|
96
|
+
(0, useIsomorphicLayoutEffect_1.useLayoutEffect)(() => {
|
|
97
|
+
toggleItemWidth({
|
|
98
|
+
initialWidth: widthRef.current,
|
|
99
|
+
changedWidth: width
|
|
100
|
+
});
|
|
101
|
+
widthRef.current = width;
|
|
102
|
+
}, [width, toggleItemWidth]);
|
|
103
|
+
(0, useIsomorphicLayoutEffect_1.useLayoutEffect)(() => {
|
|
104
|
+
tryHidingItem();
|
|
105
|
+
}, [tryHidingItem, visibleItemsAmount]);
|
|
106
|
+
return {
|
|
107
|
+
visibleItems: items.slice(0, visibleItemsAmount),
|
|
108
|
+
hiddenItems: items.slice(visibleItemsAmount)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StateProps } from './useSaveState';
|
|
2
|
+
export type FilterStateOptions<T> = {
|
|
3
|
+
filterQueryKey: string;
|
|
4
|
+
filterLocalStorageKey: string;
|
|
5
|
+
validateData: (value: unknown) => value is T;
|
|
6
|
+
};
|
|
7
|
+
type FilterStateProps<TFilter> = Omit<StateProps<TFilter>, 'source'> & {
|
|
8
|
+
options?: FilterStateOptions<TFilter>;
|
|
9
|
+
parser?: (jsonFilter: string) => TFilter;
|
|
10
|
+
encoder?: (filter: TFilter) => string;
|
|
11
|
+
};
|
|
12
|
+
export declare const useSaveFilterState: <TFilter>({ options, filter, onLoadFilter }: FilterStateProps<TFilter>) => void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useSaveFilterState = void 0;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const useSaveState_1 = require("./useSaveState");
|
|
9
|
+
const useSaveFilterState = _ref => {
|
|
10
|
+
let {
|
|
11
|
+
options,
|
|
12
|
+
filter,
|
|
13
|
+
onLoadFilter
|
|
14
|
+
} = _ref;
|
|
15
|
+
const [queryFilter, setQueryFilter] = (0, react_1.useState)(null);
|
|
16
|
+
const [localStorageFilter, setLocalStorageFilter] = (0, react_1.useState)(null);
|
|
17
|
+
const [isLoaded, setIsLoaded] = (0, react_1.useState)({
|
|
18
|
+
localStorage: false,
|
|
19
|
+
queryParams: false
|
|
20
|
+
});
|
|
21
|
+
const onLoadQueryParams = (0, react_1.useCallback)(value => {
|
|
22
|
+
setQueryFilter(value);
|
|
23
|
+
setIsLoaded(prev => Object.assign(Object.assign({}, prev), {
|
|
24
|
+
queryParams: true
|
|
25
|
+
}));
|
|
26
|
+
}, []);
|
|
27
|
+
const onLoadLocalStorage = (0, react_1.useCallback)(value => {
|
|
28
|
+
setLocalStorageFilter(value);
|
|
29
|
+
setIsLoaded(prev => Object.assign(Object.assign({}, prev), {
|
|
30
|
+
localStorage: true
|
|
31
|
+
}));
|
|
32
|
+
}, []);
|
|
33
|
+
const querySource = (0, react_1.useMemo)(() => options ? new useSaveState_1.QueryParamSource(options.filterQueryKey, options.validateData) : undefined, [options]);
|
|
34
|
+
const localStorageSource = (0, react_1.useMemo)(() => options ? new useSaveState_1.LocalStorageSource(options.filterLocalStorageKey, options.validateData) : undefined, [options]);
|
|
35
|
+
(0, useSaveState_1.useSaveState)({
|
|
36
|
+
filter,
|
|
37
|
+
onLoadFilter: onLoadQueryParams,
|
|
38
|
+
source: querySource
|
|
39
|
+
});
|
|
40
|
+
(0, useSaveState_1.useSaveState)({
|
|
41
|
+
filter,
|
|
42
|
+
onLoadFilter: onLoadLocalStorage,
|
|
43
|
+
source: localStorageSource
|
|
44
|
+
});
|
|
45
|
+
(0, react_1.useEffect)(() => {
|
|
46
|
+
if (!options) {
|
|
47
|
+
onLoadFilter(null);
|
|
48
|
+
}
|
|
49
|
+
if (isLoaded.localStorage && isLoaded.queryParams) {
|
|
50
|
+
onLoadFilter(queryFilter ? queryFilter : localStorageFilter);
|
|
51
|
+
}
|
|
52
|
+
}, [queryFilter, onLoadFilter, localStorageFilter, isLoaded, options]);
|
|
53
|
+
};
|
|
54
|
+
exports.useSaveFilterState = useSaveFilterState;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var __createBinding = void 0 && (void 0).__createBinding || (Object.create ? function (o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
get: function () {
|
|
10
|
+
return m[k];
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
} : function (o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
});
|
|
19
|
+
var __exportStar = void 0 && (void 0).__exportStar || function (m, exports) {
|
|
20
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", {
|
|
23
|
+
value: true
|
|
24
|
+
});
|
|
25
|
+
__exportStar(require("./useSaveState"), exports);
|
|
26
|
+
__exportStar(require("./sources"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare abstract class BaseSource<TFilter> {
|
|
2
|
+
filterKey: string;
|
|
3
|
+
private validate;
|
|
4
|
+
protected parse: (value: string) => TFilter;
|
|
5
|
+
protected encode: (value: TFilter) => string;
|
|
6
|
+
protected abstract getFormSource(): string;
|
|
7
|
+
protected abstract setToSource(value: string): void;
|
|
8
|
+
constructor(filterKey: string, validate: (value: unknown) => value is TFilter, parse?: (value: string) => TFilter, encode?: (value: TFilter) => string);
|
|
9
|
+
getFilter: () => NonNullable<TFilter> | null;
|
|
10
|
+
setFilter: (filter: TFilter) => void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.BaseSource = void 0;
|
|
7
|
+
class BaseSource {
|
|
8
|
+
constructor(filterKey, validate, parse, encode) {
|
|
9
|
+
this.filterKey = filterKey;
|
|
10
|
+
this.validate = validate;
|
|
11
|
+
this.parse = JSON.parse;
|
|
12
|
+
this.encode = JSON.stringify;
|
|
13
|
+
this.getFilter = () => {
|
|
14
|
+
const filterFromQueryParams = this.getFormSource();
|
|
15
|
+
const parsedValue = filterFromQueryParams ? this.parse(filterFromQueryParams) : null;
|
|
16
|
+
const isValid = parsedValue && this.validate(parsedValue);
|
|
17
|
+
return isValid ? parsedValue : null;
|
|
18
|
+
};
|
|
19
|
+
this.setFilter = filter => {
|
|
20
|
+
const encodedValue = this.encode(filter);
|
|
21
|
+
this.setToSource(encodedValue);
|
|
22
|
+
};
|
|
23
|
+
if (parse) {
|
|
24
|
+
this.parse = parse;
|
|
25
|
+
}
|
|
26
|
+
if (encode) {
|
|
27
|
+
this.encode = encode;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.BaseSource = BaseSource;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var __createBinding = void 0 && (void 0).__createBinding || (Object.create ? function (o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
get: function () {
|
|
10
|
+
return m[k];
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
} : function (o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
});
|
|
19
|
+
var __exportStar = void 0 && (void 0).__exportStar || function (m, exports) {
|
|
20
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", {
|
|
23
|
+
value: true
|
|
24
|
+
});
|
|
25
|
+
__exportStar(require("./queryParamSource"), exports);
|
|
26
|
+
__exportStar(require("./localStorageSource"), exports);
|
|
27
|
+
__exportStar(require("./baseSource"), exports);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.LocalStorageSource = void 0;
|
|
7
|
+
const utils_1 = require("../../../utils");
|
|
8
|
+
const baseSource_1 = require("./baseSource");
|
|
9
|
+
class LocalStorageSource extends baseSource_1.BaseSource {
|
|
10
|
+
getFormSource() {
|
|
11
|
+
if ((0, utils_1.isBrowser)()) {
|
|
12
|
+
return localStorage.getItem(this.filterKey) || '';
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
setToSource(value) {
|
|
17
|
+
if ((0, utils_1.isBrowser)()) {
|
|
18
|
+
localStorage.setItem(this.filterKey, value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.LocalStorageSource = LocalStorageSource;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.QueryParamSource = void 0;
|
|
7
|
+
const utils_1 = require("../../../utils");
|
|
8
|
+
const baseSource_1 = require("./baseSource");
|
|
9
|
+
class QueryParamSource extends baseSource_1.BaseSource {
|
|
10
|
+
getFormSource() {
|
|
11
|
+
if ((0, utils_1.isBrowser)()) {
|
|
12
|
+
return new URL(window.location.href).searchParams.get(this.filterKey) || '';
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
setToSource(value) {
|
|
17
|
+
if ((0, utils_1.isBrowser)()) {
|
|
18
|
+
const url = new URL(window.location.href);
|
|
19
|
+
url.searchParams.set(this.filterKey, value);
|
|
20
|
+
history.pushState({}, '', url);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.QueryParamSource = QueryParamSource;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BaseSource } from './sources/baseSource';
|
|
2
|
+
export type StateProps<T> = {
|
|
3
|
+
filter: T;
|
|
4
|
+
onLoadFilter: (filter: T | null) => void;
|
|
5
|
+
source?: BaseSource<T>;
|
|
6
|
+
};
|
|
7
|
+
export declare const useSaveState: <T>({ filter, onLoadFilter, source }: StateProps<T>) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useSaveState = void 0;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const useSaveState = _ref => {
|
|
9
|
+
let {
|
|
10
|
+
filter,
|
|
11
|
+
onLoadFilter,
|
|
12
|
+
source
|
|
13
|
+
} = _ref;
|
|
14
|
+
(0, react_1.useEffect)(() => {
|
|
15
|
+
if (!source) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
onLoadFilter(source.getFilter());
|
|
19
|
+
}, [onLoadFilter, source]);
|
|
20
|
+
(0, react_1.useEffect)(() => {
|
|
21
|
+
if (!source) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
source.setFilter(filter);
|
|
25
|
+
}, [filter, source]);
|
|
26
|
+
};
|
|
27
|
+
exports.useSaveState = useSaveState;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from './useDebounce';
|
|
2
|
+
export * from './useDynamicList';
|
|
2
3
|
export * from './useEventHandler';
|
|
3
4
|
export * from './useIsomorphicLayoutEffect';
|
|
4
5
|
export * from './useSwipeable';
|
|
5
6
|
export * from './useThemeConfig';
|
|
6
7
|
export * from './useValueControl';
|
|
8
|
+
export * from './useSaveState';
|
|
9
|
+
export * from './useSaveFilterState';
|
package/dist/esm/hooks/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from './useDebounce';
|
|
2
|
+
export * from './useDynamicList';
|
|
2
3
|
export * from './useEventHandler';
|
|
3
4
|
export * from './useIsomorphicLayoutEffect';
|
|
4
5
|
export * from './useSwipeable';
|
|
5
6
|
export * from './useThemeConfig';
|
|
6
7
|
export * from './useValueControl';
|
|
8
|
+
export * from './useSaveState';
|
|
9
|
+
export * from './useSaveFilterState';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
type UseDynamicListProps<T extends object> = {
|
|
3
|
+
items: T[];
|
|
4
|
+
resizingContainerRef?: RefObject<HTMLDivElement>;
|
|
5
|
+
parentContainerRef: RefObject<HTMLDivElement>;
|
|
6
|
+
maxVisibleItems?: number;
|
|
7
|
+
};
|
|
8
|
+
type UseDynamicListReturnType<T extends object> = {
|
|
9
|
+
visibleItems: T[];
|
|
10
|
+
hiddenItems: T[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Хук позволяет распределять элементы списка на две группы: видимые и невидимые,
|
|
14
|
+
* в зависимости от ширины контейнера
|
|
15
|
+
* @function React hook
|
|
16
|
+
*/
|
|
17
|
+
export declare function useDynamicList<T extends object>({ parentContainerRef, resizingContainerRef, items, maxVisibleItems, }: UseDynamicListProps<T>): UseDynamicListReturnType<T>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useEventHandler } from './useEventHandler';
|
|
3
|
+
import { useLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
4
|
+
/**
|
|
5
|
+
* Хук позволяет распределять элементы списка на две группы: видимые и невидимые,
|
|
6
|
+
* в зависимости от ширины контейнера
|
|
7
|
+
* @function React hook
|
|
8
|
+
*/
|
|
9
|
+
export function useDynamicList({ parentContainerRef, resizingContainerRef = parentContainerRef, items, maxVisibleItems = Infinity, }) {
|
|
10
|
+
const adjustAmount = (value) => Math.min(maxVisibleItems, value);
|
|
11
|
+
const [visibleItemsAmount, setVisibleItemsAmount] = useState(adjustAmount(items.length));
|
|
12
|
+
const [width, setWidth] = useState(Infinity);
|
|
13
|
+
const widthRef = useRef(width);
|
|
14
|
+
const handleVisibleItemsAmount = param => {
|
|
15
|
+
if (typeof param === 'number') {
|
|
16
|
+
setVisibleItemsAmount(adjustAmount(param));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
setVisibleItemsAmount(v => param(adjustAmount(v)));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const tryHidingItem = useEventHandler(() => {
|
|
23
|
+
const container = parentContainerRef.current;
|
|
24
|
+
if (container && container.scrollWidth - container.offsetWidth > 0) {
|
|
25
|
+
const itemToHide = items[visibleItemsAmount - 1];
|
|
26
|
+
if (itemToHide) {
|
|
27
|
+
handleVisibleItemsAmount(value => value - 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const tryShowingItem = useEventHandler(() => {
|
|
32
|
+
const itemToShow = items[visibleItemsAmount];
|
|
33
|
+
if (itemToShow) {
|
|
34
|
+
handleVisibleItemsAmount(value => value + 1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
const toggleItemWidth = useEventHandler(({ changedWidth, initialWidth }) => {
|
|
38
|
+
if (changedWidth > initialWidth) {
|
|
39
|
+
//try to add extra item
|
|
40
|
+
if (visibleItemsAmount < maxVisibleItems) {
|
|
41
|
+
tryShowingItem();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (changedWidth < initialWidth) {
|
|
45
|
+
// check if item should be hidden
|
|
46
|
+
tryHidingItem();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const listener = () => {
|
|
51
|
+
tryHidingItem();
|
|
52
|
+
if (parentContainerRef.current) {
|
|
53
|
+
setWidth(parentContainerRef.current.scrollWidth);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
document.fonts.addEventListener('loadingdone', listener);
|
|
57
|
+
return () => document.fonts.removeEventListener('loadingdone', listener);
|
|
58
|
+
}, [parentContainerRef, tryHidingItem]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const container = resizingContainerRef.current;
|
|
61
|
+
if (container) {
|
|
62
|
+
const observer = new ResizeObserver(entities => entities.forEach(entity => {
|
|
63
|
+
if (entity.target === container) {
|
|
64
|
+
const [{ inlineSize: newWidth }] = entity.contentBoxSize;
|
|
65
|
+
setWidth(Math.floor(newWidth));
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
observer.observe(container);
|
|
69
|
+
return () => observer.disconnect();
|
|
70
|
+
}
|
|
71
|
+
}, [resizingContainerRef]);
|
|
72
|
+
useLayoutEffect(() => {
|
|
73
|
+
if (parentContainerRef.current) {
|
|
74
|
+
toggleItemWidth({
|
|
75
|
+
initialWidth: parentContainerRef.current.scrollWidth,
|
|
76
|
+
changedWidth: widthRef.current,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}, [items, parentContainerRef, toggleItemWidth]);
|
|
80
|
+
useLayoutEffect(() => {
|
|
81
|
+
toggleItemWidth({
|
|
82
|
+
initialWidth: widthRef.current,
|
|
83
|
+
changedWidth: width,
|
|
84
|
+
});
|
|
85
|
+
widthRef.current = width;
|
|
86
|
+
}, [width, toggleItemWidth]);
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
tryHidingItem();
|
|
89
|
+
}, [tryHidingItem, visibleItemsAmount]);
|
|
90
|
+
return {
|
|
91
|
+
visibleItems: items.slice(0, visibleItemsAmount),
|
|
92
|
+
hiddenItems: items.slice(visibleItemsAmount),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StateProps } from './useSaveState';
|
|
2
|
+
export type FilterStateOptions<T> = {
|
|
3
|
+
filterQueryKey: string;
|
|
4
|
+
filterLocalStorageKey: string;
|
|
5
|
+
validateData: (value: unknown) => value is T;
|
|
6
|
+
};
|
|
7
|
+
type FilterStateProps<TFilter> = Omit<StateProps<TFilter>, 'source'> & {
|
|
8
|
+
options?: FilterStateOptions<TFilter>;
|
|
9
|
+
parser?: (jsonFilter: string) => TFilter;
|
|
10
|
+
encoder?: (filter: TFilter) => string;
|
|
11
|
+
};
|
|
12
|
+
export declare const useSaveFilterState: <TFilter>({ options, filter, onLoadFilter }: FilterStateProps<TFilter>) => void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { LocalStorageSource, QueryParamSource, useSaveState } from './useSaveState';
|
|
3
|
+
export const useSaveFilterState = ({ options, filter, onLoadFilter }) => {
|
|
4
|
+
const [queryFilter, setQueryFilter] = useState(null);
|
|
5
|
+
const [localStorageFilter, setLocalStorageFilter] = useState(null);
|
|
6
|
+
const [isLoaded, setIsLoaded] = useState({
|
|
7
|
+
localStorage: false,
|
|
8
|
+
queryParams: false,
|
|
9
|
+
});
|
|
10
|
+
const onLoadQueryParams = useCallback((value) => {
|
|
11
|
+
setQueryFilter(value);
|
|
12
|
+
setIsLoaded(prev => (Object.assign(Object.assign({}, prev), { queryParams: true })));
|
|
13
|
+
}, []);
|
|
14
|
+
const onLoadLocalStorage = useCallback((value) => {
|
|
15
|
+
setLocalStorageFilter(value);
|
|
16
|
+
setIsLoaded(prev => (Object.assign(Object.assign({}, prev), { localStorage: true })));
|
|
17
|
+
}, []);
|
|
18
|
+
const querySource = useMemo(() => (options ? new QueryParamSource(options.filterQueryKey, options.validateData) : undefined), [options]);
|
|
19
|
+
const localStorageSource = useMemo(() => (options ? new LocalStorageSource(options.filterLocalStorageKey, options.validateData) : undefined), [options]);
|
|
20
|
+
useSaveState({ filter, onLoadFilter: onLoadQueryParams, source: querySource });
|
|
21
|
+
useSaveState({ filter, onLoadFilter: onLoadLocalStorage, source: localStorageSource });
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!options) {
|
|
24
|
+
onLoadFilter(null);
|
|
25
|
+
}
|
|
26
|
+
if (isLoaded.localStorage && isLoaded.queryParams) {
|
|
27
|
+
onLoadFilter(queryFilter ? queryFilter : localStorageFilter);
|
|
28
|
+
}
|
|
29
|
+
}, [queryFilter, onLoadFilter, localStorageFilter, isLoaded, options]);
|
|
30
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare abstract class BaseSource<TFilter> {
|
|
2
|
+
filterKey: string;
|
|
3
|
+
private validate;
|
|
4
|
+
protected parse: (value: string) => TFilter;
|
|
5
|
+
protected encode: (value: TFilter) => string;
|
|
6
|
+
protected abstract getFormSource(): string;
|
|
7
|
+
protected abstract setToSource(value: string): void;
|
|
8
|
+
constructor(filterKey: string, validate: (value: unknown) => value is TFilter, parse?: (value: string) => TFilter, encode?: (value: TFilter) => string);
|
|
9
|
+
getFilter: () => NonNullable<TFilter> | null;
|
|
10
|
+
setFilter: (filter: TFilter) => void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class BaseSource {
|
|
2
|
+
constructor(filterKey, validate, parse, encode) {
|
|
3
|
+
this.filterKey = filterKey;
|
|
4
|
+
this.validate = validate;
|
|
5
|
+
this.parse = JSON.parse;
|
|
6
|
+
this.encode = JSON.stringify;
|
|
7
|
+
this.getFilter = () => {
|
|
8
|
+
const filterFromQueryParams = this.getFormSource();
|
|
9
|
+
const parsedValue = filterFromQueryParams ? this.parse(filterFromQueryParams) : null;
|
|
10
|
+
const isValid = parsedValue && this.validate(parsedValue);
|
|
11
|
+
return isValid ? parsedValue : null;
|
|
12
|
+
};
|
|
13
|
+
this.setFilter = (filter) => {
|
|
14
|
+
const encodedValue = this.encode(filter);
|
|
15
|
+
this.setToSource(encodedValue);
|
|
16
|
+
};
|
|
17
|
+
if (parse) {
|
|
18
|
+
this.parse = parse;
|
|
19
|
+
}
|
|
20
|
+
if (encode) {
|
|
21
|
+
this.encode = encode;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isBrowser } from '../../../utils';
|
|
2
|
+
import { BaseSource } from './baseSource';
|
|
3
|
+
export class LocalStorageSource extends BaseSource {
|
|
4
|
+
getFormSource() {
|
|
5
|
+
if (isBrowser()) {
|
|
6
|
+
return localStorage.getItem(this.filterKey) || '';
|
|
7
|
+
}
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
setToSource(value) {
|
|
11
|
+
if (isBrowser()) {
|
|
12
|
+
localStorage.setItem(this.filterKey, value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isBrowser } from '../../../utils';
|
|
2
|
+
import { BaseSource } from './baseSource';
|
|
3
|
+
export class QueryParamSource extends BaseSource {
|
|
4
|
+
getFormSource() {
|
|
5
|
+
if (isBrowser()) {
|
|
6
|
+
return new URL(window.location.href).searchParams.get(this.filterKey) || '';
|
|
7
|
+
}
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
setToSource(value) {
|
|
11
|
+
if (isBrowser()) {
|
|
12
|
+
const url = new URL(window.location.href);
|
|
13
|
+
url.searchParams.set(this.filterKey, value);
|
|
14
|
+
history.pushState({}, '', url);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BaseSource } from './sources/baseSource';
|
|
2
|
+
export type StateProps<T> = {
|
|
3
|
+
filter: T;
|
|
4
|
+
onLoadFilter: (filter: T | null) => void;
|
|
5
|
+
source?: BaseSource<T>;
|
|
6
|
+
};
|
|
7
|
+
export declare const useSaveState: <T>({ filter, onLoadFilter, source }: StateProps<T>) => void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
export const useSaveState = ({ filter, onLoadFilter, source }) => {
|
|
3
|
+
useEffect(() => {
|
|
4
|
+
if (!source) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
onLoadFilter(source.getFilter());
|
|
8
|
+
}, [onLoadFilter, source]);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!source) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
source.setFilter(filter);
|
|
14
|
+
}, [filter, source]);
|
|
15
|
+
};
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"title": "Utils",
|
|
7
|
-
"version": "3.
|
|
7
|
+
"version": "3.7.1-preview-e840ab81.0",
|
|
8
8
|
"sideEffects": [
|
|
9
9
|
"*.css",
|
|
10
10
|
"*.woff",
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"react-swipeable": "7.0.2",
|
|
40
40
|
"uncontrollable": "8.0.4"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "1dc61e115ed2debadc06541b10d10df416d1d5e5"
|
|
43
43
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from './useDebounce';
|
|
2
|
+
export * from './useDynamicList';
|
|
2
3
|
export * from './useEventHandler';
|
|
3
4
|
export * from './useIsomorphicLayoutEffect';
|
|
4
5
|
export * from './useSwipeable';
|
|
5
6
|
export * from './useThemeConfig';
|
|
6
7
|
export * from './useValueControl';
|
|
8
|
+
export * from './useSaveState';
|
|
9
|
+
export * from './useSaveFilterState';
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { RefObject, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useEventHandler } from './useEventHandler';
|
|
4
|
+
import { useLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
5
|
+
|
|
6
|
+
type UseDynamicListProps<T extends object> = {
|
|
7
|
+
items: T[];
|
|
8
|
+
resizingContainerRef?: RefObject<HTMLDivElement>;
|
|
9
|
+
parentContainerRef: RefObject<HTMLDivElement>;
|
|
10
|
+
maxVisibleItems?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type UseDynamicListReturnType<T extends object> = {
|
|
14
|
+
visibleItems: T[];
|
|
15
|
+
hiddenItems: T[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Хук позволяет распределять элементы списка на две группы: видимые и невидимые,
|
|
20
|
+
* в зависимости от ширины контейнера
|
|
21
|
+
* @function React hook
|
|
22
|
+
*/
|
|
23
|
+
export function useDynamicList<T extends object>({
|
|
24
|
+
parentContainerRef,
|
|
25
|
+
resizingContainerRef = parentContainerRef,
|
|
26
|
+
items,
|
|
27
|
+
maxVisibleItems = Infinity,
|
|
28
|
+
}: UseDynamicListProps<T>): UseDynamicListReturnType<T> {
|
|
29
|
+
const adjustAmount = (value: number) => Math.min(maxVisibleItems, value);
|
|
30
|
+
|
|
31
|
+
const [visibleItemsAmount, setVisibleItemsAmount] = useState<number>(adjustAmount(items.length));
|
|
32
|
+
const [width, setWidth] = useState(Infinity);
|
|
33
|
+
const widthRef = useRef(width);
|
|
34
|
+
|
|
35
|
+
const handleVisibleItemsAmount: typeof setVisibleItemsAmount = param => {
|
|
36
|
+
if (typeof param === 'number') {
|
|
37
|
+
setVisibleItemsAmount(adjustAmount(param));
|
|
38
|
+
} else {
|
|
39
|
+
setVisibleItemsAmount(v => param(adjustAmount(v)));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const tryHidingItem = useEventHandler(() => {
|
|
44
|
+
const container = parentContainerRef.current;
|
|
45
|
+
|
|
46
|
+
if (container && container.scrollWidth - container.offsetWidth > 0) {
|
|
47
|
+
const itemToHide = items[visibleItemsAmount - 1];
|
|
48
|
+
|
|
49
|
+
if (itemToHide) {
|
|
50
|
+
handleVisibleItemsAmount(value => value - 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const tryShowingItem = useEventHandler(() => {
|
|
56
|
+
const itemToShow = items[visibleItemsAmount];
|
|
57
|
+
|
|
58
|
+
if (itemToShow) {
|
|
59
|
+
handleVisibleItemsAmount(value => value + 1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const toggleItemWidth = useEventHandler(
|
|
64
|
+
({ changedWidth, initialWidth }: { changedWidth: number; initialWidth: number }) => {
|
|
65
|
+
if (changedWidth > initialWidth) {
|
|
66
|
+
//try to add extra item
|
|
67
|
+
if (visibleItemsAmount < maxVisibleItems) {
|
|
68
|
+
tryShowingItem();
|
|
69
|
+
}
|
|
70
|
+
} else if (changedWidth < initialWidth) {
|
|
71
|
+
// check if item should be hidden
|
|
72
|
+
tryHidingItem();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const listener = () => {
|
|
79
|
+
tryHidingItem();
|
|
80
|
+
|
|
81
|
+
if (parentContainerRef.current) {
|
|
82
|
+
setWidth(parentContainerRef.current.scrollWidth);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
document.fonts.addEventListener('loadingdone', listener);
|
|
87
|
+
return () => document.fonts.removeEventListener('loadingdone', listener);
|
|
88
|
+
}, [parentContainerRef, tryHidingItem]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const container = resizingContainerRef.current;
|
|
92
|
+
|
|
93
|
+
if (container) {
|
|
94
|
+
const observer = new ResizeObserver(entities =>
|
|
95
|
+
entities.forEach(entity => {
|
|
96
|
+
if (entity.target === container) {
|
|
97
|
+
const [{ inlineSize: newWidth }] = entity.contentBoxSize;
|
|
98
|
+
setWidth(Math.floor(newWidth));
|
|
99
|
+
}
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
observer.observe(container);
|
|
104
|
+
|
|
105
|
+
return () => observer.disconnect();
|
|
106
|
+
}
|
|
107
|
+
}, [resizingContainerRef]);
|
|
108
|
+
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
if (parentContainerRef.current) {
|
|
111
|
+
toggleItemWidth({
|
|
112
|
+
initialWidth: parentContainerRef.current.scrollWidth,
|
|
113
|
+
changedWidth: widthRef.current,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}, [items, parentContainerRef, toggleItemWidth]);
|
|
117
|
+
|
|
118
|
+
useLayoutEffect(() => {
|
|
119
|
+
toggleItemWidth({
|
|
120
|
+
initialWidth: widthRef.current,
|
|
121
|
+
changedWidth: width,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
widthRef.current = width;
|
|
125
|
+
}, [width, toggleItemWidth]);
|
|
126
|
+
|
|
127
|
+
useLayoutEffect(() => {
|
|
128
|
+
tryHidingItem();
|
|
129
|
+
}, [tryHidingItem, visibleItemsAmount]);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
visibleItems: items.slice(0, visibleItemsAmount),
|
|
133
|
+
hiddenItems: items.slice(visibleItemsAmount),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { LocalStorageSource, QueryParamSource, StateProps, useSaveState } from './useSaveState';
|
|
4
|
+
|
|
5
|
+
export type FilterStateOptions<T> = {
|
|
6
|
+
filterQueryKey: string;
|
|
7
|
+
filterLocalStorageKey: string;
|
|
8
|
+
validateData: (value: unknown) => value is T;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type FilterStateProps<TFilter> = Omit<StateProps<TFilter>, 'source'> & {
|
|
12
|
+
options?: FilterStateOptions<TFilter>;
|
|
13
|
+
parser?: (jsonFilter: string) => TFilter;
|
|
14
|
+
encoder?: (filter: TFilter) => string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const useSaveFilterState = <TFilter>({ options, filter, onLoadFilter }: FilterStateProps<TFilter>) => {
|
|
18
|
+
const [queryFilter, setQueryFilter] = useState<TFilter | null>(null);
|
|
19
|
+
const [localStorageFilter, setLocalStorageFilter] = useState<TFilter | null>(null);
|
|
20
|
+
const [isLoaded, setIsLoaded] = useState<{ localStorage: boolean; queryParams: boolean }>({
|
|
21
|
+
localStorage: false,
|
|
22
|
+
queryParams: false,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const onLoadQueryParams = useCallback((value: TFilter | null) => {
|
|
26
|
+
setQueryFilter(value);
|
|
27
|
+
setIsLoaded(prev => ({ ...prev, queryParams: true }));
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const onLoadLocalStorage = useCallback((value: TFilter | null) => {
|
|
31
|
+
setLocalStorageFilter(value);
|
|
32
|
+
setIsLoaded(prev => ({ ...prev, localStorage: true }));
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const querySource = useMemo(
|
|
36
|
+
() => (options ? new QueryParamSource<TFilter>(options.filterQueryKey, options.validateData) : undefined),
|
|
37
|
+
[options],
|
|
38
|
+
);
|
|
39
|
+
const localStorageSource = useMemo(
|
|
40
|
+
() => (options ? new LocalStorageSource<TFilter>(options.filterLocalStorageKey, options.validateData) : undefined),
|
|
41
|
+
[options],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
useSaveState<TFilter>({ filter, onLoadFilter: onLoadQueryParams, source: querySource });
|
|
45
|
+
useSaveState<TFilter>({ filter, onLoadFilter: onLoadLocalStorage, source: localStorageSource });
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!options) {
|
|
49
|
+
onLoadFilter(null);
|
|
50
|
+
}
|
|
51
|
+
if (isLoaded.localStorage && isLoaded.queryParams) {
|
|
52
|
+
onLoadFilter(queryFilter ? queryFilter : localStorageFilter);
|
|
53
|
+
}
|
|
54
|
+
}, [queryFilter, onLoadFilter, localStorageFilter, isLoaded, options]);
|
|
55
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export abstract class BaseSource<TFilter> {
|
|
2
|
+
protected parse: (value: string) => TFilter = JSON.parse;
|
|
3
|
+
protected encode: (value: TFilter) => string = JSON.stringify;
|
|
4
|
+
protected abstract getFormSource(): string;
|
|
5
|
+
protected abstract setToSource(value: string): void;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
public filterKey: string,
|
|
9
|
+
private validate: (value: unknown) => value is TFilter,
|
|
10
|
+
parse?: (value: string) => TFilter,
|
|
11
|
+
encode?: (value: TFilter) => string,
|
|
12
|
+
) {
|
|
13
|
+
if (parse) {
|
|
14
|
+
this.parse = parse;
|
|
15
|
+
}
|
|
16
|
+
if (encode) {
|
|
17
|
+
this.encode = encode;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getFilter = () => {
|
|
22
|
+
const filterFromQueryParams = this.getFormSource();
|
|
23
|
+
const parsedValue = filterFromQueryParams ? (this.parse(filterFromQueryParams) as TFilter) : null;
|
|
24
|
+
const isValid = parsedValue && this.validate(parsedValue);
|
|
25
|
+
return isValid ? parsedValue : null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
setFilter = (filter: TFilter) => {
|
|
29
|
+
const encodedValue = this.encode(filter);
|
|
30
|
+
this.setToSource(encodedValue);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isBrowser } from '../../../utils';
|
|
2
|
+
import { BaseSource } from './baseSource';
|
|
3
|
+
|
|
4
|
+
export class LocalStorageSource<TFilter> extends BaseSource<TFilter> {
|
|
5
|
+
getFormSource(): string {
|
|
6
|
+
if (isBrowser()) {
|
|
7
|
+
return localStorage.getItem(this.filterKey) || '';
|
|
8
|
+
}
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setToSource(value: string): void {
|
|
13
|
+
if (isBrowser()) {
|
|
14
|
+
localStorage.setItem(this.filterKey, value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isBrowser } from '../../../utils';
|
|
2
|
+
import { BaseSource } from './baseSource';
|
|
3
|
+
|
|
4
|
+
export class QueryParamSource<TFilter> extends BaseSource<TFilter> {
|
|
5
|
+
getFormSource(): string {
|
|
6
|
+
if (isBrowser()) {
|
|
7
|
+
return new URL(window.location.href).searchParams.get(this.filterKey) || '';
|
|
8
|
+
}
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setToSource(value: string): void {
|
|
13
|
+
if (isBrowser()) {
|
|
14
|
+
const url = new URL(window.location.href);
|
|
15
|
+
url.searchParams.set(this.filterKey, value);
|
|
16
|
+
history.pushState({}, '', url);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { BaseSource } from './sources/baseSource';
|
|
4
|
+
|
|
5
|
+
export type StateProps<T> = {
|
|
6
|
+
filter: T;
|
|
7
|
+
onLoadFilter: (filter: T | null) => void;
|
|
8
|
+
source?: BaseSource<T>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const useSaveState = <T>({ filter, onLoadFilter, source }: StateProps<T>) => {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!source) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
onLoadFilter(source.getFilter());
|
|
17
|
+
}, [onLoadFilter, source]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!source) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
source.setFilter(filter);
|
|
24
|
+
}, [filter, source]);
|
|
25
|
+
};
|