Frontend Development 10 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Handling Barcode Scanner Input Issues with Chinese IME in Web Applications Using Vue and Element UI

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.

frontendVueElement UIInput HandlingBarcode ScannerChinese IME
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.