Skip to content

Commit

Permalink
feat: Add number separator on amount entry form
Browse files Browse the repository at this point in the history
  • Loading branch information
nafiesl committed Jan 5, 2025
1 parent feed0dd commit c736e61
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 28 deletions.
152 changes: 152 additions & 0 deletions public/js/plugins/number-format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
(function(global) {
// Utility to escape special characters for RegExp
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// Utility function to format number with custom separators
function formatNumber(input, options = {}) {
const {
decimalSeparator = '.',
thousandSeparator = ','
} = options;

// If input is empty or just a decimal separator, return as is
if (!input || input === decimalSeparator) return input;

// Determine the "other" decimal separator to remove
const otherSeparator = decimalSeparator === '.' ? ',' : '.';

// Replace the other separator with the preferred decimal separator if present
input = input.replace(new RegExp(escapeRegExp(otherSeparator), 'g'), decimalSeparator);

// Split the input into integer and decimal parts
const [integerPart, decimalPart] = input.split(decimalSeparator);

// Remove non-digit characters from integer part
const cleanedIntegerPart = integerPart.replace(/\D/g, '');

// Format the integer part with thousand separators
const formattedInteger = cleanedIntegerPart
.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator)
.trim();

// If there's a decimal part, add it back with the custom separator
return decimalPart !== undefined
? `${formattedInteger}${decimalSeparator}${decimalPart}`
: formattedInteger;
}

// Main function to initialize number formatting on an input
function initNumberFormatter(selector, options = {}) {
const {
decimalSeparator = '.',
thousandSeparator = ',',
preventArrowKeys = true
} = options;

// Support both jQuery and vanilla JavaScript selectors
const inputs = global.jQuery
? global.jQuery(selector)
: document.querySelectorAll(selector);

// Convert to array for consistent handling
const inputElements = global.jQuery
? inputs.get()
: Array.from(inputs);

inputElements.forEach(input => {
// Set attributes for mobile numeric keyboard
input.setAttribute('inputmode', 'decimal'); // For decimal numbers

// Create a pattern that allows for the separators
const escapedDecimalSep = escapeRegExp(decimalSeparator);
const escapedThousandSep = escapeRegExp(thousandSeparator);
const pattern = `[0-9${escapedThousandSep}]*${escapedDecimalSep}?[0-9]*`;
input.setAttribute('pattern', pattern);

// Override separators from data attributes if they exist
const selectorDecimalSep = input.dataset.decimalSeparator;
const selectorThousandSep = input.dataset.thousandSeparator;

const formatterOptions = {
decimalSeparator: selectorDecimalSep || decimalSeparator,
thousandSeparator: selectorThousandSep || thousandSeparator,
};

// Update pattern if separators were provided via data attributes
if (selectorDecimalSep || selectorThousandSep) {
const newEscapedDecimalSep = escapeRegExp(formatterOptions.decimalSeparator);
const newEscapedThousandSep = escapeRegExp(formatterOptions.thousandSeparator);
const newPattern = `[0-9${newEscapedThousandSep}]*${newEscapedDecimalSep}?[0-9]*`;
input.setAttribute('pattern', newPattern);
}

// Prevent cursor movement
input.addEventListener('click', (e) => {
e.target.setSelectionRange(e.target.value.length, e.target.value.length);
});

// Prevent arrow keys if option is enabled
if (preventArrowKeys) {
input.addEventListener('keydown', (e) => {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
e.preventDefault();
}
});
}

// Input formatting
input.addEventListener('input', (e) => {
// Get the raw input value (remove existing spaces and thousand separators)
const rawValue = e.target.value
.replace(new RegExp(escapeRegExp(formatterOptions.thousandSeparator), 'g'), '');

// Format the raw value with the custom separators
const formattedValue = formatNumber(rawValue, formatterOptions);

// Update input value
e.target.value = formattedValue;

// Always move cursor to the end
e.target.setSelectionRange(e.target.value.length, e.target.value.length);
});

// Add form submit handler to clean up the value if needed
if (input.form) {
input.form.addEventListener('submit', (e) => {
// You might want to add a hidden input with the clean numeric value
const cleanValue = input.value
.replace(new RegExp(escapeRegExp(formatterOptions.thousandSeparator), 'g'), '')
.replace(formatterOptions.decimalSeparator, '.');

// Create or update hidden input
let hiddenInput = input.form.querySelector(`#${input.id}_clean`);
if (!hiddenInput) {
hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.id = `${input.id}_clean`;
hiddenInput.name = input.name;
input.removeAttribute('name'); // Remove name from formatted input
input.form.appendChild(hiddenInput);
}
hiddenInput.value = cleanValue;
});
}
});

// Return the inputs for chaining
return inputs;
}

