Handling Barcode Scanner Input Issues with Chinese IME in Web Applications Using Vue and Element UI
This article analyzes why barcode scanners lose characters under Chinese input methods, proposes two mitigation strategies—including a keydown‑event listener and a readonly password‑input workaround—and provides a complete Vue/Element‑UI component implementation to reliably capture scanner data without triggering IME or browser autofill.
Problem: In a company project, barcode scanners lose characters when Chinese input methods such as Sogou, Microsoft, Baidu, or QQ are active because the scanner emulates keyboard input, which triggers the IME’s Chinese mode and auto‑completion.
Analysis: The scanner hardware simulates user keystrokes, causing the input method to switch to Chinese mode and interfere with the rapid character stream, leading to missing characters.
Solution 1: Listen to keydown events, collect characters that arrive within about 30 ms, concatenate them into a string, and write the result back to the input field. Example code:
function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q','W','E','R','T','Y','U','I','O','P','A','S','D','F','G','H','J','K','L','Z','X','C','V','B','N','M','1','2','3','4','5','6','7','8','9','0','-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // manual Chinese input
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
this.code += shiftKey ? a : a.toLowerCase()
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
// TODO: handle completed code
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
// TODO: handle completed code
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}Solution 2: Use input[type=password] with readonly (and optionally autocomplete="off" or autocomplete="new-password" ) to force the input field into English mode, preventing the IME from interfering. This also suppresses password‑save prompts, though Chromium‑based browsers may still show autofill suggestions.
Implementation: A Vue component combined with Element‑UI is built. It renders a hidden password input that toggles readonly on focus/blur/click events and forwards scanner data through normal el-input . Full component code:
<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
</el-input>
</div>
</template>
<script>
export default {
name: 'WispathScannerInput',
data() { return { isFocus: false } },
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)
const entries = Object.entries(this.$refs.scannerInput)
for (const [key, value] of entries) {
if (typeof value === 'function') { this[key] = value }
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => { this.$el.firstElementChild.removeAttribute('readonly') })
}
},
onPasswordFocus() { this.isFocus = true; setTimeout(() => { this.$el.firstElementChild.removeAttribute('readonly') }) },
onPasswordBlur() { this.isFocus = false; this.$el.firstElementChild.setAttribute('readonly', true) },
onPasswordMouseDown() { this.$el.firstElementChild.setAttribute('readonly', true) },
oPasswordKeyDown(ev) { if (ev.key === 'Enter') { this.$el.firstElementChild.setAttribute('readonly', true); setTimeout(() => { this.$el.firstElementChild.removeAttribute('readonly') }) } },
onPasswordClick() { if (this.isFocus) { this.$el.firstElementChild.setAttribute('readonly', true); setTimeout(() => { this.$el.firstElementChild.removeAttribute('readonly') }, 200) } },
onInput(_value) { this.$emit('input', _value) },
getList(value) { this.$emit('input', value) }
}
}
</script>
<style lang="scss" scoped>
.scanner-input {
position: relative; height: 36px; width: 100%; display: inline-block;
.input-password { width: 100%; height: 100%; border: none; outline: none; padding: 0 16px; font-size: 14px; letter-spacing: 3px; background: transparent; color: transparent; }
.input-text { font-size: 14px; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none; background-color: transparent; ::v-deep .el-input__inner { padding: 0 16px; width: 100%; height: 100%; } }
.input-text-focus ::v-deep .el-input__inner { outline: none; border-color: #1c7af4; }
}
</style>Result: The component prevents password‑autocomplete prompts and save‑dialog pop‑ups while capturing scanner input, but the visual cursor is not perfectly displayed, which may affect manual editing; further research on cursor simulation is suggested.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.