parent
5f4904d3ce
commit
719c4a786c
@ -0,0 +1,251 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="keyboardWrapRef" class="keyboard-wrap" @click="focusInput">
|
||||||
|
<el-input ref="inputRef" v-model="model" @keyup.enter="handleEnter" :disabled="disabled" v-bind="$attrs">
|
||||||
|
<template v-for="(item, index) in $slots" :key="index" #[index]>
|
||||||
|
<slot :name="index"></slot>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<el-popover v-if="visible" :visible="true" :virtual-ref="inputRef" virtual-triggering placement="bottom" :width="width" :show-arrow="false" :hide-after="0" popper-style="padding: 0px;color:#000" popper-class="keyboard-popper" @after-enter="keyboardInit">
|
||||||
|
<div 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
|
||||||
|
},
|
||||||
|
disabled: Boolean
|
||||||
|
})
|
||||||
|
|
||||||
|
const keyboard = ref<any>(null)
|
||||||
|
const keyboardWrapRef = ref(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}': 'clear',
|
||||||
|
'{close}': 'close',
|
||||||
|
'{arrowleft}': '←',
|
||||||
|
'{arrowright}': '→'
|
||||||
|
})
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
if (visible.value) return
|
||||||
|
inputRef.value.focus()
|
||||||
|
emits('focus')
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
if (props.disabled) return
|
||||||
|
if (visible.value) return
|
||||||
|
emits('focus')
|
||||||
|
if (props.isOpen) visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang: any = {
|
||||||
|
'zh-cn': {
|
||||||
|
// 默认布局
|
||||||
|
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}'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
default: [
|
||||||
|
'\u005F \u0E45 \u002F \u002D \u0E20 \u0E16 \u0E38 \u0E36 \u0E04 \u0E15 \u0E08 \u0E02 \u0E0A {bksp}',
|
||||||
|
'{tab} \u0E46 \u0E44 \u0E33 \u0E1E \u0E30 \u0E31 \u0E35 \u0E23 \u0E19 \u0E22 \u0E1A \u0E25 \u0E03',
|
||||||
|
'{lock} \u0E1F \u0E2B \u0E01 \u0E14 \u0E40 \u0E49 \u0E48 \u0E32 \u0E2A \u0E27 \u0E07 {enter}',
|
||||||
|
'{shift} \u0E1C \u0E1B \u0E41 \u0E2D \u0E34 \u0E37 \u0E17 \u0E21 \u0E43 \u0E1D {clear}',
|
||||||
|
'{arrowleft} {arrowright} {space} {close}'
|
||||||
|
],
|
||||||
|
shift: [
|
||||||
|
'% + \u0E51 \u0E52 \u0E53 \u0E54 \u0E39 \u0E3F \u0E55 \u0E56 \u0E57 \u0E58 \u0E59 {bksp}',
|
||||||
|
'{tab} \u0E50 \u0022 \u0E0E \u0E11 \u0E18 \u0E4D \u0E4A \u0E13 \u0E2F \u0E0D \u0E10 \u002C \u0E05',
|
||||||
|
'{lock} \u0E24 \u0E06 \u0E0F \u0E42 \u0E0C \u0E47 \u0E4B \u0E29 \u0E28 \u0E0B \u002E {enter}',
|
||||||
|
'{shift} ( ) \u0E09 \u0E2E \u0E3A \u0E4C \u003F \u0E12 \u0E2C \u0E26 {clear}',
|
||||||
|
'{arrowleft} {arrowright} {space} {close}'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardInit = () => {
|
||||||
|
let keyboardLayout = {
|
||||||
|
// 数字布局
|
||||||
|
number: ['7 8 9', '4 5 6', '1 2 3', '. 0 {bksp}', '{arrowleft} {arrowright} {clear} {close}'],
|
||||||
|
...lang['zh-cn']
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (lang[useSession('lang').value]) {
|
||||||
|
// keyboardLayout = { ...lang[useSession('lang').value], ...keyboardLayout }
|
||||||
|
// }
|
||||||
|
keyboard.value = new Keyboard('simple-keyboard', {
|
||||||
|
onChange: onChange,
|
||||||
|
onKeyPress: onKeyPress,
|
||||||
|
onInit: onInit,
|
||||||
|
layout: keyboardLayout,
|
||||||
|
layoutName: props.layoutName,
|
||||||
|
display: displayDefault.value
|
||||||
|
// theme: 'hg-theme-default init-keyboard' // 添加自定义class处理清空逻辑
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === '{lock}') return handleLock()
|
||||||
|
if (button === '{change}') return handleChange()
|
||||||
|
if (button === '{clear}') return handleClear()
|
||||||
|
if (button === '{enter}') return handleEnter()
|
||||||
|
if (button === '{close}') return close()
|
||||||
|
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 close = () => {
|
||||||
|
if (props.layoutName == 'number') {
|
||||||
|
// 处理精度
|
||||||
|
model.value = model.value?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2').replace(/\.$/, '')
|
||||||
|
}
|
||||||
|
document.removeEventListener('click', handlePopClose)
|
||||||
|
visible.value = false
|
||||||
|
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 handlePopClose = (e: any) => {
|
||||||
|
// 虚拟键盘区域 输入框区域 中文选项区域
|
||||||
|
if (
|
||||||
|
(e.target as Element).closest('.keyboard-popper') ||
|
||||||
|
(e.target as Element).closest('.keyboard-wrap') == keyboardWrapRef.value ||
|
||||||
|
/hg-candidate-box/.test(e.target.className)
|
||||||
|
) {
|
||||||
|
const index = keyboard.value.getCaretPositionEnd()
|
||||||
|
inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
|
||||||
|
inputRef.value.focus()
|
||||||
|
} else {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 某些情况下未触发动画关闭时销毁事件。此处销毁做后备处理
|
||||||
|
document.removeEventListener('click', handlePopClose)
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ inputRef, visible, open, close, keyboardInit })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.keyboard-wrap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<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>
|
||||||
Loading…
Reference in new issue