// Expose functions globally
global.formatNumber = formatNumber;
global.initNumberFormatter = initNumberFormatter;

// Auto-initialize if no jQuery (vanilla JS approach)
if (!global.jQuery) {
document.addEventListener('DOMContentLoaded', () => {
initNumberFormatter('[data-number-format]');
});
}
})(typeof window !== 'undefined' ? window : global);
20 changes: 10 additions & 10 deletions resources/views/categories/partials/transaction-forms.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
</div>
{!! FormField::textarea('description', ['required' => true, 'label' => __('transaction.description')]) !!}
<div class="row">
<div class="col-md-4">
<div class="form-group required {{ $errors->has('amount') ? 'has-error' : '' }}">
<label for="amount" class="control-label">{{ __('transaction.amount') }}</label>
<div class="input-group"><span class="input-group-addon">{{ auth()->user()->currency_code }}</span>
<input class="form-control text-right" name="amount" type="number" id="amount" min="0" value="{{ isset($editableTransaction) ? round($editableTransaction->amount, 0) : old('amount') }}" required>
</div>
{!! $errors->first('amount', '<span class="help-block small">:message</span>') !!}
</div>
<div class="col-md-6">
{!! FormField::text('amount', [
'required' => true,
'value' => old('amount', format_number($editableTransaction->amount)),
'label' => __('transaction.amount'),
'addon' => ['before' => auth()->user()->currency_code],
'step' => '0.01',
]) !!}
</div>
<div class="col-md-4">{!! FormField::radios('in_out', [__('transaction.spending'), __('transaction.income')], ['required' => true, 'label' => __('transaction.transaction')]) !!}</div>
<div class="col-md-4">{!! FormField::select('partner_id', $partners, ['label' => __('partner.partner'), 'placeholder' => __('partner.empty')]) !!}</div>
<div class="col-md-6">{!! FormField::radios('in_out', [__('transaction.spending'), __('transaction.income')], ['required' => true, 'label' => __('transaction.transaction')]) !!}</div>
<div class="col-md-6">{!! FormField::select('partner_id', $partners, ['label' => __('partner.partner'), 'placeholder' => __('partner.empty')]) !!}</div>
</div>
</div>
<div class="modal-footer">
Expand Down
2 changes: 2 additions & 0 deletions resources/views/categories/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
@push('scripts')
{{ Html::script(url('js/plugins/bootstrap-colorpicker.min.js')) }}
{{ Html::script(url('js/plugins/jquery.datetimepicker.js')) }}
{{ Html::script(url('js/plugins/number-format.js')) }}
<script>
(function () {
$('#transactionModal').modal({
Expand All @@ -111,6 +112,7 @@
scrollInput: false,
dayOfWeekStart: 1
});
initNumberFormatter('#amount');
})();
</script>
@endpush
9 changes: 8 additions & 1 deletion resources/views/loans/create.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
{!! FormField::radios('type_id', $loanTypes, ['required' => true, 'label' => __('loan.type')]) !!}
<div class="row">
<div class="col-md-6">
{!! FormField::text('amount', ['required' => true, 'type' => 'number', 'label' => __('loan.amount')]) !!}
{!! FormField::text('amount', [
'required' => true,
'label' => __('loan.amount'),
'addon' => ['before' => auth()->user()->currency_code],
'step' => '0.01',
]) !!}
</div>
<div class="col-md-6">
{!! FormField::text('planned_payment_count', ['required' => true, 'type' => 'number', 'value' => old('planned_payment_count', 1), 'label' => __('loan.planned_payment_count')]) !!}
Expand Down Expand Up @@ -45,6 +50,7 @@

