You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
7.3 KiB
294 lines
7.3 KiB
<script setup lang="ts">
|
|
import 'simple-keyboard/build/css/index.css'
|
|
|
|
import Keyboard from 'simple-keyboard'
|
|
import layout from 'simple-keyboard-layouts/build/layouts/chinese.js'
|
|
import { useDeviceStore } from 'stores/deviceStore'
|
|
import { computed, onUnmounted, ref } from 'vue'
|
|
|
|
defineOptions({
|
|
inheritAttrs: false,
|
|
})
|
|
const props = defineProps({
|
|
layoutName: {
|
|
type: String,
|
|
default: 'default',
|
|
},
|
|
// 保留几位小数 layoutName为number时生效
|
|
precision: {
|
|
type: Number,
|
|
default: 2,
|
|
},
|
|
})
|
|
const emits = defineEmits(['onChange', 'enter', 'close', 'focus'])
|
|
const deviceStore = useDeviceStore()
|
|
const deviceType = computed(() => {
|
|
return __DEVICE_TYPE__
|
|
})
|
|
|
|
const model = defineModel()
|
|
const keyboard = ref<any>(null)
|
|
const visible = ref(false)
|
|
const inputRef = ref()
|
|
const popoverRef = ref()
|
|
const entering = ref(false)
|
|
const width = ref(800)
|
|
if (props.layoutName === 'number')
|
|
width.value = 300
|
|
const displayDefault = ref({
|
|
'{bksp}': 'backspace',
|
|
'{lock}': 'caps',
|
|
'{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 = (e: any) => {
|
|
const rect = e.target.getBoundingClientRect()
|
|
if (rect.top + window.scrollY > 800) {
|
|
placement.value = 'top'
|
|
}
|
|
else {
|
|
placement.value = 'bottom'
|
|
}
|
|
if (visible.value)
|
|
return
|
|
if (deviceType.value !== deviceStore.deviceTypeMap.LargeSpaceDM_B)
|
|
visible.value = true
|
|
}
|
|
emits('focus')
|
|
|
|
// const blurInput = debounce(() => {
|
|
// if (!entering.value) {
|
|
// handleClose()
|
|
// } else {
|
|
// entering.value = false
|
|
// }
|
|
// }, 100)
|
|
|
|
const blurInput = () => {
|
|
setTimeout(() => {
|
|
if (!entering.value) {
|
|
handleClose()
|
|
}
|
|
else {
|
|
entering.value = false
|
|
}
|
|
}, 100)
|
|
}
|
|
|
|
const afterEnter = () => {
|
|
// 存在上一个实例时移除元素
|
|
const prevKeyboard = document.querySelectorAll('.init-keyboard')
|
|
if (prevKeyboard.length > 0)
|
|
prevKeyboard[0]?.remove()
|
|
keyboard.value = new Keyboard('simple-keyboard', {
|
|
onChange,
|
|
onKeyPress,
|
|
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 beforeLeave = () => {
|
|
visible.value = false
|
|
entering.value = false
|
|
inputRef.value.blur()
|
|
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 === '{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 = () => {
|
|
entering.value = true
|
|
const currentLayout = keyboard.value.options.layoutName
|
|
const shiftToggle = currentLayout === 'default' ? 'shift' : 'default'
|
|
|
|
keyboard.value.setOptions({
|
|
layoutName: shiftToggle,
|
|
})
|
|
}
|
|
const handleChange = () => {
|
|
entering.value = true
|
|
const 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}'] = '中'
|
|
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 = () => {
|
|
if (props.layoutName === 'number') {
|
|
// 处理精度
|
|
// model.value = model.value?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2').replace(/\.$/, '')
|
|
}
|
|
popoverRef.value.hide()
|
|
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 === inputRef.value?.ref
|
|
|| /hg-candidate-box/.test(e.target.className)
|
|
) {
|
|
entering.value = true
|
|
const index = keyboard.value.getCaretPositionEnd()
|
|
inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
|
|
inputRef.value.focus()
|
|
}
|
|
}
|
|
|
|
const close = () => {
|
|
handleClose()
|
|
}
|
|
|
|
const placement = ref('bottom')
|
|
|
|
onUnmounted(() => {
|
|
// 某些情况下未触发动画关闭时销毁事件。此处销毁做后备处理
|
|
document.removeEventListener('click', handlePopClose)
|
|
})
|
|
|
|
defineExpose({ inputRef, visible, open, close })
|
|
</script>
|
|
|
|
<template>
|
|
<el-input
|
|
ref="inputRef"
|
|
v-model="model"
|
|
v-bind="$attrs"
|
|
@focus="focusInput"
|
|
@blur="blurInput"
|
|
@keyup.enter="handleEnter"
|
|
>
|
|
<template v-for="(item, index) in $slots" :key="index" #[index]>
|
|
<slot :name="index" />
|
|
</template>
|
|
</el-input>
|
|
|
|
<el-popover
|
|
ref="popoverRef"
|
|
:visible="visible"
|
|
:virtual-ref="inputRef"
|
|
virtual-triggering
|
|
:width="width"
|
|
:show-arrow="false"
|
|
:hide-after="0"
|
|
:placement="placement"
|
|
popper-style="padding: 0px;color:#000"
|
|
:persistent="false"
|
|
popper-class="keyboard-popper"
|
|
@after-enter="afterEnter"
|
|
@before-leave="beforeLeave"
|
|
>
|
|
<div class="simple-keyboard" />
|
|
</el-popover>
|
|
</template>
|
|
|
|
<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>
|