@codeleap/web 3.14.3 → 3.15.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/package.json +1 -1
- package/src/components/CropPicker/index.tsx +8 -2
- package/src/components/CropPicker/useCropPicker.tsx +2 -3
- package/src/components/SegmentedControl/index.tsx +24 -2
- package/src/components/Tag/index.tsx +126 -0
- package/src/components/Tag/styles.ts +23 -0
- package/src/components/Tag/types.ts +29 -0
- package/src/components/components.ts +1 -0
- package/src/components/defaultStyles.ts +2 -0
- package/src/index.ts +2 -0
- package/src/lib/OSAlert.tsx +20 -1
- package/src/lib/index.ts +1 -0
- package/src/lib/keyboard.ts +41 -0
- package/src/lib/localStorage.ts +191 -0
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import { Modal, Button, FileInput, FileInputRef } from '../components'
|
|
|
10
10
|
|
|
11
11
|
const ReactCrop: React.Component = require('react-image-crop').Component
|
|
12
12
|
import 'react-image-crop/dist/ReactCrop.css'
|
|
13
|
+
import { ComponentWithDefaultProps } from '../../types'
|
|
13
14
|
|
|
14
15
|
export * from './styles'
|
|
15
16
|
export * from './types'
|
|
@@ -18,6 +19,11 @@ export * from './useCropPicker'
|
|
|
18
19
|
|
|
19
20
|
export const _CropPicker = forwardRef<FileInputRef, CropPickerProps>(
|
|
20
21
|
(props: CropPickerProps, ref) => {
|
|
22
|
+
const allProps = {
|
|
23
|
+
...CropPicker.defaultProps,
|
|
24
|
+
...props,
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
const {
|
|
22
28
|
onFileSelect,
|
|
23
29
|
targetCrop,
|
|
@@ -30,7 +36,7 @@ export const _CropPicker = forwardRef<FileInputRef, CropPickerProps>(
|
|
|
30
36
|
debugName,
|
|
31
37
|
handle,
|
|
32
38
|
...fileInputProps
|
|
33
|
-
} =
|
|
39
|
+
} = allProps
|
|
34
40
|
|
|
35
41
|
const {
|
|
36
42
|
onConfirmCrop,
|
|
@@ -98,4 +104,4 @@ export const _CropPicker = forwardRef<FileInputRef, CropPickerProps>(
|
|
|
98
104
|
},
|
|
99
105
|
)
|
|
100
106
|
|
|
101
|
-
export const CropPicker = React.memo(_CropPicker) as
|
|
107
|
+
export const CropPicker = React.memo(_CropPicker) as ComponentWithDefaultProps<CropPickerProps>
|
|
@@ -33,9 +33,10 @@ export function useCropPicker({
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const cleanup = () => {
|
|
36
|
-
|
|
36
|
+
toggle()
|
|
37
37
|
setRelativeCrop(null)
|
|
38
38
|
setCrop(undefined)
|
|
39
|
+
setTimeout(() => setImage(null), 500)
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const onConfirmCrop = async () => {
|
|
@@ -46,7 +47,6 @@ export function useCropPicker({
|
|
|
46
47
|
preview,
|
|
47
48
|
},
|
|
48
49
|
])
|
|
49
|
-
toggle()
|
|
50
50
|
setTimeout(() => cleanup())
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -80,7 +80,6 @@ export function useCropPicker({
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const onClose = () => {
|
|
83
|
-
toggle()
|
|
84
83
|
onCancel()
|
|
85
84
|
setTimeout(() => cleanup())
|
|
86
85
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View } from '../View'
|
|
3
3
|
import { SegmentedControlOption } from './SegmentedControlOption'
|
|
4
|
-
import { ComponentVariants, useDefaultComponentStyle, PropsOf, IconPlaceholder, StylesOf, useRef } from '@codeleap/common'
|
|
4
|
+
import { ComponentVariants, useDefaultComponentStyle, PropsOf, IconPlaceholder, StylesOf, useRef, TypeGuards } from '@codeleap/common'
|
|
5
5
|
import { SegmentedControlPresets } from './styles'
|
|
6
6
|
import { Text } from '../Text'
|
|
7
7
|
import { Touchable } from '../Touchable'
|
|
@@ -49,6 +49,8 @@ export type SegmentedControlProps<T = string> = ComponentVariants<typeof Segment
|
|
|
49
49
|
RenderAnimatedView?: ForwardRefComponent<HTMLDivElement, any>
|
|
50
50
|
textProps?: Omit<PropsOf<typeof Text>, 'key'>
|
|
51
51
|
iconProps?: Partial<IconProps>
|
|
52
|
+
debounce?: number
|
|
53
|
+
debounceEnabled?: boolean
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
const defaultProps: Partial<SegmentedControlProps> = {
|
|
@@ -56,6 +58,8 @@ const defaultProps: Partial<SegmentedControlProps> = {
|
|
|
56
58
|
transitionDuration: 0.2,
|
|
57
59
|
disabled: false,
|
|
58
60
|
RenderAnimatedView: motion.div,
|
|
61
|
+
debounce: 1000,
|
|
62
|
+
debounceEnabled: true,
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
export const SegmentedControl = (props: SegmentedControlProps) => {
|
|
@@ -81,6 +85,8 @@ export const SegmentedControl = (props: SegmentedControlProps) => {
|
|
|
81
85
|
textProps = {},
|
|
82
86
|
iconProps = {},
|
|
83
87
|
debugName,
|
|
88
|
+
debounce,
|
|
89
|
+
debounceEnabled,
|
|
84
90
|
...rest
|
|
85
91
|
} = allProps
|
|
86
92
|
|
|
@@ -98,6 +104,7 @@ export const SegmentedControl = (props: SegmentedControlProps) => {
|
|
|
98
104
|
}, [value])
|
|
99
105
|
|
|
100
106
|
const maxDivWidthRef = useRef(null)
|
|
107
|
+
const sectionPressedRef = useRef(null)
|
|
101
108
|
|
|
102
109
|
const largestWidth = React.useMemo(() => {
|
|
103
110
|
return {
|
|
@@ -128,6 +135,21 @@ export const SegmentedControl = (props: SegmentedControlProps) => {
|
|
|
128
135
|
largestWidth,
|
|
129
136
|
]
|
|
130
137
|
|
|
138
|
+
const onSelectTab = (option: SegmentedControlOptionProps) => {
|
|
139
|
+
if (!debounceEnabled || !TypeGuards.isNumber(debounce)) {
|
|
140
|
+
onValueChange(option.value)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (sectionPressedRef.current !== null) return
|
|
145
|
+
|
|
146
|
+
onValueChange(option.value)
|
|
147
|
+
sectionPressedRef.current = setTimeout(() => {
|
|
148
|
+
clearTimeout(sectionPressedRef.current)
|
|
149
|
+
sectionPressedRef.current = null
|
|
150
|
+
}, debounce)
|
|
151
|
+
}
|
|
152
|
+
|
|
131
153
|
return (
|
|
132
154
|
<View css={[variantStyles.wrapper, style]} {...rest}>
|
|
133
155
|
{label && <Text text={label} css={[variantStyles.label, disabled && variantStyles['label:disabled']]} />}
|
|
@@ -148,7 +170,7 @@ export const SegmentedControl = (props: SegmentedControlProps) => {
|
|
|
148
170
|
debugName={debugName}
|
|
149
171
|
label={o.label}
|
|
150
172
|
value={o.value}
|
|
151
|
-
onPress={() =>
|
|
173
|
+
onPress={() => onSelectTab(o)}
|
|
152
174
|
key={idx}
|
|
153
175
|
icon={o.icon}
|
|
154
176
|
selected={value === o.value}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { TypeGuards, useDefaultComponentStyle, useNestedStylesByKey } from '@codeleap/common'
|
|
3
|
+
import { TagParts, TagPresets } from './styles'
|
|
4
|
+
import { TagProps } from './types'
|
|
5
|
+
import { Icon } from '../Icon'
|
|
6
|
+
import { Text } from '../Text'
|
|
7
|
+
import { Touchable } from '../Touchable'
|
|
8
|
+
import { View } from '../View'
|
|
9
|
+
import { Badge } from '../Badge'
|
|
10
|
+
|
|
11
|
+
export * from './styles'
|
|
12
|
+
export * from './types'
|
|
13
|
+
|
|
14
|
+
const defaultProps: Partial<TagProps> = {
|
|
15
|
+
debugName: 'Tag component',
|
|
16
|
+
disabled: false,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Tag = (props: TagProps) => {
|
|
20
|
+
const allProps = {
|
|
21
|
+
...Tag.defaultProps,
|
|
22
|
+
...props,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
variants,
|
|
27
|
+
rightComponent,
|
|
28
|
+
leftComponent,
|
|
29
|
+
responsiveVariants,
|
|
30
|
+
styles,
|
|
31
|
+
style,
|
|
32
|
+
css,
|
|
33
|
+
leftIcon,
|
|
34
|
+
text,
|
|
35
|
+
textProps,
|
|
36
|
+
rightIcon,
|
|
37
|
+
rightIconProps,
|
|
38
|
+
leftIconProps,
|
|
39
|
+
leftBadgeProps,
|
|
40
|
+
leftBadge,
|
|
41
|
+
rightBadge,
|
|
42
|
+
rightBadgeProps,
|
|
43
|
+
children,
|
|
44
|
+
onPress,
|
|
45
|
+
disabled,
|
|
46
|
+
...touchableProps
|
|
47
|
+
} = allProps
|
|
48
|
+
|
|
49
|
+
const variantStyles = useDefaultComponentStyle<'u:Tag', typeof TagPresets>('u:Tag', {
|
|
50
|
+
variants,
|
|
51
|
+
responsiveVariants,
|
|
52
|
+
styles,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const leftBadgeStyles = useNestedStylesByKey('leftBadge', variantStyles)
|
|
56
|
+
const rightBadgeStyles = useNestedStylesByKey('rightBadge', variantStyles)
|
|
57
|
+
|
|
58
|
+
const isPressable = TypeGuards.isFunction(onPress)
|
|
59
|
+
|
|
60
|
+
const Wrapper: any = isPressable ? Touchable : View
|
|
61
|
+
|
|
62
|
+
const wrapperProps = isPressable ? { onPress, ...touchableProps } : touchableProps
|
|
63
|
+
|
|
64
|
+
const getStylesByKey = (styleKey: TagParts) => ([
|
|
65
|
+
variantStyles?.[styleKey],
|
|
66
|
+
isPressable && variantStyles[`${styleKey}:pressable`],
|
|
67
|
+
disabled && variantStyles[`${styleKey}:disabled`],
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
const wrapperStyles = React.useMemo(() => ([
|
|
71
|
+
getStylesByKey('wrapper'),
|
|
72
|
+
css,
|
|
73
|
+
style,
|
|
74
|
+
]), [variantStyles, disabled, isPressable, style])
|
|
75
|
+
|
|
76
|
+
const textStyles = React.useMemo(() => getStylesByKey('text'), [variantStyles, disabled, isPressable])
|
|
77
|
+
const leftIconStyles = React.useMemo(() => getStylesByKey('leftIcon'), [variantStyles, disabled, isPressable])
|
|
78
|
+
const rightIconStyles = React.useMemo(() => getStylesByKey('rightIcon'), [variantStyles, disabled, isPressable])
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Wrapper css={wrapperStyles} disabled={disabled} {...wrapperProps}>
|
|
82
|
+
{leftComponent}
|
|
83
|
+
{leftBadge && (
|
|
84
|
+
<Badge
|
|
85
|
+
debugName={`${touchableProps?.debugName}:leftBadge`}
|
|
86
|
+
styles={leftBadgeStyles}
|
|
87
|
+
badge={leftBadge}
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
{...leftBadgeProps}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
{!TypeGuards.isNil(leftIcon) && (
|
|
93
|
+
<Icon
|
|
94
|
+
debugName={`${touchableProps?.debugName}:leftIcon`}
|
|
95
|
+
css={leftIconStyles}
|
|
96
|
+
name={leftIcon}
|
|
97
|
+
{...leftIconProps}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{TypeGuards.isString(text) ? <Text text={text} css={textStyles} {...textProps} /> : text}
|
|
102
|
+
{children}
|
|
103
|
+
|
|
104
|
+
{!TypeGuards.isNil(rightIcon) && (
|
|
105
|
+
<Icon
|
|
106
|
+
debugName={`${touchableProps?.debugName}:rightIcon`}
|
|
107
|
+
css={rightIconStyles}
|
|
108
|
+
name={rightIcon}
|
|
109
|
+
{...rightIconProps}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
{rightBadge && (
|
|
113
|
+
<Badge
|
|
114
|
+
debugName={`${touchableProps?.debugName}:rightBadge`}
|
|
115
|
+
styles={rightBadgeStyles}
|
|
116
|
+
badge={rightBadge}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
{...rightBadgeProps}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
{rightComponent}
|
|
122
|
+
</Wrapper>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Tag.defaultProps = defaultProps
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createDefaultVariantFactory, includePresets } from '@codeleap/common'
|
|
2
|
+
import { BadgeComposition } from '../Badge'
|
|
3
|
+
|
|
4
|
+
export type TagStates = 'pressable' | 'disabled'
|
|
5
|
+
|
|
6
|
+
type TagBadgeParts =
|
|
7
|
+
| `leftBadge${Capitalize<BadgeComposition>}`
|
|
8
|
+
| `rightBadge${Capitalize<BadgeComposition>}`
|
|
9
|
+
|
|
10
|
+
export type TagParts =
|
|
11
|
+
| `wrapper`
|
|
12
|
+
| 'text'
|
|
13
|
+
| 'leftIcon'
|
|
14
|
+
| 'rightIcon'
|
|
15
|
+
|
|
16
|
+
export type TagComposition = TagParts | TagBadgeParts | `${TagParts}:${TagStates}`
|
|
17
|
+
|
|
18
|
+
const createTagStyle = createDefaultVariantFactory<TagComposition>()
|
|
19
|
+
|
|
20
|
+
export const TagPresets = includePresets((styles) => createTagStyle(() => ({
|
|
21
|
+
wrapper: styles,
|
|
22
|
+
})),
|
|
23
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StylesOf, ComponentVariants, IconPlaceholder } from '@codeleap/common'
|
|
2
|
+
import { ReactElement } from 'react'
|
|
3
|
+
import { TagComposition, TagPresets } from './styles'
|
|
4
|
+
import { BadgeProps } from '../Badge'
|
|
5
|
+
import { TextProps } from '../Text'
|
|
6
|
+
import { IconProps } from '../Icon'
|
|
7
|
+
import { ComponentCommonProps } from '../../types'
|
|
8
|
+
import { TouchableProps } from '../Touchable'
|
|
9
|
+
import { ViewProps } from '../View'
|
|
10
|
+
|
|
11
|
+
export type TagProps =
|
|
12
|
+
Omit<ViewProps<'div'>, 'styles' | 'variants' | 'responsiveVariants'> &
|
|
13
|
+
Omit<TouchableProps, 'styles' | 'variants' | 'responsiveVariants'> &
|
|
14
|
+
ComponentVariants<typeof TagPresets> &
|
|
15
|
+
ComponentCommonProps & {
|
|
16
|
+
styles?: StylesOf<TagComposition>
|
|
17
|
+
text?: TextProps<'p'>['text'] | ReactElement
|
|
18
|
+
textProps?: Partial<TextProps<'p'>>
|
|
19
|
+
leftIcon?: IconPlaceholder
|
|
20
|
+
leftIconProps?: Partial<IconProps>
|
|
21
|
+
rightIcon?: IconPlaceholder
|
|
22
|
+
rightIconProps?: Partial<IconProps>
|
|
23
|
+
leftComponent?: ReactElement
|
|
24
|
+
rightComponent?: ReactElement
|
|
25
|
+
leftBadge?: BadgeProps['badge']
|
|
26
|
+
rightBadge?: BadgeProps['badge']
|
|
27
|
+
leftBadgeProps?: Partial<BadgeProps>
|
|
28
|
+
rightBadgeProps?: Partial<BadgeProps>
|
|
29
|
+
}
|
|
@@ -27,6 +27,7 @@ import { EmptyPlaceholderPresets } from './EmptyPlaceholder/styles'
|
|
|
27
27
|
import { GridPresets } from './Grid/styles'
|
|
28
28
|
import { BadgePresets } from './Badge/styles'
|
|
29
29
|
import { CropPickerPresets } from './CropPicker'
|
|
30
|
+
import { TagPresets } from './Tag/styles'
|
|
30
31
|
|
|
31
32
|
export const defaultStyles = {
|
|
32
33
|
View: ViewPresets,
|
|
@@ -61,6 +62,7 @@ export const defaultStyles = {
|
|
|
61
62
|
Badge: BadgePresets,
|
|
62
63
|
Dropzone: DropzonePresets,
|
|
63
64
|
CropPicker: CropPickerPresets,
|
|
65
|
+
Tag: TagPresets,
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
import createCache from '@emotion/cache'
|
package/src/index.ts
CHANGED
|
@@ -8,3 +8,5 @@ export * from './lib/usePopState'
|
|
|
8
8
|
export { default as Toast } from './lib/Toast'
|
|
9
9
|
export { CreateOSAlert, useGlobalAlertComponent, AlertOutlet } from './lib/OSAlert'
|
|
10
10
|
export type { GlobalAlertComponentProps, GlobalAlertType } from './lib/OSAlert'
|
|
11
|
+
export * from './lib/keyboard'
|
|
12
|
+
export * from './lib/localStorage'
|
package/src/lib/OSAlert.tsx
CHANGED
|
@@ -14,12 +14,13 @@ type OSAlertArgs = {
|
|
|
14
14
|
options?: AlertButton[]
|
|
15
15
|
onDismiss?: AnyFunction
|
|
16
16
|
onAction?: AnyFunction
|
|
17
|
+
type?: Exclude<GlobalAlertType, 'custom'> | string
|
|
17
18
|
}
|
|
18
19
|
type AlertEvent = AlertButton['onPress']
|
|
19
20
|
|
|
20
21
|
type NamedEvents<E extends string> = Partial<Record<E, AlertEvent>>
|
|
21
22
|
|
|
22
|
-
export type GlobalAlertType = 'info' | 'error' | 'warn' | 'ask'
|
|
23
|
+
export type GlobalAlertType = 'info' | 'error' | 'warn' | 'ask' | 'custom'
|
|
23
24
|
|
|
24
25
|
export type GlobalAlertComponentProps = {
|
|
25
26
|
args: OSAlertArgs
|
|
@@ -166,11 +167,29 @@ export function CreateOSAlert(Component) {
|
|
|
166
167
|
onDismiss,
|
|
167
168
|
})
|
|
168
169
|
}
|
|
170
|
+
|
|
171
|
+
function custom(args: OSAlertArgs & {type: string}) {
|
|
172
|
+
const {
|
|
173
|
+
title = 'Hang on',
|
|
174
|
+
body = 'Are you sure?',
|
|
175
|
+
type,
|
|
176
|
+
...rest
|
|
177
|
+
} = args
|
|
178
|
+
|
|
179
|
+
OSAlert({
|
|
180
|
+
title,
|
|
181
|
+
body,
|
|
182
|
+
type: type as GlobalAlertType,
|
|
183
|
+
...rest,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
169
187
|
return {
|
|
170
188
|
ask,
|
|
171
189
|
warn,
|
|
172
190
|
info,
|
|
173
191
|
error: OSError,
|
|
192
|
+
custom,
|
|
174
193
|
}
|
|
175
194
|
}
|
|
176
195
|
|
package/src/lib/index.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { onUpdate, TypeGuards } from '@codeleap/common'
|
|
2
|
+
|
|
3
|
+
export const keydownDefaultKeyOptions = {
|
|
4
|
+
ArrowLeft: 'ArrowLeft',
|
|
5
|
+
ArrowRight: 'ArrowRight',
|
|
6
|
+
ArrowUp: 'ArrowUp',
|
|
7
|
+
ArrowDown: 'ArrowDown',
|
|
8
|
+
Enter: 'Enter',
|
|
9
|
+
Space: {
|
|
10
|
+
key: ' ',
|
|
11
|
+
code: 'Space',
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useKeydown(
|
|
16
|
+
expectedKey: keyof typeof useKeydown.keys | { key: string; code: string },
|
|
17
|
+
handler: (event: KeyboardEvent) => void,
|
|
18
|
+
deps: Array<any> = [],
|
|
19
|
+
options?: boolean | AddEventListenerOptions
|
|
20
|
+
) {
|
|
21
|
+
const eventKey = TypeGuards.isString(expectedKey) ? (useKeydown?.keys?.[expectedKey] ?? expectedKey) : expectedKey
|
|
22
|
+
|
|
23
|
+
const handleKeyPress = (event: KeyboardEvent) => {
|
|
24
|
+
const { key, code } = TypeGuards.isString(eventKey) ? { key: eventKey, code: eventKey } : eventKey
|
|
25
|
+
|
|
26
|
+
if (event?.key === key || event?.code === code) {
|
|
27
|
+
handler?.(event)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onUpdate(() => {
|
|
32
|
+
document.addEventListener('keydown', handleKeyPress, options)
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
document.removeEventListener('keydown', handleKeyPress)
|
|
36
|
+
}
|
|
37
|
+
}, deps)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
useKeydown.keys = keydownDefaultKeyOptions
|
|
41
|
+
useKeydown.defaultKeyOptions = keydownDefaultKeyOptions
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { onMount, TypeGuards, useState } from '@codeleap/common'
|
|
3
|
+
|
|
4
|
+
export type LocalStorageHandler<T> = (key: T, event: StorageEvent, value: any) => void
|
|
5
|
+
export type Key<T> = keyof T
|
|
6
|
+
|
|
7
|
+
type UseLocalStorageOptions = {
|
|
8
|
+
disableListen?: boolean
|
|
9
|
+
setItemValueOnMutate?: boolean
|
|
10
|
+
getItemValueOnMount?: boolean
|
|
11
|
+
parseValueOnGet?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class LocalStorage<T extends Record<string, string>> {
|
|
15
|
+
public storageKeys: T
|
|
16
|
+
|
|
17
|
+
private storageListeners: ((event: StorageEvent) => void)[] = []
|
|
18
|
+
|
|
19
|
+
constructor(keys: T) {
|
|
20
|
+
this.storageKeys = keys
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public getLocalStorage(): Storage {
|
|
24
|
+
if (typeof window === 'undefined') {
|
|
25
|
+
return {} as Storage
|
|
26
|
+
}
|
|
27
|
+
return localStorage
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public getStorageKey(key: Key<T>): string {
|
|
31
|
+
return String(this.storageKeys[key] ?? key)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private parseValue(value: any) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(value)
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private serializeValue(value: any): string {
|
|
43
|
+
if (TypeGuards.isString(value)) return value
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.stringify(value)
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public replaceItem(key: Key<T>, value: any): string {
|
|
53
|
+
const storageKey = this.getStorageKey(key)
|
|
54
|
+
const storage = this.getLocalStorage()
|
|
55
|
+
storage.removeItem(storageKey)
|
|
56
|
+
const serializedValue = this.serializeValue(value)
|
|
57
|
+
storage.setItem(storageKey, serializedValue)
|
|
58
|
+
return serializedValue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public getItem(key: Key<T>, parseValue = true): string | null {
|
|
62
|
+
const storageKey = this.getStorageKey(key)
|
|
63
|
+
const storage = this.getLocalStorage()
|
|
64
|
+
|
|
65
|
+
let value = storage.getItem(storageKey)
|
|
66
|
+
|
|
67
|
+
if (parseValue) {
|
|
68
|
+
value = this.parseValue(value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return value
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public removeItem(key: Key<T>): void {
|
|
75
|
+
const storageKey = this.getStorageKey(key)
|
|
76
|
+
const storage = this.getLocalStorage()
|
|
77
|
+
storage.removeItem(storageKey)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public setItem(key: Key<T>, value: any): string {
|
|
81
|
+
const storageKey = this.getStorageKey(key)
|
|
82
|
+
const storage = this.getLocalStorage()
|
|
83
|
+
const serializedValue = this.serializeValue(value)
|
|
84
|
+
storage.setItem(storageKey, serializedValue)
|
|
85
|
+
return serializedValue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public clear(): void {
|
|
89
|
+
const storage = this.getLocalStorage()
|
|
90
|
+
storage.clear()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public multiSet(keyValuePairs: Array<[Key<T>, any]>): void {
|
|
94
|
+
for (const [key, value] of keyValuePairs) {
|
|
95
|
+
this.setItem(key, value)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public multiRemove(keys: Key<T>[]): void {
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
this.removeItem(key)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public multiGet(keys: Key<T>[]): Record<string, any> {
|
|
106
|
+
const storage = this.getLocalStorage()
|
|
107
|
+
const values: Record<string, any> = {}
|
|
108
|
+
|
|
109
|
+
for (const key of keys) {
|
|
110
|
+
const storageKey = this.getStorageKey(key)
|
|
111
|
+
const value = storage.getItem(storageKey)
|
|
112
|
+
|
|
113
|
+
values[key as string] = value
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return values
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public use<S = any>(
|
|
120
|
+
key: Key<T>,
|
|
121
|
+
initialValue: any = null,
|
|
122
|
+
options: UseLocalStorageOptions = {}
|
|
123
|
+
): [S, (to: S | ((prev: S) => S)) => any] {
|
|
124
|
+
const {
|
|
125
|
+
disableListen = false,
|
|
126
|
+
setItemValueOnMutate = true,
|
|
127
|
+
getItemValueOnMount = true,
|
|
128
|
+
parseValueOnGet = true,
|
|
129
|
+
} = options
|
|
130
|
+
|
|
131
|
+
const [value, _setValue] = useState<S>(() => {
|
|
132
|
+
return getItemValueOnMount ? (this.getItem(key, parseValueOnGet) ?? initialValue) : initialValue
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
onMount(() => {
|
|
136
|
+
const handler = () => {
|
|
137
|
+
let _initialValue = initialValue
|
|
138
|
+
let storedValue = this.getItem(key, parseValueOnGet)
|
|
139
|
+
|
|
140
|
+
if (!TypeGuards.isNil(storedValue) && getItemValueOnMount) {
|
|
141
|
+
_initialValue = this.parseValue(storedValue)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_setValue(_initialValue)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
handler()
|
|
148
|
+
|
|
149
|
+
return disableListen ? null : this.listen(key, handler)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const setValue = (to: S | ((prev:S) => S)) => {
|
|
153
|
+
return _setValue((prev) => {
|
|
154
|
+
let newValue = prev
|
|
155
|
+
|
|
156
|
+
if (!TypeGuards.isFunction(to)) {
|
|
157
|
+
newValue = to
|
|
158
|
+
} else {
|
|
159
|
+
const fn = to as ((prev:S) => S)
|
|
160
|
+
newValue = fn(value)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (setItemValueOnMutate) {
|
|
164
|
+
this.setItem(key, newValue)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return newValue
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [value, setValue]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public listen(key: Key<T>, handler: LocalStorageHandler<Key<T>>) {
|
|
175
|
+
const trigger = (event: StorageEvent) => {
|
|
176
|
+
const storageKey = this.getStorageKey(key)
|
|
177
|
+
|
|
178
|
+
if (event?.key === storageKey) {
|
|
179
|
+
handler(key, event, this.parseValue(event?.newValue))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const newLength = this.storageListeners.push(trigger)
|
|
184
|
+
window.addEventListener('storage', trigger)
|
|
185
|
+
|
|
186
|
+
return () => {
|
|
187
|
+
this.storageListeners.splice(newLength - 1, 1)
|
|
188
|
+
window.removeEventListener('storage', trigger)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|