@push('scripts')
{{ Html::script(url('js/plugins/jquery.datetimepicker.js')) }}
{{ Html::script(url('js/plugins/number-format.js')) }}
<script>
(function () {
$('.date-select').datetimepicker({
Expand All @@ -54,6 +60,7 @@
scrollInput: false,
dayOfWeekStart: 1
});
initNumberFormatter('#amount');
})();
</script>
@endpush
10 changes: 9 additions & 1 deletion resources/views/loans/edit.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@
{!! FormField::radios('type_id', $loanTypes, ['required' => true, 'label' => __('loan.type')]) !!}
<div class="row">
<div class="col-md-6">
{!! FormField::text('amount', ['required' => true, 'type' => 'number', 'label' => __('loan.amount')]) !!}
{!! FormField::text('amount', [
'required' => true,
'value' => old('amount', format_number($loan->amount)),
'label' => __('loan.amount'),
'addon' => ['before' => auth()->user()->currency_code],
'step' => '0.01',
]) !!}
</div>
<div class="col-md-6">
{!! FormField::text('planned_payment_count', ['required' => true, 'type' => 'number', 'value' => old('planned_payment_count', $loan->planned_payment_count), 'label' => __('loan.planned_payment_count')]) !!}
Expand Down Expand Up @@ -82,6 +88,7 @@

@push('scripts')
{{ Html::script(url('js/plugins/jquery.datetimepicker.js')) }}
{{ Html::script(url('js/plugins/number-format.js')) }}
<script>
(function () {
$('.date-select').datetimepicker({
Expand All @@ -91,6 +98,7 @@
scrollInput: false,
dayOfWeekStart: 1
});
initNumberFormatter('#amount');
})();
</script>
@endpush
7 changes: 6 additions & 1 deletion resources/views/loans/partials/single_actions.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
{!! FormField::textarea('description', ['required' => true, 'label' => __('app.description')]) !!}
<div class="row">
<div class="col-md-6">
{!! FormField::text('amount', ['required' => true, 'type' => 'number', 'min' => '0', 'label' => __('transaction.amount'), 'class' => 'text-right']) !!}
{!! FormField::text('amount', [
'required' => true,
'label' => __('transaction.amount'),
'addon' => ['before' => auth()->user()->currency_code],
'step' => '0.01',
]) !!}
</div>
<div class="col-md-6">
{!! FormField::textDisplay('partner', $loan->partner->name) !!}
Expand Down
2 changes: 2 additions & 0 deletions resources/views/loans/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@

