1
- import React , { ReactElement , useState } from 'react'
1
+ import React , { ReactElement , useEffect , useState } from 'react'
2
2
import cn from 'clsx'
3
3
4
4
import { ActionType , useSidebar } from '@/contexts/sidebar'
@@ -7,71 +7,92 @@ import { EmptyFolderIcon } from '@/nextra/icons/empty-folder'
7
7
import { EmptyFileIcon } from '@/nextra/icons/empty-file'
8
8
import { SeparatorIcon } from '@/nextra/icons/separator'
9
9
import { type CustomEventDetail , nextraCustomEventName } from '@/index'
10
+ import { useDebouncedCallback } from 'use-debounce'
10
11
11
12
type Props = {
12
13
type : ActionType
13
14
}
14
15
15
16
function ControlInput ( { type } : Props ) : ReactElement {
16
17
const sidebar = useSidebar ( )
17
- const [ title , setTitle ] = useState ( '' )
18
+ const [ title , setTitle ] = useState < string | null > ( null )
19
+ const [ error , setError ] = useState ( '' )
18
20
19
- const onChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
20
- e . preventDefault ( )
21
- setTitle ( e . target . value )
21
+ const nameMapper : Record < ActionType , string > = {
22
+ folder : '폴더' ,
23
+ page : '페이지' ,
24
+ separator : '구분선' ,
25
+ '' : '' ,
22
26
}
23
27
24
- const onClick = ( e : React . MouseEvent < HTMLDivElement > ) => {
25
- e . preventDefault ( )
26
- if ( ( e . target as any ) . tagName !== 'INPUT' ) {
27
- onComplete ( )
28
- }
28
+ const forbiddenCharsRegex = / [ < > # % " { } | \^ ~ \[ \] ` \/ : @ = & + $ , ; ! * ( ) \\ ' ] /
29
+ const typeName = nameMapper [ type ]
29
30
30
- if ( title ) {
31
- dispatchEvent ( )
32
- return
31
+ useEffect ( ( ) => {
32
+ if ( title === null ) return
33
+ if ( forbiddenCharsRegex . test ( title ) ) {
34
+ setError ( `${ typeName } 이름에 사용해서는 안 되는 기호가 포함되어 있습니다.` )
35
+ } else if ( title === '' ) {
36
+ setError ( `${ typeName } 이름을 입력해야 합니다.` )
37
+ } else {
38
+ setError ( '' )
33
39
}
34
- onCancel ( )
40
+ } , [ title ] )
41
+
42
+ const onChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
43
+ e . preventDefault ( )
44
+ // 두개 이상의 공백은 하나의 공백으로 변경
45
+ const value = e . target . value . replace ( / \s \s + / g, ' ' ) . trim ( )
46
+ setTitle ( value )
35
47
}
36
48
37
49
const onComplete = ( ) => {
38
50
if ( ! sidebar . actionActive ) return
39
- sidebar . setActionComplete ( true )
40
- }
41
-
42
- const onCancel = ( ) => {
43
51
setTitle ( '' )
52
+ dispatchEvent ( )
44
53
}
45
54
46
55
const onKeyDown = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
56
+ if ( e . key === ' ' ) {
57
+ e . preventDefault ( )
58
+ setTitle ( ( title ) => `${ title } ` )
59
+ return
60
+ }
47
61
if ( e . key !== 'Enter' ) return
48
62
e . preventDefault ( )
49
- onComplete ( )
50
63
dispatchEvent ( )
51
64
}
52
65
53
- const dispatchEvent = ( ) => {
54
- const { parentUrlSlug, bookUrlSlug, index, type } = sidebar . actionInfo
55
- if ( type === '' ) return
56
- const event = new CustomEvent < CustomEventDetail [ 'addActionEvent' ] > (
57
- nextraCustomEventName . addActionEvent ,
58
- {
59
- detail : { title, parentUrlSlug, index, bookUrlSlug, type } ,
60
- } ,
61
- )
62
- window . dispatchEvent ( event )
63
- }
66
+ const dispatchEvent = useDebouncedCallback (
67
+ ( ) => {
68
+ const { parentUrlSlug, bookUrlSlug, index, type } = sidebar . actionInfo
69
+ if ( type === '' ) return
70
+ if ( ! title ) return
71
+ if ( error ) return
72
+ const event = new CustomEvent < CustomEventDetail [ 'addActionEvent' ] > (
73
+ nextraCustomEventName . addActionEvent ,
74
+ {
75
+ detail : { title, parentUrlSlug, index, bookUrlSlug, type } ,
76
+ } ,
77
+ )
78
+ window . dispatchEvent ( event )
79
+ sidebar . reset ( )
80
+ } ,
81
+ 1000 ,
82
+ { leading : true } ,
83
+ )
64
84
65
85
const { ref } = useOutsideClick < HTMLDivElement > ( onComplete )
86
+
66
87
if ( type === '' ) return < > </ >
67
88
return (
68
89
< div
69
90
ref = { ref }
70
91
className = { cn (
92
+ 'nextra-control-input' ,
71
93
'nx-flex nx-w-full nx-items-center nx-px-2 nx-py-1.5 [word-break:break-word]' ,
72
- 'nx-transition-colors' ,
94
+ 'nx-relative nx- transition-colors' ,
73
95
) }
74
- onClick = { onClick }
75
96
>
76
97
< span >
77
98
{ type === 'folder' && < EmptyFolderIcon /> }
@@ -85,14 +106,32 @@ function ControlInput({ type }: Props): ReactElement {
85
106
'nx-bg-gray-100 nx-text-gray-600 ' ,
86
107
'dark:nx-bg-primary-100/5 dark:nx-text-gray-400' ,
87
108
) }
88
- value = { title }
109
+ value = { title === null ? '' : title }
89
110
onChange = { onChange }
90
- autoFocus = { true }
91
111
onKeyDown = { onKeyDown }
112
+ autoFocus = { true }
92
113
style = { {
93
114
boxShadow : 'none' ,
94
115
} }
95
116
/>
117
+ { error && (
118
+ < div
119
+ className = { cn (
120
+ 'nx-absolute nx-left-0 nx-top-[35px] nx-z-10 nx-w-full' ,
121
+ 'nx-bg-white nx-text-[12px] nx-font-medium nx-text-gray-500' ,
122
+ 'nx-px-2' ,
123
+ ) }
124
+ >
125
+ < div
126
+ className = { cn (
127
+ 'nx-border dark:nx-border-gray-100/20 dark:nx-bg-dark/50 ' ,
128
+ 'nx-px-2 nx-py-1' ,
129
+ ) }
130
+ >
131
+ { error }
132
+ </ div >
133
+ </ div >
134
+ ) }
96
135
</ div >
97
136
)
98
137
}
0 commit comments