@@ -7,7 +7,7 @@ import { isMaskDelimiter, makeMaskProps, useMask } from '@/composables/mask'
7
7
import { useProxiedModel } from '@/composables/proxiedModel'
8
8
9
9
// Utilities
10
- import { computed , onBeforeMount , ref , shallowRef , toRef } from 'vue'
10
+ import { computed , nextTick , onBeforeMount , ref , shallowRef , toRef } from 'vue'
11
11
import { genericComponent , propsFactory , useRender } from '@/util'
12
12
13
13
// Types
@@ -33,8 +33,8 @@ export const VMaskInput = genericComponent<VMaskInputSlots>()({
33
33
setup ( props , { slots, emit } ) {
34
34
const vTextFieldRef = ref < VTextField > ( )
35
35
36
- const selection = shallowRef ( 0 )
37
- const lazySelection = shallowRef ( 0 )
36
+ const inputAction = shallowRef ( )
37
+ const caretPosition = shallowRef ( 0 )
38
38
39
39
const mask = useMask ( props )
40
40
const returnMaskedValue = computed ( ( ) => props . mask && props . returnMaskedValue )
@@ -47,62 +47,166 @@ export const VMaskInput = genericComponent<VMaskInputSlots>()({
47
47
val => props . mask ? mask . mask ( mask . unmask ( val ) ) : val ,
48
48
val => {
49
49
if ( props . mask ) {
50
- const valueBeforeChange = mask . unmask ( model . value )
50
+ const valueWithoutDelimiters = removeMaskDelimiters ( val )
51
+
51
52
// E.g. mask is #-# and the input value is '2-23'
52
53
// model-value should be enforced to '2-2'
53
- const enforcedMaskedValue = mask . mask ( mask . unmask ( val ) )
54
- const newUnmaskedValue = mask . unmask ( enforcedMaskedValue )
55
-
56
- if ( newUnmaskedValue === valueBeforeChange ) {
57
- vTextFieldRef . value ! . value = enforcedMaskedValue
58
- }
59
- val = newUnmaskedValue
60
- updateRange ( )
61
- return returnMaskedValue . value ? mask . mask ( val ) : val
54
+ const newMaskedValue = mask . mask ( valueWithoutDelimiters )
55
+ const newUnmaskedValue = mask . unmask ( newMaskedValue )
56
+
57
+ const newCaretPosition = getNewCaretPosition ( {
58
+ oldValue : model . value ,
59
+ newValue : newMaskedValue ,
60
+ oldCaret : caretPosition . value ,
61
+ } )
62
+
63
+ vTextFieldRef . value ! . value = newMaskedValue
64
+ vTextFieldRef . value ! . setSelectionRange ( newCaretPosition , newCaretPosition )
65
+
66
+ return returnMaskedValue . value ? mask . mask ( newUnmaskedValue ) : newUnmaskedValue
62
67
}
63
68
return val
64
69
} ,
65
70
)
66
71
67
72
const validationValue = toRef ( ( ) => returnMaskedValue . value ? model . value : mask . unmask ( model . value ) )
68
73
74
+ function removeMaskDelimiters ( val : string ) : string {
75
+ return val . split ( '' ) . filter ( ch => ! isMaskDelimiter ( ch ) ) . join ( '' )
76
+ }
77
+
78
+ function getNewCaretPosition ( {
79
+ oldValue,
80
+ newValue,
81
+ oldCaret,
82
+ } : {
83
+ oldValue : string
84
+ newValue : string
85
+ oldCaret : number
86
+ } ) : number {
87
+ if ( ! newValue ) return 0
88
+ if ( ! oldValue ) return newValue . length
89
+
90
+ let newCaret : number
91
+
92
+ if ( inputAction . value === 'Backspace' ) {
93
+ newCaret = oldCaret - 1
94
+ while ( newCaret > 0 && isMaskDelimiter ( newValue [ newCaret - 1 ] ) ) newCaret --
95
+ } else if ( inputAction . value === 'Delete' ) {
96
+ newCaret = oldCaret
97
+ } else { // insertion
98
+ newCaret = oldCaret + 1
99
+ while ( isMaskDelimiter ( newValue [ newCaret ] ) ) newCaret ++
100
+ if ( isMaskDelimiter ( newValue [ oldCaret ] ) ) newCaret ++
101
+ }
102
+
103
+ return newCaret
104
+ }
105
+
69
106
onBeforeMount ( ( ) => {
70
107
if ( props . returnMaskedValue ) {
71
108
emit ( 'update:modelValue' , model . value )
72
109
}
73
110
} )
74
111
75
- function setCaretPosition ( newSelection : number ) {
76
- selection . value = newSelection
77
- vTextFieldRef . value && vTextFieldRef . value . setSelectionRange ( selection . value , selection . value )
112
+ function onKeyDown ( e : KeyboardEvent ) {
113
+ if ( e . metaKey ) return
114
+
115
+ const inputElement = e . target as HTMLInputElement
116
+
117
+ caretPosition . value = inputElement . selectionStart || 0
118
+ inputAction . value = e . key
119
+
120
+ const hasSelection = inputElement . selectionStart !== inputElement . selectionEnd
121
+ if ( e . key === 'Backspace' && hasSelection ) {
122
+ e . preventDefault ( )
123
+ deleteSelection ( e )
124
+ }
125
+ }
126
+
127
+ async function onCut ( e : Event ) {
128
+ e . preventDefault ( )
129
+
130
+ copySelectionToClipboard ( e )
131
+ deleteSelection ( e )
132
+ }
133
+
134
+ async function onPaste ( e : ClipboardEvent ) {
135
+ e . preventDefault ( )
136
+
137
+ const inputElement = e . target as HTMLInputElement
138
+ const pastedString = removeMaskDelimiters ( e . clipboardData ?. getData ( 'text' ) || '' )
139
+
140
+ if ( ! pastedString ) return
141
+
142
+ const pastedCharacters = [ ...pastedString ]
143
+
144
+ const hasSelection = inputElement . selectionStart !== inputElement . selectionEnd
145
+
146
+ if ( hasSelection ) {
147
+ replaceSelection ( inputElement , pastedCharacters )
148
+ } else {
149
+ insertCharacters ( inputElement , pastedCharacters )
150
+ }
78
151
}
79
152
80
- function resetSelections ( ) {
81
- if ( ! vTextFieldRef . value ?. selectionEnd ) return
153
+ function copySelectionToClipboard ( e : Event ) {
154
+ const inputElement = e . target as HTMLInputElement
155
+ const start = inputElement . selectionStart || 0
156
+ const end = inputElement . selectionEnd || 0
157
+ const selectedText = inputElement . value . substring ( start , end )
158
+ navigator . clipboard . writeText ( selectedText )
159
+ }
82
160
83
- selection . value = vTextFieldRef . value . selectionEnd
84
- lazySelection . value = 0
161
+ async function deleteSelection ( e : Event ) {
162
+ const inputElement = e . target as HTMLInputElement
163
+ const curStart = inputElement . selectionStart || 0
164
+ caretPosition . value = inputElement . selectionEnd || 0
85
165
86
- for ( let index = 0 ; index < selection . value ; index ++ ) {
87
- isMaskDelimiter ( vTextFieldRef . value . value [ index ] ) || lazySelection . value ++
166
+ while ( caretPosition . value > curStart ) {
167
+ const success = await simulateBackspace ( inputElement )
168
+ if ( ! success ) break
88
169
}
89
170
}
90
171
91
- function updateRange ( ) {
92
- if ( ! vTextFieldRef . value ) return
93
- resetSelections ( )
172
+ async function simulateBackspace ( inputElement : HTMLInputElement ) {
173
+ inputAction . value = 'Backspace'
174
+ model . value = inputElement . value . slice ( 0 , caretPosition . value - 1 ) + inputElement . value . slice ( caretPosition . value )
175
+ inputAction . value = ''
176
+ if ( caretPosition . value === inputElement . selectionEnd ) return false
177
+ caretPosition . value = inputElement . selectionEnd || 0
178
+ await nextTick ( )
179
+ return true
180
+ }
181
+
182
+ async function insertCharacters ( inputElement : HTMLInputElement , pastedCharacters : string [ ] ) {
183
+ for ( let i = 0 ; i < pastedCharacters . length ; i ++ ) {
184
+ await insertCharacter ( inputElement , pastedCharacters [ i ] )
185
+ }
186
+ }
94
187
95
- let selection = 0
96
- const newValue = vTextFieldRef . value . value
188
+ async function insertCharacter ( inputElement : HTMLInputElement , character : string ) {
189
+ caretPosition . value = inputElement . selectionEnd || 0
190
+ model . value = inputElement . value . slice ( 0 , caretPosition . value ) + character + inputElement . value . slice ( caretPosition . value )
191
+ await nextTick ( )
192
+ }
97
193
98
- if ( newValue ) {
99
- for ( let index = 0 ; index < newValue . length ; index ++ ) {
100
- if ( lazySelection . value <= 0 ) break
101
- isMaskDelimiter ( newValue [ index ] ) || lazySelection . value --
102
- selection ++
103
- }
194
+ async function replaceSelection ( inputElement : HTMLInputElement , pastedCharacters : string [ ] ) {
195
+ caretPosition . value = inputElement . selectionStart || 0
196
+ for ( let i = 0 ; i < pastedCharacters . length ; i ++ ) {
197
+ await replaceCharacter ( caretPosition . value , pastedCharacters [ i ] )
198
+ caretPosition . value ++
104
199
}
105
- setCaretPosition ( selection )
200
+ }
201
+
202
+ async function replaceCharacter ( index : number , character : string ) {
203
+ let targetIndex = index
204
+
205
+ // Find next non-delimiter position
206
+ while ( targetIndex < model . value . length && isMaskDelimiter ( model . value [ targetIndex ] ) ) targetIndex ++
207
+
208
+ model . value = model . value . slice ( 0 , targetIndex ) + character + model . value . slice ( targetIndex + 1 )
209
+ await nextTick ( )
106
210
}
107
211
108
212
useRender ( ( ) => {
@@ -114,6 +218,9 @@ export const VMaskInput = genericComponent<VMaskInputSlots>()({
114
218
v-model = { model . value }
115
219
ref = { vTextFieldRef }
116
220
validationValue = { validationValue . value }
221
+ onCut = { onCut }
222
+ onPaste = { onPaste }
223
+ onKeydown = { onKeyDown }
117
224
>
118
225
{ { ...slots } }
119
226
</ VTextField >
0 commit comments