routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
import { mergeProps as _mergeProps, createVNode as _createVNode } from "vue";
|
||||
// Components
|
||||
import { makeVTextFieldProps, VTextField } from "../../components/VTextField/VTextField.js"; // Composables
|
||||
import { forwardRefs } from "../../composables/forwardRefs.js";
|
||||
import { makeMaskProps, useMask } from "../../composables/mask/index.js";
|
||||
import { useProxiedModel } from "../../composables/proxiedModel.js"; // Utilities
|
||||
import { computed, nextTick, onBeforeMount, ref, shallowRef, toRef } from 'vue';
|
||||
import { genericComponent, propsFactory, useRender } from "../../util/index.js"; // Types
|
||||
export const makeVMaskInputProps = propsFactory({
|
||||
returnMaskedValue: Boolean,
|
||||
...makeVTextFieldProps(),
|
||||
...makeMaskProps()
|
||||
}, 'VMaskInput');
|
||||
export const VMaskInput = genericComponent()({
|
||||
name: 'VMaskInput',
|
||||
props: makeVMaskInputProps(),
|
||||
emits: {
|
||||
'update:modelValue': val => true
|
||||
},
|
||||
setup(props, {
|
||||
slots,
|
||||
emit
|
||||
}) {
|
||||
const vTextFieldRef = ref();
|
||||
const inputAction = shallowRef();
|
||||
const caretPosition = shallowRef(0);
|
||||
const mask = useMask(props);
|
||||
const returnMaskedValue = computed(() => props.mask && props.returnMaskedValue);
|
||||
const model = useProxiedModel(props, 'modelValue', undefined,
|
||||
// Always display masked value in input when mask is applied
|
||||
val => props.mask ? mask.mask(mask.unmask(val)) : val, val => {
|
||||
if (props.mask) {
|
||||
// E.g. mask is #-# and the input value is '2-23'
|
||||
// model-value should be enforced to '2-2'
|
||||
const newMaskedValue = mask.mask(mask.unmask(val));
|
||||
const newUnmaskedValue = mask.unmask(newMaskedValue);
|
||||
const newCaretPosition = getNewCaretPosition({
|
||||
oldValue: model.value,
|
||||
newValue: newMaskedValue,
|
||||
oldCaret: caretPosition.value
|
||||
});
|
||||
vTextFieldRef.value.value = newMaskedValue;
|
||||
vTextFieldRef.value.setSelectionRange(newCaretPosition, newCaretPosition);
|
||||
return returnMaskedValue.value ? mask.mask(newUnmaskedValue) : newUnmaskedValue;
|
||||
}
|
||||
return val;
|
||||
});
|
||||
const validationValue = toRef(() => returnMaskedValue.value ? model.value : mask.unmask(model.value));
|
||||
function getNewCaretPosition({
|
||||
oldValue,
|
||||
newValue,
|
||||
oldCaret
|
||||
}) {
|
||||
if (!newValue) return 0;
|
||||
if (!oldValue) return newValue.length;
|
||||
let newCaret;
|
||||
if (inputAction.value === 'Backspace') {
|
||||
newCaret = oldCaret - 1;
|
||||
while (newCaret > 0 && mask.isDelimiter(newValue, newCaret - 1)) newCaret--;
|
||||
} else if (inputAction.value === 'Delete') {
|
||||
newCaret = oldCaret;
|
||||
} else {
|
||||
// insertion
|
||||
newCaret = oldCaret + 1;
|
||||
while (mask.isDelimiter(newValue, newCaret)) newCaret++;
|
||||
if (mask.isDelimiter(newValue, oldCaret)) newCaret++;
|
||||
}
|
||||
return newCaret;
|
||||
}
|
||||
onBeforeMount(() => {
|
||||
if (props.returnMaskedValue) {
|
||||
emit('update:modelValue', model.value);
|
||||
}
|
||||
});
|
||||
function onKeyDown(e) {
|
||||
if (e.metaKey) return;
|
||||
const inputElement = e.target;
|
||||
caretPosition.value = inputElement.selectionStart || 0;
|
||||
inputAction.value = e.key;
|
||||
const hasSelection = inputElement.selectionStart !== inputElement.selectionEnd;
|
||||
if (e.key === 'Backspace' && hasSelection) {
|
||||
e.preventDefault();
|
||||
deleteSelection(e);
|
||||
}
|
||||
}
|
||||
async function onCut(e) {
|
||||
e.preventDefault();
|
||||
await copySelectionToClipboard(e);
|
||||
await deleteSelection(e);
|
||||
}
|
||||
async function onPaste(e) {
|
||||
e.preventDefault();
|
||||
const inputElement = e.target;
|
||||
const pastedString = e.clipboardData?.getData('text') || '';
|
||||
if (!pastedString) return;
|
||||
const pastedCharacters = [...pastedString];
|
||||
const hasSelection = inputElement.selectionStart !== inputElement.selectionEnd;
|
||||
if (hasSelection) {
|
||||
replaceSelection(inputElement, pastedCharacters);
|
||||
} else {
|
||||
insertCharacters(inputElement, pastedCharacters);
|
||||
}
|
||||
}
|
||||
async function copySelectionToClipboard(e) {
|
||||
const inputElement = e.target;
|
||||
const start = inputElement.selectionStart || 0;
|
||||
const end = inputElement.selectionEnd || 0;
|
||||
const selectedText = inputElement.value.substring(start, end);
|
||||
await navigator.clipboard.writeText(selectedText);
|
||||
}
|
||||
async function deleteSelection(e) {
|
||||
const inputElement = e.target;
|
||||
const curStart = inputElement.selectionStart || 0;
|
||||
caretPosition.value = inputElement.selectionEnd || 0;
|
||||
while (caretPosition.value > curStart) {
|
||||
const success = await simulateBackspace(inputElement);
|
||||
if (!success) break;
|
||||
}
|
||||
}
|
||||
async function simulateBackspace(inputElement) {
|
||||
inputAction.value = 'Backspace';
|
||||
model.value = inputElement.value.slice(0, caretPosition.value - 1) + inputElement.value.slice(caretPosition.value);
|
||||
inputAction.value = '';
|
||||
if (caretPosition.value === inputElement.selectionEnd) return false;
|
||||
caretPosition.value = inputElement.selectionEnd || 0;
|
||||
await nextTick();
|
||||
return true;
|
||||
}
|
||||
async function insertCharacters(inputElement, pastedCharacters) {
|
||||
for (let i = 0; i < pastedCharacters.length; i++) {
|
||||
await insertCharacter(inputElement, pastedCharacters[i]);
|
||||
}
|
||||
}
|
||||
async function insertCharacter(inputElement, character) {
|
||||
caretPosition.value = inputElement.selectionEnd || 0;
|
||||
model.value = inputElement.value.slice(0, caretPosition.value) + character + inputElement.value.slice(caretPosition.value);
|
||||
await nextTick();
|
||||
}
|
||||
async function replaceSelection(inputElement, pastedCharacters) {
|
||||
caretPosition.value = inputElement.selectionStart || 0;
|
||||
for (let i = 0; i < pastedCharacters.length; i++) {
|
||||
if (await replaceCharacter(caretPosition.value, pastedCharacters[i])) {
|
||||
caretPosition.value++;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function replaceCharacter(index, character) {
|
||||
let targetIndex = index;
|
||||
|
||||
// Find next non-delimiter position
|
||||
while (targetIndex < model.value.length && mask.isDelimiter(model.value, targetIndex)) {
|
||||
targetIndex++;
|
||||
}
|
||||
const newValue = model.value.slice(0, targetIndex) + character + model.value.slice(targetIndex + 1);
|
||||
if (mask.isValid(newValue)) {
|
||||
model.value = newValue;
|
||||
await nextTick();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
useRender(() => {
|
||||
const textFieldProps = VTextField.filterProps(props);
|
||||
return _createVNode(VTextField, _mergeProps(textFieldProps, {
|
||||
"modelValue": model.value,
|
||||
"onUpdate:modelValue": $event => model.value = $event,
|
||||
"ref": vTextFieldRef,
|
||||
"class": ['v-mask-input', props.class],
|
||||
"style": props.style,
|
||||
"validationValue": validationValue.value,
|
||||
"onCut": onCut,
|
||||
"onPaste": onPaste,
|
||||
"onKeydown": onKeyDown
|
||||
}), {
|
||||
...slots
|
||||
});
|
||||
});
|
||||
return forwardRefs({}, vTextFieldRef);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=VMaskInput.js.map
|
||||
Reference in New Issue
Block a user