dinocollab-core 1.0.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/README.md +54 -0
- package/dist/_virtual/_rollupPluginBabelHelpers.js +431 -0
- package/dist/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
- package/dist/assets/vector-404265a04f4f9c8be1f.webp +0 -0
- package/dist/node_modules/.pnpm/@rollup_plugin-typescript@1_d0d2002d9033600b6738d939bd598bc6/node_modules/tslib/tslib.es6.js +46 -0
- package/dist/node_modules/.pnpm/@rollup_plugin-typescript@1_d0d2002d9033600b6738d939bd598bc6/node_modules/tslib/tslib.es6.js.map +1 -0
- package/dist/src/api-context/alert-global.js +151 -0
- package/dist/src/api-context/alert-global.js.map +1 -0
- package/dist/src/api-context/drawer-global.js +105 -0
- package/dist/src/api-context/drawer-global.js.map +1 -0
- package/dist/src/api-context/global-modal.js +87 -0
- package/dist/src/api-context/global-modal.js.map +1 -0
- package/dist/src/api-context/popover-global.js +102 -0
- package/dist/src/api-context/popover-global.js.map +1 -0
- package/dist/src/api-context/popover.js +86 -0
- package/dist/src/api-context/popover.js.map +1 -0
- package/dist/src/api-context/ui.units.js +21 -0
- package/dist/src/api-context/ui.units.js.map +1 -0
- package/dist/src/components/copy-to-clipboard.js +105 -0
- package/dist/src/components/copy-to-clipboard.js.map +1 -0
- package/dist/src/components/custom.breadcrumbs.js +61 -0
- package/dist/src/components/custom.breadcrumbs.js.map +1 -0
- package/dist/src/components/help-tooltip.js +91 -0
- package/dist/src/components/help-tooltip.js.map +1 -0
- package/dist/src/components/image-with-fallback.js +48 -0
- package/dist/src/components/image-with-fallback.js.map +1 -0
- package/dist/src/components/text-editor.js +117 -0
- package/dist/src/components/text-editor.js.map +1 -0
- package/dist/src/form/create.autocomplete.chips.js +218 -0
- package/dist/src/form/create.autocomplete.chips.js.map +1 -0
- package/dist/src/form/create.date-expired.js +201 -0
- package/dist/src/form/create.date-expired.js.map +1 -0
- package/dist/src/form/create.date-picker.js +125 -0
- package/dist/src/form/create.date-picker.js.map +1 -0
- package/dist/src/form/create.form-base.js +135 -0
- package/dist/src/form/create.form-base.js.map +1 -0
- package/dist/src/form/create.form-comfirm.js +119 -0
- package/dist/src/form/create.form-comfirm.js.map +1 -0
- package/dist/src/form/create.form-grid-layout.js +177 -0
- package/dist/src/form/create.form-grid-layout.js.map +1 -0
- package/dist/src/form/create.form-grid-layout.units.js +39 -0
- package/dist/src/form/create.form-grid-layout.units.js.map +1 -0
- package/dist/src/form/create.input-base.js +260 -0
- package/dist/src/form/create.input-base.js.map +1 -0
- package/dist/src/form/create.input.file.js +74 -0
- package/dist/src/form/create.input.file.js.map +1 -0
- package/dist/src/form/create.select-simple.js +104 -0
- package/dist/src/form/create.select-simple.js.map +1 -0
- package/dist/src/form/create.select-with-api.js +271 -0
- package/dist/src/form/create.select-with-api.js.map +1 -0
- package/dist/src/form/create.text-editor.js +156 -0
- package/dist/src/form/create.text-editor.js.map +1 -0
- package/dist/src/form/dino-form.js +42 -0
- package/dist/src/form/dino-form.js.map +1 -0
- package/dist/src/form/helper.js +157 -0
- package/dist/src/form/helper.js.map +1 -0
- package/dist/src/form/modal-wrapper.js +75 -0
- package/dist/src/form/modal-wrapper.js.map +1 -0
- package/dist/src/form/validator.js +186 -0
- package/dist/src/form/validator.js.map +1 -0
- package/dist/src/hooks/index.js +48 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/index.js +26 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/redux/create.hoc-lazy.js +67 -0
- package/dist/src/redux/create.hoc-lazy.js.map +1 -0
- package/dist/src/redux/dino.js +11 -0
- package/dist/src/redux/dino.js.map +1 -0
- package/dist/src/redux/types.js +9 -0
- package/dist/src/redux/types.js.map +1 -0
- package/dist/src/redux/ui.error-page.js +80 -0
- package/dist/src/redux/ui.error-page.js.map +1 -0
- package/dist/src/redux/vector-404.webp.js +4 -0
- package/dist/src/redux/vector-404.webp.js.map +1 -0
- package/dist/src/table/context.js +12 -0
- package/dist/src/table/context.js.map +1 -0
- package/dist/src/table/create.action-row.js +135 -0
- package/dist/src/table/create.action-row.js.map +1 -0
- package/dist/src/table/create.status-cell.js +49 -0
- package/dist/src/table/create.status-cell.js.map +1 -0
- package/dist/src/table/create.table.js +233 -0
- package/dist/src/table/create.table.js.map +1 -0
- package/dist/src/table/custom.filter-operators.js +89 -0
- package/dist/src/table/custom.filter-operators.js.map +1 -0
- package/dist/src/table/dino.js +129 -0
- package/dist/src/table/dino.js.map +1 -0
- package/dist/src/table/helpers.js +116 -0
- package/dist/src/table/helpers.js.map +1 -0
- package/dist/src/table/model-filter.js +23 -0
- package/dist/src/table/model-filter.js.map +1 -0
- package/dist/src/table/toolbar-pannel.js +134 -0
- package/dist/src/table/toolbar-pannel.js.map +1 -0
- package/dist/src/table/ui.buttons.js +60 -0
- package/dist/src/table/ui.buttons.js.map +1 -0
- package/dist/src/table/ui.units.js +201 -0
- package/dist/src/table/ui.units.js.map +1 -0
- package/dist/src/utils/dayjs-config.js +12 -0
- package/dist/src/utils/dayjs-config.js.map +1 -0
- package/dist/src/utils/helpers.js +197 -0
- package/dist/src/utils/helpers.js.map +1 -0
- package/dist/src/utils/json-object.js +38 -0
- package/dist/src/utils/json-object.js.map +1 -0
- package/dist/src/utils/query-param.js +172 -0
- package/dist/src/utils/query-param.js.map +1 -0
- package/package.json +52 -0
- package/rollup.config.js +39 -0
- package/src/@types/global.d.ts +5 -0
- package/src/api-context/alert-global.tsx +174 -0
- package/src/api-context/drawer-global.tsx +116 -0
- package/src/api-context/global-modal.tsx +109 -0
- package/src/api-context/index.ts +13 -0
- package/src/api-context/popover-global.tsx +107 -0
- package/src/api-context/popover.tsx +89 -0
- package/src/api-context/ui.units.tsx +10 -0
- package/src/components/copy-to-clipboard.tsx +86 -0
- package/src/components/custom.breadcrumbs.tsx +67 -0
- package/src/components/help-tooltip.tsx +75 -0
- package/src/components/image-with-fallback.tsx +51 -0
- package/src/components/index.tsx +1 -0
- package/src/components/input-debounce-timer.tsx +138 -0
- package/src/components/loading-buttons.tsx +35 -0
- package/src/components/text-editor.preview.tsx +30 -0
- package/src/components/text-editor.tsx +125 -0
- package/src/form/README.md +55 -0
- package/src/form/create.autocomplete.chips.tsx +199 -0
- package/src/form/create.date-expired.tsx +195 -0
- package/src/form/create.date-picker.tsx +122 -0
- package/src/form/create.form-base.tsx +102 -0
- package/src/form/create.form-comfirm.tsx +83 -0
- package/src/form/create.form-grid-layout.tsx +170 -0
- package/src/form/create.form-grid-layout.units.tsx +37 -0
- package/src/form/create.input-base.tsx +222 -0
- package/src/form/create.input.file.tsx +76 -0
- package/src/form/create.select-simple.tsx +101 -0
- package/src/form/create.select-with-api.tsx +213 -0
- package/src/form/create.text-editor.tsx +161 -0
- package/src/form/dino-form.tsx +40 -0
- package/src/form/helper.ts +132 -0
- package/src/form/index.ts +12 -0
- package/src/form/modal-wrapper.tsx +75 -0
- package/src/form/types.ts +16 -0
- package/src/form/validator.ts +202 -0
- package/src/hooks/index.ts +44 -0
- package/src/index.ts +7 -0
- package/src/lab/create.autocomplete.simple.tsx +57 -0
- package/src/lab/create.dino-store.ts +59 -0
- package/src/lab/create.multi-select-dropdown.tsx +189 -0
- package/src/lab/create.select-mul-with-api/index.tsx +271 -0
- package/src/lab/create.select-mul-with-api/table-custom.tsx +194 -0
- package/src/lab/create.select-mul-with-api/types.ts +26 -0
- package/src/lab/create.select-mul-with-api/ui.units.tsx +163 -0
- package/src/lab/filter-bar/base.tsx +162 -0
- package/src/lab/filter-bar/create.filter-bar.tsx +190 -0
- package/src/lab/filter-bar/create.filter-menu.tsx +156 -0
- package/src/lab/filter-bar/create.filter-panel.tsx +95 -0
- package/src/lab/filter-bar/create.filtered.tsx +41 -0
- package/src/lab/filter-bar/create.sort-menu.tsx +43 -0
- package/src/lab/filter-bar/demo.tsx +50 -0
- package/src/lab/filter-bar/index.ts +6 -0
- package/src/lab/filter-bar/types.ts +105 -0
- package/src/lab/filter-bar/ui.units.tsx +70 -0
- package/src/lab/grafana-dashboard/configs.ts +43 -0
- package/src/lab/grafana-dashboard/date-time-range/absolute-time-rage.tsx +137 -0
- package/src/lab/grafana-dashboard/date-time-range/helpers.ts +126 -0
- package/src/lab/grafana-dashboard/date-time-range/index.tsx +62 -0
- package/src/lab/grafana-dashboard/date-time-range/menu-wrap.tsx +101 -0
- package/src/lab/grafana-dashboard/date-time-range/quick-ranges.tsx +161 -0
- package/src/lab/grafana-dashboard/date-time-range/types.ts +9 -0
- package/src/lab/grafana-dashboard/date-time-range/units.tsx +18 -0
- package/src/lab/grafana-dashboard/helper.ts +25 -0
- package/src/lab/grafana-dashboard/hooks.tsx +79 -0
- package/src/lab/grafana-dashboard/icons.tsx +67 -0
- package/src/lab/grafana-dashboard/index.tsx +120 -0
- package/src/lab/grafana-dashboard/top-bar.tsx +62 -0
- package/src/lab/grafana-dashboard/top-bar.types.ts +5 -0
- package/src/lab/grafana-dashboard/types.ts +8 -0
- package/src/lab/media-player.core1.tsx +273 -0
- package/src/lab/media-player.muted.tsx +62 -0
- package/src/lab/media-player.units.ts +80 -0
- package/src/lab/table-grid/create.table-grid.tsx +183 -0
- package/src/lab/table-grid/demo.tsx +53 -0
- package/src/lab/table-grid/dino.tsx +8 -0
- package/src/lab/table-grid/helpers.tsx +11 -0
- package/src/lab/table-grid/index.ts +3 -0
- package/src/lab/table-grid/item-actions.tsx +138 -0
- package/src/lab/table-grid/toolbar-pannel.tsx +98 -0
- package/src/lab/table-grid/types.ts +68 -0
- package/src/redux/create.hoc-lazy.tsx +80 -0
- package/src/redux/dino.ts +9 -0
- package/src/redux/index.ts +6 -0
- package/src/redux/types.ts +27 -0
- package/src/redux/ui.error-page.tsx +62 -0
- package/src/redux/ui.units.tsx +41 -0
- package/src/redux/vector-404.webp +0 -0
- package/src/table/context.tsx +16 -0
- package/src/table/create.action-row.tsx +91 -0
- package/src/table/create.status-cell.tsx +51 -0
- package/src/table/create.table.tsx +239 -0
- package/src/table/custom.filter-operators.ts +94 -0
- package/src/table/dino.tsx +120 -0
- package/src/table/helpers.ts +94 -0
- package/src/table/index.ts +13 -0
- package/src/table/model-filter.ts +43 -0
- package/src/table/toolbar-pannel.tsx +106 -0
- package/src/table/types.ts +50 -0
- package/src/table/ui.buttons.tsx +54 -0
- package/src/table/ui.units.tsx +189 -0
- package/src/utils/dayjs-config.ts +13 -0
- package/src/utils/helpers.ts +171 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/json-object.ts +29 -0
- package/src/utils/mfe-events.tsx +34 -0
- package/src/utils/query-param.ts +129 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React, { Component, createRef } from 'react'
|
|
2
|
+
import { Box, BoxProps, IconButton, styled, Tooltip, Typography } from '@mui/material'
|
|
3
|
+
import FullscreenIcon from '@mui/icons-material/Fullscreen'
|
|
4
|
+
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit'
|
|
5
|
+
import { IFormInputBase } from './types'
|
|
6
|
+
import { getErrorMessage } from './helper'
|
|
7
|
+
import TextEditor, { textEditorClasses } from '../components/text-editor'
|
|
8
|
+
|
|
9
|
+
interface IParams {
|
|
10
|
+
id: string
|
|
11
|
+
title: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function CreateTextEditor<T>(params: IParams) {
|
|
15
|
+
interface IProps extends IFormInputBase<T> {}
|
|
16
|
+
|
|
17
|
+
interface IState {
|
|
18
|
+
isFullscreen: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class Editor extends Component<IProps, IState> {
|
|
22
|
+
contentRef
|
|
23
|
+
constructor(props: IProps) {
|
|
24
|
+
super(props)
|
|
25
|
+
this.state = {
|
|
26
|
+
isFullscreen: false
|
|
27
|
+
}
|
|
28
|
+
this.contentRef = createRef<HTMLDivElement | null>()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
componentDidMount() {
|
|
32
|
+
document.addEventListener('fullscreenchange', this.handleFullscreenChange)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
componentWillUnmount() {
|
|
36
|
+
document.removeEventListener('fullscreenchange', this.handleFullscreenChange)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handleFullscreenChange = () => {
|
|
40
|
+
this.setState({ isFullscreen: !!document.fullscreenElement })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
handleFullscreenToggle = () => {
|
|
44
|
+
if (this.contentRef.current) {
|
|
45
|
+
if (!document.fullscreenElement) {
|
|
46
|
+
this.contentRef.current.requestFullscreen().catch((err) => {
|
|
47
|
+
console.error(`Error attempting to enable full-screen mode: ${err.message}`)
|
|
48
|
+
})
|
|
49
|
+
} else {
|
|
50
|
+
document.exitFullscreen()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getWrapClasses = () => {
|
|
56
|
+
const classes = []
|
|
57
|
+
if (this.state.isFullscreen) classes.push(textEditorClasses.fullscreen)
|
|
58
|
+
return classes.join(' ')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render() {
|
|
62
|
+
const errorMessage = getErrorMessage(this.props.messageErrors, this.props.name)
|
|
63
|
+
const dValue = (this.props.name ? this.props.data?.[this.props.name] : '')?.toString() ?? ''
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Box id={params.id} ref={this.contentRef} sx={{ backgroundColor: '#fff' }}>
|
|
67
|
+
{this.state.isFullscreen && (
|
|
68
|
+
<WrapTitle>
|
|
69
|
+
<Typography variant='h4' sx={{ fontWeight: 600, flex: 1, textAlign: 'center' }}>
|
|
70
|
+
{params.title}
|
|
71
|
+
</Typography>
|
|
72
|
+
</WrapTitle>
|
|
73
|
+
)}
|
|
74
|
+
<Wrap className={this.getWrapClasses()}>
|
|
75
|
+
<WrapActions>
|
|
76
|
+
<Tooltip title={this.state.isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'} arrow>
|
|
77
|
+
<CustomButton sx={{ color: '#06c' }} onClick={this.handleFullscreenToggle}>
|
|
78
|
+
{this.state.isFullscreen ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
|
79
|
+
</CustomButton>
|
|
80
|
+
</Tooltip>
|
|
81
|
+
</WrapActions>
|
|
82
|
+
<WrapTextEditor>
|
|
83
|
+
<TextEditor
|
|
84
|
+
defautValue={dValue}
|
|
85
|
+
name={this.props.name?.toString() ?? ''}
|
|
86
|
+
readOnly={this.props.disabled}
|
|
87
|
+
error={errorMessage.error}
|
|
88
|
+
onBlur={() => {
|
|
89
|
+
if (!this.props.name) return
|
|
90
|
+
this.props.onBlur?.(this.props.name)
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</WrapTextEditor>
|
|
94
|
+
</Wrap>
|
|
95
|
+
</Box>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Editor
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default CreateTextEditor
|
|
103
|
+
|
|
104
|
+
const editorHeight = 64
|
|
105
|
+
|
|
106
|
+
const Wrap = styled(Box)(({ theme }) => ({
|
|
107
|
+
maxWidth: theme.breakpoints.values.xl,
|
|
108
|
+
margin: '0 auto',
|
|
109
|
+
position: 'relative',
|
|
110
|
+
[`&.${textEditorClasses.fullscreen} .${textEditorClasses.root}`]: {
|
|
111
|
+
height: `calc(100vh - ${editorHeight + 10}px)`
|
|
112
|
+
},
|
|
113
|
+
[`&.${textEditorClasses.fullscreen}`]: {
|
|
114
|
+
width: '871px'
|
|
115
|
+
}
|
|
116
|
+
}))
|
|
117
|
+
|
|
118
|
+
const WrapTitle = styled(Box)({
|
|
119
|
+
height: `${editorHeight}px`,
|
|
120
|
+
display: 'flex',
|
|
121
|
+
justifyContent: 'center',
|
|
122
|
+
alignItems: 'center'
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const WrapActions = styled(({ children, ...props }: BoxProps) => (
|
|
126
|
+
<Box {...props}>
|
|
127
|
+
<Box>{children}</Box>
|
|
128
|
+
</Box>
|
|
129
|
+
))({
|
|
130
|
+
position: 'sticky',
|
|
131
|
+
top: '-1px',
|
|
132
|
+
zIndex: 2,
|
|
133
|
+
'& > div': {
|
|
134
|
+
height: 0,
|
|
135
|
+
display: 'flex',
|
|
136
|
+
justifyContent: 'right',
|
|
137
|
+
padding: '0 6px'
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const CustomButton = styled(IconButton)({
|
|
142
|
+
flex: '0 0 auto',
|
|
143
|
+
marginTop: '1px',
|
|
144
|
+
width: '40px',
|
|
145
|
+
height: '40px'
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const WrapTextEditor = styled(Box)({
|
|
149
|
+
backgroundColor: '#fff',
|
|
150
|
+
width: '100%',
|
|
151
|
+
'& .ql-container': {
|
|
152
|
+
fontFamily: '"Roboto", serif',
|
|
153
|
+
fontSize: '14px'
|
|
154
|
+
},
|
|
155
|
+
'& .ql-toolbar': {
|
|
156
|
+
position: 'sticky',
|
|
157
|
+
top: '-1px',
|
|
158
|
+
backgroundColor: '#fff',
|
|
159
|
+
zIndex: 1
|
|
160
|
+
}
|
|
161
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import FormValidator from './validator'
|
|
2
|
+
import FormModalWrapper from './modal-wrapper'
|
|
3
|
+
import CreateFormBase from './create.form-base'
|
|
4
|
+
import CreateInputFile from './create.input.file'
|
|
5
|
+
import CreateInputBase from './create.input-base'
|
|
6
|
+
import CreateDatePicker from './create.date-picker'
|
|
7
|
+
import CreateTextEditor from './create.text-editor'
|
|
8
|
+
import CreateDateExpired from './create.date-expired'
|
|
9
|
+
import CreateFormComfirm from './create.form-comfirm'
|
|
10
|
+
import CreateSelectSimple from './create.select-simple'
|
|
11
|
+
import CreateSelectWithApi from './create.select-with-api'
|
|
12
|
+
import CreateFormGridLayout from './create.form-grid-layout'
|
|
13
|
+
import CreateAutocompleteChip from './create.autocomplete.chips'
|
|
14
|
+
import { FormBottomBarWrap } from './create.form-grid-layout.units'
|
|
15
|
+
|
|
16
|
+
class DinoFormBase {
|
|
17
|
+
//#region Base
|
|
18
|
+
ModalWrap = FormModalWrapper
|
|
19
|
+
BottomBarWrap = FormBottomBarWrap
|
|
20
|
+
FormValidator = FormValidator
|
|
21
|
+
validator = FormValidator.initial
|
|
22
|
+
createFormBase = CreateFormBase
|
|
23
|
+
createFormComfirm = CreateFormComfirm
|
|
24
|
+
createFormGridLayout = CreateFormGridLayout
|
|
25
|
+
//#endregion
|
|
26
|
+
|
|
27
|
+
//#region Inputs
|
|
28
|
+
createDateExpired= CreateDateExpired
|
|
29
|
+
createDatePicker = CreateDatePicker
|
|
30
|
+
createAutocompleteChip = CreateAutocompleteChip
|
|
31
|
+
createInput = CreateInputBase
|
|
32
|
+
createInputFile = CreateInputFile
|
|
33
|
+
createSelectSimple = CreateSelectSimple
|
|
34
|
+
createSelectWithApi = CreateSelectWithApi
|
|
35
|
+
createTextEditor = CreateTextEditor
|
|
36
|
+
//#endregion
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DinoForm = new DinoFormBase()
|
|
40
|
+
export default DinoForm
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { AxiosError } from 'axios'
|
|
2
|
+
import FormValidator, { IModelError, IRuleOption, PartialError } from './validator'
|
|
3
|
+
|
|
4
|
+
type AnyObject = { [key: string]: any }
|
|
5
|
+
const ObjectAssign = <T extends AnyObject>(model: T, ...sources: (Partial<T> | undefined)[]) => {
|
|
6
|
+
if (sources.length < 1) {
|
|
7
|
+
return model
|
|
8
|
+
}
|
|
9
|
+
const temps = sources.filter((x) => x) as T[]
|
|
10
|
+
return temps.reduce((a, b) => {
|
|
11
|
+
a = Object.assign(a, b)
|
|
12
|
+
return a
|
|
13
|
+
}, model)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DeepMerge = <T extends AnyObject>(model: T, ...sources: (Partial<T> | undefined)[]) => {
|
|
17
|
+
sources = sources.filter((x) => !!x)
|
|
18
|
+
if (sources.length < 1) {
|
|
19
|
+
return model
|
|
20
|
+
}
|
|
21
|
+
const temps = [model, ...sources].filter((x) => x)
|
|
22
|
+
const fieldObjects = temps.reduce<string[]>((a, b) => {
|
|
23
|
+
if (!b) return a
|
|
24
|
+
const fields = Object.keys(b).filter((x) => typeof b[x] === 'object' && !Array.isArray(b[x]))
|
|
25
|
+
a.push(...fields)
|
|
26
|
+
return a
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
const target = ObjectAssign<T>(model, ...temps.slice(1))
|
|
30
|
+
const temp = target as any
|
|
31
|
+
const temp2 = temps.slice(1).filter((x) => !!x) as Partial<T>[]
|
|
32
|
+
for (let index = 0; index < fieldObjects.length; index++) {
|
|
33
|
+
const element = fieldObjects[index]
|
|
34
|
+
temp[element] = DeepMerge({}, temp[element], ...temp2.map((x) => x[element]))
|
|
35
|
+
}
|
|
36
|
+
return target
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const getErrorMessage = function <TModel>(MessageErrors: PartialError<TModel> | undefined, key?: keyof TModel): IModelError {
|
|
40
|
+
return MessageErrors && (MessageErrors as any)[key] ? { ...(MessageErrors as any)[key][0], error: true } : { error: false, message: '' }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ModelBase = {
|
|
44
|
+
[key: string]: any
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const convertFormDataToJson = function <TModel>(form: FormData): Partial<TModel> {
|
|
48
|
+
const data = Array.from(form).reduce<Partial<TModel>>((a: any, b: any[]) => {
|
|
49
|
+
if (!a[b[0]]) {
|
|
50
|
+
a[b[0]] = b[1]
|
|
51
|
+
} else if (Array.isArray(a[b[0]])) {
|
|
52
|
+
a[b[0]].push(b[1])
|
|
53
|
+
} else {
|
|
54
|
+
a[b[0]] = [a[b[0]], b[1]]
|
|
55
|
+
}
|
|
56
|
+
return a
|
|
57
|
+
}, {} as any) as any
|
|
58
|
+
|
|
59
|
+
Object.keys(data).forEach((key) => {
|
|
60
|
+
const value = data[key]
|
|
61
|
+
if (typeof value === 'string' && (value.toString().toLocaleLowerCase() === 'true' || value.toString().toLocaleLowerCase() === 'false')) {
|
|
62
|
+
data[key] = value.toString().toLocaleLowerCase() === 'true'
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
return data
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const SingleValidate = function <TModel, TPartial = TModel>(
|
|
69
|
+
key: keyof TModel,
|
|
70
|
+
modelState: TPartial,
|
|
71
|
+
MessageErrors: PartialError<TModel>,
|
|
72
|
+
Validator: FormValidator<TPartial>
|
|
73
|
+
) {
|
|
74
|
+
const messageErrors = Validator.run(modelState) as { [key: string]: any }
|
|
75
|
+
if (messageErrors) {
|
|
76
|
+
let errors = (MessageErrors || {}) as { [key: string]: any }
|
|
77
|
+
const keys = Object.keys(modelState as any)
|
|
78
|
+
.filter((key) => !!(modelState as any)[key])
|
|
79
|
+
.filter((key) => ((modelState as any)[key] instanceof File ? !!((modelState as any)[key] as File).size : false))
|
|
80
|
+
keys.push(key as string)
|
|
81
|
+
keys.forEach((key) => {
|
|
82
|
+
if (messageErrors[key]) {
|
|
83
|
+
errors[key] = messageErrors[key]
|
|
84
|
+
} else {
|
|
85
|
+
delete errors[key]
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
return errors
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const GetErrorFromResponse = function <TModel>(error: AxiosError, ModelRender: TModel) {
|
|
94
|
+
const data = error.response?.data as any
|
|
95
|
+
if (data) {
|
|
96
|
+
const keys = Object.keys(ModelRender as any)
|
|
97
|
+
const MessageErrors: PartialError<TModel> | undefined = {}
|
|
98
|
+
keys.forEach((key) => {
|
|
99
|
+
const messages = data[key]
|
|
100
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
101
|
+
;(MessageErrors as any)[key] = [{ message: messages[0] }]
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
return MessageErrors
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const ClearFieldEmpty = <TModel>(model: Extract<TModel, ModelBase>) => {
|
|
109
|
+
Object.keys(model).forEach((key: keyof Extract<TModel, ModelBase>) => {
|
|
110
|
+
if (!model[key]) delete model[key]
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const ValidateMerge = <TModel>(...validateor: (FormValidator<TModel> | undefined)[]): FormValidator<TModel> => {
|
|
115
|
+
const configs = validateor.map((x) => x?.configs).filter((x) => !!x)
|
|
116
|
+
|
|
117
|
+
let temp: any = Object.assign({}, ...configs)
|
|
118
|
+
|
|
119
|
+
for (let index = 0; index < Object.keys(temp).length; index++) {
|
|
120
|
+
const key = Object.keys(temp)[index]
|
|
121
|
+
//merge configs
|
|
122
|
+
temp[key] = DeepMerge({}, ...configs.map((x) => (x ? x[key] : { Rules: [] })))
|
|
123
|
+
//merge rules
|
|
124
|
+
temp[key].Rules = configs
|
|
125
|
+
.map((x) => (x ? x[key] : { Rules: [] }))
|
|
126
|
+
.reduce<IRuleOption<TModel>[]>((a, b) => {
|
|
127
|
+
a.push(...(b?.Rules ?? []))
|
|
128
|
+
return a
|
|
129
|
+
}, [])
|
|
130
|
+
}
|
|
131
|
+
return new FormValidator(temp)
|
|
132
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './types'
|
|
2
|
+
|
|
3
|
+
export { default as FormValidator } from './validator'
|
|
4
|
+
export * from './validator'
|
|
5
|
+
|
|
6
|
+
export { default as DinoForm } from './dino-form'
|
|
7
|
+
|
|
8
|
+
export { getErrorMessage, convertFormDataToJson } from './helper'
|
|
9
|
+
|
|
10
|
+
export type { ISelectWithApiOption } from './create.select-with-api'
|
|
11
|
+
|
|
12
|
+
export type { InputBaseImage } from './create.input-base'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { FC, PropsWithChildren } from 'react'
|
|
2
|
+
import { Box, Breakpoint, IconButton, Paper, styled, SxProps, Theme, Typography } from '@mui/material'
|
|
3
|
+
import CloseIcon from '@mui/icons-material/Close'
|
|
4
|
+
import { MapGlobalModalContext } from '../api-context'
|
|
5
|
+
|
|
6
|
+
interface IProps extends PropsWithChildren {
|
|
7
|
+
title: string
|
|
8
|
+
size?: Breakpoint
|
|
9
|
+
fullHeight?: boolean
|
|
10
|
+
onCloseClick?: () => void
|
|
11
|
+
slots?: {
|
|
12
|
+
sxTopbarProps?: SxProps<Theme>
|
|
13
|
+
beforeTitle?: JSX.Element
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const FormModalWrapper: FC<IProps> = (props) => {
|
|
17
|
+
return MapGlobalModalContext((context) => (
|
|
18
|
+
<Wrap size={props.size} className={props.fullHeight ? 'full-height' : undefined}>
|
|
19
|
+
<TopBar sx={props.slots?.sxTopbarProps}>
|
|
20
|
+
{props.slots?.beforeTitle && (
|
|
21
|
+
<>
|
|
22
|
+
{props.slots.beforeTitle}
|
|
23
|
+
<Box flex={1} />
|
|
24
|
+
</>
|
|
25
|
+
)}
|
|
26
|
+
<Typography component='h3' variant='subtitle1' sx={{ mt: '2px', ml: '8px', fontWeight: 700 }}>
|
|
27
|
+
{props.title}
|
|
28
|
+
</Typography>
|
|
29
|
+
<Box flex={1} />
|
|
30
|
+
<BtnClose
|
|
31
|
+
onClick={() => {
|
|
32
|
+
props.onCloseClick && props.onCloseClick()
|
|
33
|
+
context.close()
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<CloseIcon />
|
|
37
|
+
</BtnClose>
|
|
38
|
+
</TopBar>
|
|
39
|
+
{props.children}
|
|
40
|
+
</Wrap>
|
|
41
|
+
))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default FormModalWrapper
|
|
45
|
+
|
|
46
|
+
const Wrap = styled(Paper)<{ size?: Breakpoint }>(({ theme, size }) => ({
|
|
47
|
+
maxHeight: 'calc(100vh - 24px)',
|
|
48
|
+
overflow: 'hidden',
|
|
49
|
+
display: 'flex',
|
|
50
|
+
flexDirection: 'column',
|
|
51
|
+
position: 'relative',
|
|
52
|
+
maxWidth: 'calc(100vw - 24px)',
|
|
53
|
+
width: theme.breakpoints.values[size ?? 'lg'],
|
|
54
|
+
[theme.breakpoints.down('sm')]: { width: 'calc(100vw - 24px)' },
|
|
55
|
+
'&.full-height': {
|
|
56
|
+
height: 'calc(100vh - 24px)'
|
|
57
|
+
}
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
const BtnClose = styled(IconButton)({
|
|
61
|
+
flex: '0 0 auto',
|
|
62
|
+
color: '#3c3c3c',
|
|
63
|
+
'& svg': { transition: 'all 0.2s' },
|
|
64
|
+
'&:hover svg': { color: '#ff200c' }
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const TopBar = styled(Box)({
|
|
68
|
+
display: 'flex',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
padding: '0 8px 0 8px',
|
|
71
|
+
gap: '6px',
|
|
72
|
+
flex: '0 0 auto',
|
|
73
|
+
boxShadow: 'rgba(145, 158, 171, 0.1) 0px 0px 2px 0px,rgba(145, 158, 171, 0.12) 0px 0px 24px -4px',
|
|
74
|
+
height: '48px'
|
|
75
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PartialError } from './validator'
|
|
2
|
+
|
|
3
|
+
export interface IFormBase<TModel> {
|
|
4
|
+
data?: Partial<TModel>
|
|
5
|
+
messageErrors: PartialError<TModel>
|
|
6
|
+
onBlur: (keyName: keyof TModel) => void
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IFormInputBase<TModel, TSlots = any> extends Partial<IFormBase<TModel>> {
|
|
11
|
+
name?: keyof TModel
|
|
12
|
+
label?: React.ReactNode
|
|
13
|
+
placeholder?: string
|
|
14
|
+
defaultValue?: any
|
|
15
|
+
slots?: TSlots
|
|
16
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { formatFileSize } from '../utils'
|
|
2
|
+
|
|
3
|
+
declare type ModelBase = { [key: string]: any }
|
|
4
|
+
|
|
5
|
+
export enum SingleRuleValidate {
|
|
6
|
+
Required = 'Required',
|
|
7
|
+
Regex = 'Regex',
|
|
8
|
+
RequiredValue = 'RequiredValue',
|
|
9
|
+
Array = 'Array',
|
|
10
|
+
Number = 'Number',
|
|
11
|
+
Email = 'Email',
|
|
12
|
+
PhoneNumber = 'PhoneNumber',
|
|
13
|
+
Url = 'Url',
|
|
14
|
+
File = 'File',
|
|
15
|
+
FileRequired = 'FileRequired',
|
|
16
|
+
Custom = 'Custom'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MessageDefault = {
|
|
20
|
+
Required: 'The {0} is required',
|
|
21
|
+
Regex: 'The {0} is not match {1}',
|
|
22
|
+
RequiredValue: 'the {0} is not equal {1}',
|
|
23
|
+
Array: 'The {0} is not valid',
|
|
24
|
+
Number: '{0} must be a number',
|
|
25
|
+
Email: 'The {0} is not valid',
|
|
26
|
+
PhoneNumber: 'The {0} is not valid',
|
|
27
|
+
Url: 'The {0} is not valid',
|
|
28
|
+
File: 'File size must be less than {1}',
|
|
29
|
+
FileRequired: 'The {0} is required',
|
|
30
|
+
Custom: 'Error Custom'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RuleOptions {
|
|
34
|
+
File: {
|
|
35
|
+
maxSize: number
|
|
36
|
+
// toal size of files
|
|
37
|
+
all?: boolean
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const MapRuleOptions = <TK extends keyof RuleOptions>(key: TK, options?: RuleOptions[TK]) => options
|
|
42
|
+
|
|
43
|
+
export interface IRuleOption<TModel = any> {
|
|
44
|
+
rule: SingleRuleValidate
|
|
45
|
+
message?: string
|
|
46
|
+
Value?: RegExp | ((value: any, model: TModel) => boolean) | number | Array<any> | boolean
|
|
47
|
+
options?: any
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface IConfigValue<TModel = any> {
|
|
51
|
+
Rules: IRuleOption<TModel>[]
|
|
52
|
+
label?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface IModelError {
|
|
56
|
+
message: string
|
|
57
|
+
error: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type IFormValidatorConfig<TModel> = {
|
|
61
|
+
[key in keyof Extract<TModel, ModelBase>]?: IConfigValue<TModel>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type PartialError<TModel> = {
|
|
65
|
+
[key in keyof TModel]?: IRuleOption<TModel>[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default class FormValidator<TModel> {
|
|
69
|
+
configs: IFormValidatorConfig<TModel>
|
|
70
|
+
constructor(configs: IFormValidatorConfig<TModel>) {
|
|
71
|
+
this.configs = configs
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static initial = <T>(configs: IFormValidatorConfig<T>) => new FormValidator(configs)
|
|
75
|
+
|
|
76
|
+
AddRules = (key: keyof TModel, ...rule: IRuleOption<TModel>[]) => {
|
|
77
|
+
let fieldConfig = this.configs[key]
|
|
78
|
+
if (!fieldConfig) {
|
|
79
|
+
fieldConfig = { Rules: [] }
|
|
80
|
+
this.configs[key] = fieldConfig
|
|
81
|
+
}
|
|
82
|
+
fieldConfig.Rules.push(...rule)
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Required = (RuleOption: IRuleOption, value: any) => {
|
|
87
|
+
return value === null || value === undefined || value === ''
|
|
88
|
+
}
|
|
89
|
+
Regex = (RuleOption: IRuleOption, value: string, Regex: RegExp) => {
|
|
90
|
+
if (!value) return false
|
|
91
|
+
return !(RuleOption.Value as RegExp).test(value)
|
|
92
|
+
}
|
|
93
|
+
RequiredValue = (RuleOption: IRuleOption, value: any) => {
|
|
94
|
+
if (!value) return false
|
|
95
|
+
return value !== RuleOption.Value
|
|
96
|
+
}
|
|
97
|
+
Custom = (RuleOption: IRuleOption, value: any, model: any) => {
|
|
98
|
+
if (typeof RuleOption.Value === 'function') {
|
|
99
|
+
return RuleOption.Value(value, model)
|
|
100
|
+
}
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
Array = (RuleOption: IRuleOption, value: any) => {
|
|
104
|
+
if (!value) return false
|
|
105
|
+
if (typeof RuleOption.Value === 'number') {
|
|
106
|
+
return Array.isArray(value) ? value.length <= RuleOption.Value : true
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
Number = (RuleOption: IRuleOption, value: any) => {
|
|
111
|
+
if (!value) return false
|
|
112
|
+
return isNaN(Number(value))
|
|
113
|
+
}
|
|
114
|
+
Email = (RuleOption: IRuleOption, value: any) => {
|
|
115
|
+
if (!value) return false
|
|
116
|
+
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
|
117
|
+
return !regex.test(value)
|
|
118
|
+
}
|
|
119
|
+
PhoneNumber = (RuleOption: IRuleOption, value: any) => {
|
|
120
|
+
if (!value) return false
|
|
121
|
+
const phoneRegex = /^(\+?\d{1,4}[\s-]?)?((\(\d{1,4}\))|\d{1,4})[\s-]?\d{1,4}[\s-]?\d{1,9}$/
|
|
122
|
+
return !phoneRegex.test((value ?? '').trim())
|
|
123
|
+
}
|
|
124
|
+
Url = (RuleOption: IRuleOption, value: any) => {
|
|
125
|
+
if (!value) return false
|
|
126
|
+
try {
|
|
127
|
+
new URL(value)
|
|
128
|
+
return false
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
File = (RuleOption: IRuleOption, value: any) => {
|
|
134
|
+
if (!value) return false
|
|
135
|
+
const options = MapRuleOptions('File', RuleOption.options)
|
|
136
|
+
const files: File[] = Array.isArray(value) ? value : [value]
|
|
137
|
+
if (!options) return false
|
|
138
|
+
if (options.all === true) {
|
|
139
|
+
const size = files.reduce((a, b) => {
|
|
140
|
+
a += b.size
|
|
141
|
+
return a
|
|
142
|
+
}, 0)
|
|
143
|
+
return size > options.maxSize
|
|
144
|
+
} else {
|
|
145
|
+
return files.some((x) => x.size > options.maxSize)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
FileRequired = (RuleOption: IRuleOption, value: any) => {
|
|
149
|
+
const files: File[] = Array.isArray(value) ? value : [value]
|
|
150
|
+
return !!files.filter((x) => !!x).some((x) => x.size <= 0)
|
|
151
|
+
}
|
|
152
|
+
RenderMessage = (RuleOption: IRuleOption, key: string, label?: string) => {
|
|
153
|
+
if (!RuleOption.message) {
|
|
154
|
+
RuleOption.message = MessageDefault[RuleOption.rule].replace('{0}', label ?? key)
|
|
155
|
+
switch (RuleOption.rule) {
|
|
156
|
+
case SingleRuleValidate.Regex: {
|
|
157
|
+
RuleOption.message = RuleOption.message.replace('{1}', (RuleOption.Value as RegExp).source)
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
case SingleRuleValidate.RequiredValue: {
|
|
161
|
+
RuleOption.message = RuleOption.message.replace('{1}', RuleOption.Value?.toString() ?? '')
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
case SingleRuleValidate.File: {
|
|
165
|
+
const options = MapRuleOptions('File', RuleOption.options)
|
|
166
|
+
if (options?.all === true) {
|
|
167
|
+
RuleOption.message = `Total file size must be less than ${formatFileSize((options?.maxSize ?? 0) / 1024)}`
|
|
168
|
+
} else {
|
|
169
|
+
RuleOption.message = RuleOption.message.replace('{1}', formatFileSize((options?.maxSize ?? 0) / 1024))
|
|
170
|
+
}
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
RuleOption.message = RuleOption.message.replace('{0}', key)
|
|
178
|
+
return RuleOption
|
|
179
|
+
}
|
|
180
|
+
run = (model: TModel) => {
|
|
181
|
+
const keys = Object.keys(this.configs)
|
|
182
|
+
const data: PartialError<TModel> = {}
|
|
183
|
+
keys.forEach((key) => {
|
|
184
|
+
const config = this.configs[key]
|
|
185
|
+
if (!config) return
|
|
186
|
+
const tmp = this.Executed(config, (model as any)[key], model)
|
|
187
|
+
if (tmp.length < 1) return
|
|
188
|
+
;(data as any)[key] = tmp.map((x) => this.RenderMessage(x, key, config.label))
|
|
189
|
+
})
|
|
190
|
+
return data
|
|
191
|
+
}
|
|
192
|
+
Executed = (Config: IConfigValue, valueField: any, model: any) => {
|
|
193
|
+
return Config.Rules.filter((Value) => {
|
|
194
|
+
const action = (this as any)[Value.rule.toString()]
|
|
195
|
+
return action && action(Value, valueField, model)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const CreateFormValidator = <TModel>() => {
|
|
201
|
+
return new FormValidator<Partial<TModel>>({})
|
|
202
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/exhaustive-deps*/
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
export const useCheckScrolled = () => {
|
|
5
|
+
const [isScrolled, setIsScrolled] = useState(false)
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const handleScroll = () => {
|
|
9
|
+
const scrolled = window.scrollY > 0
|
|
10
|
+
if (scrolled !== isScrolled) setIsScrolled(scrolled)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
window.addEventListener('scroll', handleScroll)
|
|
14
|
+
return () => window.removeEventListener('scroll', handleScroll)
|
|
15
|
+
}, [isScrolled])
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const scrolled = window.scrollY > 0
|
|
19
|
+
if (scrolled !== isScrolled) setIsScrolled(scrolled)
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
return { isScrolled }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useDebounce = <T>(value: T, delay: number): T => {
|
|
26
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (delay <= 0) {
|
|
30
|
+
setDebouncedValue(value)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handler = setTimeout(() => {
|
|
35
|
+
setDebouncedValue(value)
|
|
36
|
+
}, delay)
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
clearTimeout(handler)
|
|
40
|
+
}
|
|
41
|
+
}, [value, delay])
|
|
42
|
+
|
|
43
|
+
return debouncedValue
|
|
44
|
+
}
|