vue3中使用simple-keyboard實現虛擬鍵盤(帶中文切換數字鍵盤)

萝卜爱吃肉發表於2024-03-26

效果圖

image

官網

simple-keyboard官網:https://hodgef.com/simple-keyboard/ 打不開的話請用魔法

不足

中文語言包支援度不夠。不過自己可以找語言包替換

依賴安裝

npm install simple-keyboard --save
npm install simple-keyboard-layouts --save // 中文語言包

元件程式碼

<template>
  <el-input ref="inputRef" v-model="model" @focus="focusInput" @keyup.enter="handleEnter" v-bind="$attrs">
    <template v-for="(item, index) in $slots" :key="index" #[index]>
      <slot :name="index"></slot>
    </template>
  </el-input>

  <el-popover
    :visible="visible"
    :virtual-ref="inputRef"
    virtual-triggering
    placement="bottom"
    :width="width"
    :show-arrow="false"
    :hide-after="0"
    popper-style="padding: 0px;color:#000"
    :persistent="false"
    popper-class="keyboard-popper"
    @after-leave="afterLeave"
    @after-enter="afterEnter"
  >
    <div v-if="visible" class="simple-keyboard"></div>
  </el-popover>
</template>

<script setup lang="ts">
import Keyboard from 'simple-keyboard'
import 'simple-keyboard/build/css/index.css'
import layout from 'simple-keyboard-layouts/build/layouts/chinese.js'
defineOptions({
  inheritAttrs: false
})
const model = defineModel<string>()
const emits = defineEmits(['onChange', 'enter', 'close', 'focus'])
const props = defineProps({
  layoutName: {
    type: String,
    default: 'default'
  },
  // 保留幾位小數 layoutName為number時生效
  precision: {
    type: Number,
    default: 2
  },
  // 獲取焦點開啟鍵盤
  isOpen: {
    type: Boolean,
    default: true
  }
})
const keyboard = ref<any>(null)
const visible = ref(false)
const inputRef = ref()
const width = ref(1000)
if (props.layoutName == 'number') width.value = 300
const displayDefault = ref({
  '{bksp}': 'backspace',
  '{lock}': 'caps',
  '{enter}': 'enter',
  '{tab}': 'tab',
  '{shift}': 'shift',
  '{change}': 'en',
  '{space}': 'space',
  '{clear}': '清空',
  '{close}': '關閉',
  '{arrowleft}': '←',
  '{arrowright}': '→'
})

const open = () => {
  if (visible.value) return
  inputRef.value.focus()
  emits('focus')
  visible.value = true
}
const focusInput = () => {
  if (visible.value) return
  emits('focus')
  if (props.isOpen) visible.value = true
}
const afterEnter = () => {
  // 存在上一個例項時移除元素
  const prevKeyboard = document.querySelectorAll('.init-keyboard')
  if (prevKeyboard.length > 0) prevKeyboard[0]?.remove()
  keyboard.value = new Keyboard('simple-keyboard', {
    onChange: onChange,
    onKeyPress: onKeyPress,
    onInit: onInit,
    layout: {
      // 預設佈局
      default: [
        '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
        '{tab} q w e r t y u i o p [ ] \\',
        "{lock} a s d f g h j k l ; ' {enter}",
        '{change} z x c v b n m , . / {clear}',
        '{arrowleft} {arrowright} {space} {close}'
      ],
      // 大小寫
      shift: [
        '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
        '{tab} Q W E R T Y U I O P { } |',
        '{lock} A S D F G H J K L : " {enter}',
        '{change} Z X C V B N M < > ? {clear}',
        '{arrowleft} {arrowright} {space} {close}'
      ],
      // 數字佈局
      number: ['7 8 9', '4 5 6', '1 2 3', '. 0 {bksp}', '{arrowleft} {arrowright} {clear} {close}']
    },
    layoutName: props.layoutName,
    display: displayDefault.value,
    theme: 'hg-theme-default init-keyboard' // 新增自定義class處理清空邏輯
  })
}
const afterLeave = () => {
  displayDefault.value['{change}'] = 'en'
  document.removeEventListener('click', handlePopClose)
}
const onInit = (keyboard: any) => {
  keyboard.setInput(model.value)
  keyboard.setCaretPosition(inputRef.value?.ref.selectionEnd)
  document.addEventListener('click', handlePopClose)
}
const onChange = (input: any) => {
  model.value = input
  emits('onChange', input)
}
const onKeyPress = (button: any) => {
  if (button !== '{close}') handleFocus()
  if (button === '{lock}') return handleLock()
  if (button === '{change}') return handleChange()
  if (button === '{clear}') return handleClear()
  if (button === '{enter}') return handleEnter()
  if (button === '{close}') return handleClose()
  if (button === '{arrowleft}') return handleArrow(0)
  if (button === '{arrowright}') return handleArrow(1)
}
const handleLock = () => {
  let currentLayout = keyboard.value.options.layoutName
  let shiftToggle = currentLayout === 'default' ? 'shift' : 'default'

  keyboard.value.setOptions({
    layoutName: shiftToggle
  })
}
const handleChange = () => {
  let layoutCandidates = keyboard.value.options.layoutCandidates
  // 切換中英文輸入法
  if (layoutCandidates != null && layoutCandidates != undefined) {
    displayDefault.value['{change}'] = 'en'
    keyboard.value.setOptions({
      layoutName: 'default',
      layoutCandidates: null,
      display: displayDefault.value
    })
  } else {
    displayDefault.value['{change}'] = 'cn'
    keyboard.value.setOptions({
      layoutName: 'default',
      layoutCandidates: (layout as any).layoutCandidates,
      display: displayDefault.value
    })
  }
}
const handleClear = () => {
  keyboard.value.clearInput()
  model.value = ''
}
const handleEnter = () => {
  emits('enter')
}
const handleClose = () => {
  visible.value = false
  if (props.layoutName == 'number') {
    // 處理精度
    model.value = model.value?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2').replace(/\.$/, '')
  }
  emits('close')
}
const handleArrow = (num: number) => {
  // 處理左右箭頭下標位置
  const index = keyboard.value.getCaretPositionEnd()
  if (num == 0 && index - 1 >= 0) {
    keyboard.value.setCaretPosition(index - 1)
  } else if (num == 1 && index + 1 <= (model.value?.length || 0)) {
    keyboard.value.setCaretPosition(index + 1)
  }
}
const handleFocus = () => {
  // 處理焦點顯示
  setTimeout(() => {
    const index = keyboard.value.getCaretPositionEnd()
    inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
    inputRef.value.focus()
  }, 0)
}
const handlePopClose = (e: any) => {
  // 虛擬鍵盤區域 輸入框區域 中文選項區域
  if (
    (e.target as Element).closest('.keyboard-popper') ||
    e.target == inputRef.value?.ref ||
    /hg-candidate-box/.test(e.target.className)
  ) {
    return
  }
  handleClose()
}
const close = () => {
  handleClose()
}
defineExpose({ inputRef, visible, open, close })
</script>

<style>
.hg-theme-default .hg-button.hg-button-arrowleft,
.hg-theme-default .hg-button.hg-button-arrowright {
  max-width: 70px;
}
.hg-theme-default .hg-button.hg-button-close {
  max-width: 100px;
}
.hg-layout-number .hg-button.hg-button-close {
  max-width: none;
}
.hg-layout-number .hg-button.hg-button-bksp {
  max-width: 92px;
}
</style>

相關文章