migrate pin code input to composition API and typescript

This commit is contained in:
MaysWind
2025-01-04 16:11:02 +08:00
parent 27f8c90dae
commit af9aa726f4
+119 -115
View File
@@ -3,8 +3,7 @@
<div class="pin-code-input pin-code-input-outline" <div class="pin-code-input pin-code-input-outline"
:class="{ 'pin-code-input-focued': codes[index].focused }" :key="index" :class="{ 'pin-code-input-focued': codes[index].focused }" :key="index"
v-for="(code, index) in codes"> v-for="(code, index) in codes">
<input min="0" maxlength="1" pattern="[0-9]*" <input ref="pin-code-input" min="0" maxlength="1" pattern="[0-9]*"
:ref="`pin-code-input-${index}`"
:value="codes[index].value" :value="codes[index].value"
:type="codes[index].inputType" :type="codes[index].inputType"
:disabled="disabled ? 'disabled' : undefined" :disabled="disabled ? 'disabled' : undefined"
@@ -19,66 +18,51 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { type Ref, ref, computed, watch, useTemplateRef } from 'vue';
props: [
'modelValue',
'disabled',
'autofocus',
'secure',
'length'
],
emits: [
'update:modelValue',
'pincode:confirm'
],
data() {
return {
codes: []
}
},
computed: {
finalPinCode() {
let finalPinCode = '';
for (let i = 0; i < this.codes.length; i++) { interface PinCode {
if (this.codes[i].value) { value: string;
finalPinCode += this.codes[i].value; inputType: string;
inputTimer: number | null;
focused: boolean;
}
const props = defineProps<{
modelValue: string;
length: number;
disabled?: boolean;
autofocus?: boolean;
secure?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'pincode:confirm', value: string): void
}>();
const codes: Ref<PinCode[]> = ref([]);
const pinCodeInputs: Ref<HTMLInputElement[]> = useTemplateRef('pin-code-input');
const finalPinCode = computed<string>(() => {
let ret = '';
for (let i = 0; i < codes.value.length; i++) {
if (codes.value[i].value) {
ret += codes.value[i].value;
} else { } else {
break; break;
} }
} }
return finalPinCode; return ret;
} });
},
watch: {
'length': function (newValue) {
this.init(newValue, this.modelValue);
},
'modelValue': function (newValue) {
if (newValue === this.finalPinCode) {
return;
}
this.init(this.length, newValue); function init(length: number, value: string): void {
}, codes.value.length = 0;
'codes': {
handler() {
this.$emit('update:modelValue', this.finalPinCode);
},
deep: true
}
},
created() {
this.init(this.length, this.modelValue);
},
methods: {
init(length, value) {
this.codes.length = 0;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const code = { const code: PinCode = {
value: '', value: '',
inputType: 'tel', inputType: 'tel',
inputTimer: null, inputTimer: null,
@@ -88,111 +72,111 @@ export default {
if (value && value[i]) { if (value && value[i]) {
code.value = value[i]; code.value = value[i];
if (this.secure) { if (props.secure) {
code.inputType = 'password'; code.inputType = 'password';
} }
} }
this.codes.push(code); codes.value.push(code);
} }
}, }
autoFillText(index, text) {
function autoFillText(index: number, text: string): void {
let lastIndex = index; let lastIndex = index;
for (let i = index, j = 0; i < this.codes.length && j < text.length; i++, j++) { for (let i = index, j = 0; i < codes.value.length && j < text.length; i++, j++) {
if (text[j] < '0' || text[j] > '9') { if (text[j] < '0' || text[j] > '9') {
this.codes[i].value = ''; codes.value[i].value = '';
this.$forceUpdate();
break; break;
} }
this.codes[i].value = text[j]; codes.value[i].value = text[j];
this.setInputType(i); setInputType(i);
lastIndex = i; lastIndex = i;
} }
this.setFocus(lastIndex); setFocus(lastIndex);
if (this.finalPinCode.length === this.length) { if (finalPinCode.value.length === length) {
this.$emit('pincode:confirm', this.finalPinCode); emit('pincode:confirm', finalPinCode.value);
}
} }
},
setInputType(index) {
const self = this;
if (!self.secure) { function setInputType(index: number): void {
if (!props.secure) {
return; return;
} }
if (!self.codes[index].value) { if (!codes.value[index].value) {
self.codes[index].inputType = 'tel'; codes.value[index].inputType = 'tel';
return; return;
} }
if (self.codes[index].inputTimer) { if (codes.value[index].inputTimer) {
return; return;
} }
self.codes[index].inputTimer = setTimeout(() => { codes.value[index].inputTimer = setTimeout(() => {
if (self.codes[index].value) { if (codes.value[index].value) {
self.codes[index].inputType = 'password'; codes.value[index].inputType = 'password';
} else { } else {
self.codes[index].inputType = 'tel'; codes.value[index].inputType = 'tel';
} }
self.codes[index].inputTimer = null; codes.value[index].inputTimer = null;
}, 300); }, 300);
}, }
setFocus(index) {
const refId = `pin-code-input-${index}`;
const ref = this.$refs[refId];
if (ref && ref[0]) { function setFocus(index: number): void {
ref[0].focus(); if (pinCodeInputs.value[index]) {
ref[0].select(); pinCodeInputs.value[index].focus();
pinCodeInputs.value[index].select();
} }
}, }
setPreviousFocus(index) {
function setPreviousFocus(index: number): void {
if (index > 0) { if (index > 0) {
this.setFocus(index - 1); setFocus(index - 1);
} }
},
setNextFocus(index) {
if (index < this.length - 1) {
this.setFocus(index + 1);
} }
},
onKeydown(index, event) { function setNextFocus(index: number): void {
if (index < props.length - 1) {
setFocus(index + 1);
}
}
function onKeydown(index: number, event: KeyboardEvent): void {
if (event.altKey || (event.key.indexOf('F') === 0 && (event.key.length === 2 || event.key.length === 3))) { if (event.altKey || (event.key.indexOf('F') === 0 && (event.key.length === 2 || event.key.length === 3))) {
return; return;
} }
if (event.key === 'Enter' && this.finalPinCode.length === this.length) { if (event.key === 'Enter' && finalPinCode.value.length === props.length) {
this.$emit('pincode:confirm', this.finalPinCode); emit('pincode:confirm', finalPinCode.value);
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.key === 'ArrowLeft' || (event.shiftKey && event.key === 'Tab')) { if (event.key === 'ArrowLeft' || (event.shiftKey && event.key === 'Tab')) {
this.setPreviousFocus(index); setPreviousFocus(index);
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.key === 'ArrowRight' || (!event.shiftKey && event.key === 'Tab')) { if (event.key === 'ArrowRight' || (!event.shiftKey && event.key === 'Tab')) {
this.setNextFocus(index); setNextFocus(index);
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.key === 'Home') { if (event.key === 'Home') {
this.setFocus(0); setFocus(0);
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.key === 'End') { if (event.key === 'End') {
this.setFocus(this.length - 1); setFocus(props.length - 1);
event.preventDefault(); event.preventDefault();
return; return;
} }
@@ -202,13 +186,13 @@ export default {
} }
if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Del') { if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Del') {
for (let i = index; i < this.codes.length; i++) { for (let i = index; i < codes.value.length; i++) {
this.codes[i].value = ''; codes.value[i].value = '';
this.setInputType(i); setInputType(i);
} }
if (event.code === 'Backspace') { if (event.code === 'Backspace') {
this.setPreviousFocus(index); setPreviousFocus(index);
} }
event.preventDefault(); event.preventDefault();
@@ -216,18 +200,19 @@ export default {
} }
if (event.key.length === 1 && '0' <= event.key && event.key <= '9') { if (event.key.length === 1 && '0' <= event.key && event.key <= '9') {
this.codes[index].value = event.key; codes.value[index].value = event.key;
this.setInputType(index); setInputType(index);
this.setNextFocus(index); setNextFocus(index);
if (this.finalPinCode.length === this.length) { if (finalPinCode.value.length === props.length) {
this.$emit('pincode:confirm', this.finalPinCode); emit('pincode:confirm', finalPinCode.value);
} }
} }
event.preventDefault(); event.preventDefault();
}, }
onPaste(index, event) {
function onPaste(index: number, event: ClipboardEvent): void {
if (!event.clipboardData) { if (!event.clipboardData) {
event.preventDefault(); event.preventDefault();
return; return;
@@ -240,22 +225,41 @@ export default {
return; return;
} }
this.autoFillText(index, text); autoFillText(index, text);
event.preventDefault(); event.preventDefault();
}, }
onInput(index, event) {
function onInput(index: number, event: InputEvent): void {
if (!event.target.value) { if (!event.target.value) {
event.preventDefault(); event.preventDefault();
return; return;
} }
this.autoFillText(index, event.target.value); autoFillText(index, event.target.value);
event.preventDefault(); event.preventDefault();
} }
watch(() => props.length, newValue => {
init(newValue, props.modelValue);
});
watch(() => props.modelValue, newValue => {
if (newValue === finalPinCode.value) {
return;
} }
}
init(props.length, newValue);
});
watch(codes, () => {
emit('update:modelValue', finalPinCode.value);
}, {
deep: true
});
init(props.length, props.modelValue);
</script> </script>
<style> <style>