first commit + add fe component vue accone

This commit is contained in:
2026-05-07 17:10:10 +07:00
parent fd8e4d694b
commit 38a182641a
327 changed files with 263946 additions and 0 deletions

218
common/MoneyInput.vue Normal file
View File

@@ -0,0 +1,218 @@
<template>
<div class="money-input-container">
<input
ref="input"
:value="displayValue"
@input="handleInput"
@keydown="handleKeydown"
@blur="handleBlur"
v-bind="$attrs"
v-on="inputListeners"
/>
</div>
</template>
<script>
export default {
name: 'MoneyInput',
inheritAttrs: false,
props: {
value: {
type: [String, Number],
default: ''
}
},
data() {
return {
localValue: '',
hasComma: false,
rawDisplayValue: ''
}
},
computed: {
// Format untuk tampilan
displayValue() {
// Jika pengguna baru mengetik, gunakan rawDisplayValue
if (this.rawDisplayValue) {
return this.rawDisplayValue;
}
// Format dari nilai model
return this.formatValue(this.localValue);
},
// Event listeners yang diteruskan
inputListeners() {
const listeners = { ...this.$listeners };
// Hapus event handlers yang dikelola sendiri
delete listeners.input;
delete listeners.keydown;
delete listeners.blur;
return listeners;
}
},
watch: {
// Sinkronkan props dengan data lokal
value: {
immediate: true,
handler(newValue) {
// Hindari loop jika nilai sama
if (newValue !== this.localValue) {
this.localValue = newValue;
this.rawDisplayValue = '';
}
}
}
},
methods: {
// Format nilai untuk tampilan (1234.56 -> 1.234,56)
formatValue(value) {
if (!value && value !== 0) return '';
// Convert ke string
const strValue = value.toString();
// Pisahkan bagian integer dan desimal
const parts = strValue.split('.');
const integerPart = parts[0];
const decimalPart = parts.length > 1 ? parts[1] : '';
// Format bagian integer dengan titik ribuan
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
// Gabungkan dengan format display
return decimalPart ? `${formattedInteger},${decimalPart}` : formattedInteger;
},
// Parse dari format display ke nilai model (1.234,56 -> 1234.56)
parseValue(displayValue) {
if (!displayValue) return '';
// Pisahkan bagian integer dan desimal
const parts = displayValue.split(',');
const integerPart = parts[0].replace(/\./g, ''); // Hapus titik ribuan
const decimalPart = parts.length > 1 ? parts[1] : '';
// Gabungkan dengan format model (titik untuk desimal)
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
},
// Tangani event input
handleInput(event) {
const input = event.target;
const cursorPos = input.selectionStart;
const rawValue = input.value;
// Cek apakah ada koma
this.hasComma = rawValue.includes(',');
// Pisahkan bagian integer dan desimal
let integerPart, decimalPart;
if (this.hasComma) {
const parts = rawValue.split(',');
integerPart = parts[0];
decimalPart = parts.length > 1 ? parts[1] : '';
} else {
integerPart = rawValue;
decimalPart = '';
}
// Format bagian integer (hapus titik lama, tambahkan titik baru)
const cleanInteger = integerPart.replace(/\./g, '');
const formattedInteger = cleanInteger.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
// Buat nilai tampilan baru
this.rawDisplayValue = this.hasComma ? `${formattedInteger},${decimalPart}` : formattedInteger;
// Update model value (tanpa formatter)
const modelValue = this.hasComma ? `${cleanInteger}.${decimalPart}` : cleanInteger;
this.localValue = modelValue;
// Emit event
this.$emit('input', modelValue);
// Set cursor position setelah DOM diupdate
this.$nextTick(() => {
// Posisi kursor relatif
if (this.hasComma) {
const commaPos = rawValue.indexOf(',');
if (cursorPos <= commaPos) {
// Hitung pergeseran karena dot formatting
const oldDotsBeforeCursor = (integerPart.substring(0, cursorPos).match(/\./g) || []).length;
const newDotsBeforeCursor = (formattedInteger.substring(0, cursorPos).match(/\./g) || []).length;
const offset = newDotsBeforeCursor - oldDotsBeforeCursor;
input.setSelectionRange(cursorPos + offset, cursorPos + offset);
} else {
// Setelah koma, sesuaikan dengan posisi koma baru
const newCommaPos = formattedInteger.length;
const offsetAfterComma = cursorPos - commaPos;
input.setSelectionRange(newCommaPos + offsetAfterComma, newCommaPos + offsetAfterComma);
}
} else {
// Format tanpa koma
const oldDots = (rawValue.substring(0, cursorPos).match(/\./g) || []).length;
const newDots = (this.rawDisplayValue.substring(0, cursorPos).match(/\./g) || []).length;
input.setSelectionRange(cursorPos + (newDots - oldDots), cursorPos + (newDots - oldDots));
}
});
},
// Tangani tombol koma dan titik
handleKeydown(event) {
// Khusus untuk tombol koma dan titik
if (event.key === ',' || event.key === '.') {
// Jika sudah ada koma, cegah input
if (this.rawDisplayValue.includes(',')) {
event.preventDefault();
return;
}
event.preventDefault();
// Ambil nilai dan posisi kursor saat ini
const input = event.target;
const cursorPos = input.selectionStart;
const currentValue = this.rawDisplayValue || this.displayValue;
// Sisipkan koma
const beforeComma = currentValue.substring(0, cursorPos);
const afterComma = currentValue.substring(cursorPos);
const newDisplayValue = beforeComma + ',' + afterComma;
// Set nilai display
this.rawDisplayValue = newDisplayValue;
// Update model value
const parts = newDisplayValue.split(',');
const integerPart = parts[0].replace(/\./g, '');
const decimalPart = parts.length > 1 ? parts[1] : '';
const modelValue = integerPart + (decimalPart ? '.' + decimalPart : '');
this.localValue = modelValue;
this.hasComma = true;
// Emit event
this.$emit('input', modelValue);
// Set kursor setelah koma
this.$nextTick(() => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1);
});
}
},
// Format ulang pada blur
handleBlur() {
// Reset raw display value
this.rawDisplayValue = '';
// Emit event
this.$emit('blur');
}
}
};
</script>