Files
CRM_Montzar/apps/web/src/components/MoneyInput.vue
T

138 lines
3.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue';
import {
caretIndexForDigitCount,
countDigitChars,
formatGroupedInput,
parseMoneyNumber,
} from '@/lib/money';
const model = defineModel<string | number | null>({ default: '' });
const props = withDefaults(
defineProps<{
id?: string;
placeholder?: string;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
/** حداکثر رقم اعشار؛ پیش‌فرض ۲ */
decimals?: number;
/** اگر true باشد مقدار عددی emit می‌شود (نه رشته) */
asNumber?: boolean;
dir?: 'ltr' | 'rtl';
}>(),
{
required: false,
disabled: false,
readonly: false,
decimals: 2,
asNumber: false,
dir: 'ltr',
},
);
const text = ref('');
const focused = ref(false);
const inputRef = ref<HTMLInputElement | null>(null);
function readModelNumber(): number | null {
if (model.value === '' || model.value == null) return null;
const n = typeof model.value === 'number' ? model.value : Number(model.value);
return Number.isFinite(n) ? n : null;
}
function writeModel(n: number | null) {
if (props.asNumber || typeof model.value === 'number') {
model.value = n == null ? (props.asNumber ? 0 : null) : n;
return;
}
model.value = n == null ? '' : String(n);
}
function syncDisplayFromModel() {
if (focused.value) return;
const n = readModelNumber();
text.value = n == null ? '' : formatGroupedInput(n, props.decimals);
}
watch(model, syncDisplayFromModel, { immediate: true });
watch(
() => props.decimals,
() => syncDisplayFromModel(),
);
function onFocus() {
focused.value = true;
}
function onBlur() {
focused.value = false;
const n = parseMoneyNumber(text.value, props.decimals);
writeModel(n);
text.value = n == null ? '' : formatGroupedInput(n, props.decimals);
}
async function onInput(event: Event) {
const el = event.target as HTMLInputElement;
const raw = el.value;
const caret = el.selectionStart ?? raw.length;
const digitsBefore = countDigitChars(raw.slice(0, caret));
const endsWithSep = /[.\u066B]$/.test(normalizeSep(raw));
const n = parseMoneyNumber(raw, props.decimals);
if (n == null) {
// اجازهٔ تایپ ناقص مثل "-" یا خالی
const cleaned = raw.replace(/[^\d۰-۹٠-٩.\u066B٬,،\s-]/g, '');
text.value = cleaned;
writeModel(null);
return;
}
let display = formatGroupedInput(n, props.decimals);
if (endsWithSep && props.decimals > 0 && Number.isInteger(n)) {
display += '٫';
}
text.value = display;
writeModel(n);
await nextTick();
const input = inputRef.value;
if (!input) return;
const nextCaret = caretIndexForDigitCount(display, digitsBefore);
input.setSelectionRange(nextCaret, nextCaret);
}
function normalizeSep(raw: string): string {
return raw.replace(/٫/g, '.');
}
</script>
<template>
<input
:id="id"
ref="inputRef"
class="money-input"
type="text"
inputmode="decimal"
autocomplete="off"
:dir="dir"
:placeholder="placeholder"
:required="required"
:disabled="disabled"
:readonly="readonly"
:value="text"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
/>
</template>
<style scoped>
.money-input {
font-variant-numeric: tabular-nums;
}
</style>