@push('scripts')
{{ Html::script(url('js/plugins/jquery.datetimepicker.js')) }}
{{ Html::script(url('js/plugins/number-format.js')) }}
<script>
(function () {
$('#transactionModal').modal({
Expand All @@ -109,6 +110,7 @@
scrollInput: false,
dayOfWeekStart: 1
});
initNumberFormatter('#amount');
})();
</script>
@endpush
20 changes: 10 additions & 10 deletions resources/views/partners/partials/transaction-forms.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
</div>
{!! FormField::textarea('description', ['required' => true, 'label' => __('transaction.description')]) !!}
<div class="row">
<div class="col-md-4">
<div class="form-group required {{ $errors->has('amount') ? 'has-error' : '' }}">
<label for="amount" class="control-label">{{ __('transaction.amount') }}</label>
<div class="input-group"><span class="input-group-addon">{{ auth()->user()->currency_code }}</span>
<input class="form-control text-right" name="amount" type="number" id="amount" min="0" value="{{ isset($editableTransaction) ? round($editableTransaction->amount, 0) : old('amount') }}" required>
</div>
{!! $errors->first('amount', '<span class="help-block small">:message</span>') !!}
</div>
<div class="col-md-6">
{!! FormField::text('amount', [
'required' => true,
'value' => old('amount', format_number($editableTransaction->amount)),
'label' => __('transaction.amount'),
'addon' => ['before' => auth()->user()->currency_code],
'step' => '0.01',
]) !!}
</div>
<div class="col-md-4">{!! FormField::radios('in_out', [__('transaction.spending'), __('transaction.income')], ['required' => true, 'label' => __('transaction.transaction')]) !!}</div>
<div class="col-md-4">{!! FormField::select('partner_id', $partners, ['label' => __('partner.partner'), 'placeholder' => __('partner.empty')]) !!}</div>
<div class="col-md-6">{!! FormField::radios('in_out', [__('transaction.spending'), __('transaction.income')], ['required' => true, 'label' => __('transaction.transaction')]) !!}</div>
<div class="col-md-6">{!! FormField::select('partner_id', $partners, ['label' => __('partner.partner'), 'placeholder' => __('partner.empty')]) !!}</div>
</div>
</div>
<div class="modal-footer">
Expand Down
2 changes: 2 additions & 0 deletions resources/views/partners/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
@push('scripts')
{{ Html::script(url('js/plugins/bootstrap-colorpicker.min.js')) }}
{{ Html::script(url('js/plugins/jquery.datetimepicker.js')) }}
{{ Html::script(url('js/plugins/number-format.js')) }}
<script>
(function () {
$('#transactionModal').modal({
Expand All @@ -111,6 +112,7 @@
scrollInput: false,
dayOfWeekStart: 1
});
initNumberFormatter('#amount');
})();
</script>
@endpush
20 changes: 18 additions & 2 deletions resources/views/transactions/create.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@
</div>
{!! FormField::textarea('description', ['required' => true, 'label' => __('transaction.description')]) !!}
<div class="row">
<div class="col-md-6">{!! FormField::price('amount', ['required' => true, 'label' => __('transaction.amount'), 'type' => 'number', 'currency' => config('money.currency_code'), 'step' => '0.01']) !!}</div>
<div class="col-md-6">
{!! FormField::text('amount', [
'required' => true,
'label' => __('transaction.amount'),
'addon' => ['before' => auth()->user()->currency_code],
'step' => '0.01',
]) !!}
</div>
<div class="col-md-6">{!! FormField::select('partner_id', $partners, ['label' => __('partner.partner'), 'placeholder' => __('partner.no_partner')]) !!}</div>
</div>
</div>
Expand Down Expand Up @@ -53,7 +60,14 @@
</div>
{!! FormField::textarea('description', ['required' => true, 'label' => __('transaction.description')]) !!}
<div class="row">
<div class="col-md-6">{!! FormField::price('amount', ['required' => true, 'label' => __('transaction.amount'), 'type' => 'number', 'currency' => config('money.currency_code'), 'step' => '0.01']) !!}</div>
<div class="col-md-6">
{!! FormField::text('amount', [
'required' => true,
'label' => __('transaction.amount'),
'addon' => ['before' => auth()->user()->currency_code],
'step' => '0.01',
]) !!}
</div>
<div class="col-md-6">{!! FormField::select('partner_id', $partners, ['label' => __('partner.partner'), 'placeholder' => __('partner.no_partner')]) !!}</div>
</div>
</div>
Expand All @@ -75,6 +89,7 @@

@push('scripts')
{{ Html::script(url('js/plugins/jquery.datetimepicker.js')) }}
{{ Html::script(url('js/plugins/number-format.js')) }}
<script>
(function () {
$('.date-select').datetimepicker({
Expand All @@ -84,6 +99,7 @@
scrollInput: false,
dayOfWeekStart: 1
});
initNumberFormatter('#amount');
})();
</script>
@endpush
Loading

0 comments on commit c736e61

Please sign in to comment.