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

141 lines
3.5 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { nextTick, ref, watch } from 'vue';
import { Input as AInput } from 'ant-design-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';
addonAfter?: string;
}>(),
{
required: false,
disabled: false,
readonly: false,
decimals: 2,
asNumber: false,
dir: 'ltr',
addonAfter: '',
},
);
const text = ref('');
const focused = ref(false);
const inputRef = ref<{ input?: HTMLInputElement } | HTMLInputElement | null>(null);
function nativeInput(): HTMLInputElement | null {
const el = inputRef.value;
if (!el) return null;
if (el instanceof HTMLInputElement) return el;
return el.input ?? 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 = nativeInput();
if (!input) return;
const nextCaret = caretIndexForDigitCount(display, digitsBefore);
input.setSelectionRange(nextCaret, nextCaret);
}
function normalizeSep(raw: string): string {
return raw.replace(/٫/g, '.');
}
</script>
<template>
<a-input
:id="id"
ref="inputRef"
type="text"
inputmode="decimal"
autocomplete="off"
:dir="dir"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:value="text"
:addon-after="addonAfter || undefined"
style="width: 100%; font-variant-numeric: tabular-nums"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
/>
</template>