Skip to content

Commit 39b743f

Browse files
scott-vanbrugRonaldJerez
authored andcommitted
fix: handle composition events for better international keyboard support
Add handling for composition events, allowing users to compose inputs normally before committing a value to be masked. Replaces fix from v1.3.8 with more robust handling of the composition events.
1 parent bccc119 commit 39b743f

File tree

3 files changed

+123
-4
lines changed

3 files changed

+123
-4
lines changed

src/core.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,23 @@ export function getInputElement(el) {
5454
* @param {Event} event The event object
5555
*/
5656
export function inputHandler(event) {
57-
const { target, detail } = event
57+
const { target, detail, inputType } = event
5858

5959
// We dont need to run this method on the event we emit (prevent event loop)
60-
if ((detail && detail.facade) || event.inputType === 'insertCompositionText') {
60+
if (detail && detail.facade) {
6161
return false
6262
}
6363

6464
// since we will be emitting our own custom input event
6565
// we can stop propagation of this native event
6666
event.stopPropagation()
6767

68+
// Ignore input events related to composition, specific composition
69+
// events will handle updating the input after text is composed
70+
if (['insertCompositionText', 'insertFromComposition'].includes(inputType)) {
71+
return false
72+
}
73+
6874
const originalValue = target.value
6975
const originalPosition = target.selectionEnd
7076
const { oldValue } = target[CONFIG_KEY]
@@ -138,9 +144,14 @@ export function updateCursor(event, originalValue, originalPosition) {
138144
* @param {Event} [event] The event that triggered this this update, null if not triggered by an input event
139145
*/
140146
export function updateValue(el, vnode, { emit = true, force = false } = {}, event) {
141-
let { config, oldValue } = el[CONFIG_KEY]
147+
let { config, oldValue, isComposing } = el[CONFIG_KEY]
142148
let currentValue = vnode && vnode.data.model ? vnode.data.model.value : el.value
143149

150+
// manipulating input value while text is being composed can lead to inputs being duplicated
151+
if (isComposing) {
152+
return
153+
}
154+
144155
oldValue = oldValue || ''
145156
currentValue = currentValue || ''
146157

src/directive.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,30 @@ export default {
2626
core.inputHandler(e, el)
2727
}
2828

29+
const compositionHandler = (e) => {
30+
if (e.target !== el) {
31+
return
32+
}
33+
34+
if (['compositionstart', 'compositionupdate'].includes(e.type)) {
35+
el[CONFIG_KEY].isComposing = true
36+
} else if (e.type === 'compositionend') {
37+
el[CONFIG_KEY].isComposing = false
38+
core.inputHandler(e, el)
39+
}
40+
}
41+
2942
handlerOwner.addEventListener('input', handler, true)
43+
handlerOwner.addEventListener('compositionstart', compositionHandler, true)
44+
handlerOwner.addEventListener('compositionupdate', compositionHandler, true)
45+
handlerOwner.addEventListener('compositionend', compositionHandler, true)
3046

31-
config.cleanup = () => handlerOwner.removeEventListener('input', handler, true)
47+
config.cleanup = () => {
48+
handlerOwner.removeEventListener('input', handler, true)
49+
handlerOwner.removeEventListener('compositionstart', compositionHandler, true)
50+
handlerOwner.removeEventListener('compositionend', compositionHandler, true)
51+
handlerOwner.removeEventListener('compositionupdate', compositionHandler, true)
52+
}
3253
},
3354

3455
update: (el, { value, oldValue, modifiers }, vnode) => {

tests/directive.test.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,71 @@ describe('Directive', () => {
9292
expect(wrapper.element.unmaskedValue).toBe('1122')
9393
})
9494

95+
describe('Composition events', () => {
96+
test('Should not update value when composing the input', () => {
97+
buildWrapper({ value: 1234 })
98+
expect(wrapper.element.value).toBe('12.34')
99+
100+
wrapper.element.value = '1122'
101+
wrapper.find('input').trigger('compositionstart')
102+
wrapper.find('input').trigger('input', { inputType: 'insertCompositionText' })
103+
104+
expect(wrapper.element.value).toBe('1122')
105+
expect(wrapper.element.unmaskedValue).toBe('1234')
106+
})
107+
108+
test('Should not update value when updating the composed input', () => {
109+
buildWrapper({ value: 1234 })
110+
expect(wrapper.element.value).toBe('12.34')
111+
112+
wrapper.element.value = '1122'
113+
wrapper.find('input').trigger('compositionupdate')
114+
wrapper.find('input').trigger('input', { inputType: 'insertCompositionText' })
115+
116+
expect(wrapper.element.value).toBe('1122')
117+
expect(wrapper.element.unmaskedValue).toBe('1234')
118+
})
119+
120+
test('Should update value when composition ends', () => {
121+
buildWrapper({ value: 1234 })
122+
expect(wrapper.element.value).toBe('12.34')
123+
124+
wrapper.element.value = '1122'
125+
wrapper.find('input').trigger('compositionstart')
126+
wrapper.find('input').trigger('input', { inputType: 'insertCompositionText' })
127+
128+
expect(wrapper.element.value).toBe('1122')
129+
expect(wrapper.element.unmaskedValue).toBe('1234')
130+
131+
wrapper.find('input').trigger('compositionend')
132+
133+
expect(wrapper.element.value).toBe('11.22')
134+
expect(wrapper.element.unmaskedValue).toBe('1122')
135+
})
136+
137+
test('Should prevent all value updates while composing text', () => {
138+
buildWrapper({ value: 1234 })
139+
expect(wrapper.element.value).toBe('12.34')
140+
141+
wrapper.element.value = '1122'
142+
wrapper.find('input').trigger('compositionstart')
143+
wrapper.find('input').trigger('input', { inputType: 'insertCompositionText' })
144+
wrapper.setValue('4321')
145+
146+
expect(wrapper.element.value).toBe('4321')
147+
expect(wrapper.element.unmaskedValue).toBe('1234')
148+
})
149+
150+
test('Should prevent composition input events from propagating', () => {
151+
buildWrapper()
152+
153+
wrapper.find('input').trigger('input', { inputType: 'insertCompositionText' })
154+
wrapper.find('input').trigger('input', { inputType: 'insertFromComposition' })
155+
156+
expect(inputListener).toHaveBeenCalledTimes(0)
157+
})
158+
})
159+
95160
test('Should honor short modifier', async () => {
96161
buildWrapper({
97162
template: `<input v-facade.short="mask" value="12" @input="inputListener" />`
@@ -241,5 +306,27 @@ describe('Directive', () => {
241306

242307
expect(inputListener).toBeCalledTimes(1)
243308
})
309+
310+
test('should not prevent updates when other inputs are composing text', async () => {
311+
otherInput.setValue('1122')
312+
313+
otherInput.trigger('compositionstart')
314+
315+
facadeInput.element.value = '1234'
316+
facadeInput.trigger('input')
317+
318+
expect(otherInput.element.value).toBe('1122')
319+
expect(facadeInput.element.value).toBe('12.34')
320+
expect(inputListener).toBeCalledTimes(1)
321+
})
322+
323+
test('should handle composition events on the main element', async () => {
324+
facadeInput.setValue('1122')
325+
facadeInput.element.value = '1234'
326+
facadeInput.trigger('compositionstart')
327+
facadeInput.trigger('input', { inputType: 'insertCompositionText' })
328+
329+
expect(facadeInput.element.unmaskedValue).toBe('1122')
330+
})
244331
})
245332
})

0 commit comments

Comments
 (0)