Add currency and percentage input plugins to mortgage calculator

- Currency inputs auto-format with thousand separators on input
- Only allows numeric characters (strips letters, symbols, etc.)
- .val() returns raw integer for calculations
- Percentage inputs allow only numbers and one decimal point
- Select all on focus for easy replacement
- Maintains cursor position while typing
- Real-time sync between down payment dollar and percent fields

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-01 00:37:22 -06:00
parent ebbc9ec03b
commit cf0debb970
6 changed files with 217 additions and 49 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -47,7 +47,7 @@ $bg_style = $hero_bg ? 'style="background-image: url(' . esc_url($hero_bg) . ');
<label for="home-price">Home Price</label>
<div class="input-wrapper input-currency">
<span class="input-prefix">$</span>
<input type="text" id="home-price" name="home-price" value="250,000" inputmode="numeric">
<input type="text" id="home-price" name="home-price" value="250000" inputmode="numeric">
</div>
</div>
@@ -56,7 +56,7 @@ $bg_style = $hero_bg ? 'style="background-image: url(' . esc_url($hero_bg) . ');
<div class="input-row">
<div class="input-wrapper input-currency">
<span class="input-prefix">$</span>
<input type="text" id="down-payment" name="down-payment" value="50,000" inputmode="numeric">
<input type="text" id="down-payment" name="down-payment" value="50000" inputmode="numeric">
</div>
<div class="input-wrapper input-percent">
<input type="text" id="down-payment-percent" name="down-payment-percent" value="20" inputmode="decimal">
@@ -9,6 +9,178 @@
if (!$('.mortgage-calculator-main').length) return;
// ============================================
// Currency Input Plugin
// ============================================
let _hasInitCurrencyInput = false;
$.fn.currencyInput = function(show_symbol = true) {
let $that = this;
$that.data('ci_show_symbol', show_symbol);
// Override $.fn.val once globally
if (!_hasInitCurrencyInput) {
_hasInitCurrencyInput = true;
$.fn._CIOriginalVal = $.fn.val;
$.fn.val = function(value) {
if ($(this).data("_currencyInput")) {
if (arguments.length === 0) {
// Getter - return raw integer
var raw = $(this)._CIOriginalVal();
if (raw == '') {
return '';
}
var number = parseInt(raw.replace(/[^0-9]/g, ''));
return number;
} else {
// Setter - format and display
value = String(value).replace(/[^0-9]/g, '');
if (value != '') {
var formatted = parseInt(value).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
});
if (!$(this).data('ci_show_symbol')) {
formatted = formatted.replace("$", "");
}
return $(this)._CIOriginalVal(formatted);
}
return $(this)._CIOriginalVal(value);
}
} else if ($(this).data("_percentInput")) {
if (arguments.length === 0) {
// Getter - return raw float
var raw = $(this)._CIOriginalVal();
if (raw == '') {
return '';
}
var number = parseFloat(raw.replace(/[^0-9.]/g, ''));
return isNaN(number) ? '' : number;
} else {
// Setter - clean and display
value = String(value).replace(/[^0-9.]/g, '');
// Ensure only one decimal point
var parts = value.split('.');
if (parts.length > 2) {
value = parts[0] + '.' + parts.slice(1).join('');
}
return $(this)._CIOriginalVal(value);
}
} else {
// Default behavior for other inputs
if (arguments.length === 0) {
return $(this)._CIOriginalVal();
} else {
return $(this)._CIOriginalVal(value);
}
}
};
}
// Skip if already initialized
if (this.data("_currencyInput")) {
return this;
}
this.data("_currencyInput", true);
// Select all on focus
this.on('focus', function() {
$(this).select();
});
// Format on input, maintain cursor position
this.on('input', function(event) {
var cursorPos = this.selectionStart;
var val = $(this)._CIOriginalVal();
var lengthBefore = val.length;
// Setting with .val() applies formatting
$(this).val(val);
var lengthAfter = $(this)._CIOriginalVal().length;
// Adjust cursor position based on length change
if (lengthAfter > lengthBefore) {
cursorPos += (lengthAfter - lengthBefore);
} else if (lengthAfter < lengthBefore) {
cursorPos -= (lengthBefore - lengthAfter);
}
this.setSelectionRange(cursorPos, cursorPos);
// Clear input when backspace leaves only one digit
if (this.value.replace(/[^0-9]/g, '').length === 1 && event.originalEvent && event.originalEvent.inputType === "deleteContentBackward") {
$(this)._CIOriginalVal('');
}
});
// Initialize display from current value
$(this).val($(this)._CIOriginalVal());
return this;
};
// ============================================
// Percentage Input Plugin
// ============================================
$.fn.percentInput = function() {
// Skip if already initialized
if (this.data("_percentInput")) {
return this;
}
this.data("_percentInput", true);
// Select all on focus
this.on('focus', function() {
$(this).select();
});
// Clean on input - only allow numbers and one decimal point
this.on('input', function(event) {
var cursorPos = this.selectionStart;
var val = $(this)._CIOriginalVal();
var lengthBefore = val.length;
// Remove any character that isn't a number or period
var cleaned = val.replace(/[^0-9.]/g, '');
// Ensure only one decimal point
var parts = cleaned.split('.');
if (parts.length > 2) {
cleaned = parts[0] + '.' + parts.slice(1).join('');
}
$(this)._CIOriginalVal(cleaned);
var lengthAfter = cleaned.length;
// Adjust cursor position based on length change
if (lengthAfter < lengthBefore) {
cursorPos -= (lengthBefore - lengthAfter);
}
if (cursorPos < 0) cursorPos = 0;
this.setSelectionRange(cursorPos, cursorPos);
});
return this;
};
// ============================================
// Mortgage Calculator
// ============================================
var MortgageCalculator = {
init: function() {
this.$form = $('#mortgage-calculator-form');
@@ -22,6 +194,12 @@
this.$loanAmount = $('#loan-amount');
this.$totalInterest = $('#total-interest');
// Initialize input plugins
this.$homePrice.currencyInput(false);
this.$downPayment.currencyInput(false);
this.$downPaymentPercent.percentInput();
this.$interestRate.percentInput();
this.bindEvents();
this.calculate();
},
@@ -29,88 +207,69 @@
bindEvents: function() {
var self = this;
// Format inputs on blur
this.$homePrice.on('blur', function() {
self.formatCurrency($(this));
// Sync down payment when home price changes
this.$homePrice.on('input', function() {
self.syncDownPaymentFromPercent();
self.calculate();
});
this.$downPayment.on('blur', function() {
self.formatCurrency($(this));
// Sync percentage when down payment dollar amount changes
this.$downPayment.on('input', function() {
self.syncDownPaymentPercent();
self.calculate();
});
this.$downPaymentPercent.on('blur', function() {
// Sync dollar amount when percentage changes
this.$downPaymentPercent.on('input', function() {
self.syncDownPaymentFromPercent();
self.calculate();
});
this.$interestRate.on('blur', function() {
// Calculate on interest rate change
this.$interestRate.on('input', function() {
self.calculate();
});
// Calculate on loan term change
this.$loanTerm.on('change', function() {
self.calculate();
});
// Calculate on input (debounced)
var debounceTimer;
this.$form.find('input').on('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
self.calculate();
}, 300);
});
// Prevent form submission
this.$form.on('submit', function(e) {
e.preventDefault();
});
},
parseNumber: function(value) {
if (typeof value === 'number') return value;
return parseFloat(value.replace(/[^0-9.-]/g, '')) || 0;
},
formatCurrency: function($input) {
var value = this.parseNumber($input.val());
if (value > 0) {
$input.val(value.toLocaleString('en-US', { maximumFractionDigits: 0 }));
}
},
formatCurrencyDisplay: function(value) {
return '$' + value.toLocaleString('en-US', { maximumFractionDigits: 0 });
return '$' + Math.round(value).toLocaleString('en-US');
},
syncDownPaymentPercent: function() {
var homePrice = this.parseNumber(this.$homePrice.val());
var downPayment = this.parseNumber(this.$downPayment.val());
var homePrice = this.$homePrice.val();
var downPayment = this.$downPayment.val();
if (homePrice > 0) {
if (homePrice && homePrice > 0) {
var percent = (downPayment / homePrice) * 100;
this.$downPaymentPercent.val(percent.toFixed(1));
this.$downPaymentPercent._CIOriginalVal(percent.toFixed(1));
}
},
syncDownPaymentFromPercent: function() {
var homePrice = this.parseNumber(this.$homePrice.val());
var percent = this.parseNumber(this.$downPaymentPercent.val());
var homePrice = this.$homePrice.val();
var percent = this.$downPaymentPercent.val();
if (homePrice > 0 && percent >= 0) {
var downPayment = (homePrice * percent) / 100;
this.$downPayment.val(downPayment.toLocaleString('en-US', { maximumFractionDigits: 0 }));
if (homePrice && homePrice > 0 && percent !== '' && percent >= 0) {
var downPayment = Math.round((homePrice * percent) / 100);
this.$downPayment.val(downPayment);
}
},
calculate: function() {
var homePrice = this.parseNumber(this.$homePrice.val());
var downPayment = this.parseNumber(this.$downPayment.val());
var homePrice = this.$homePrice.val() || 0;
var downPayment = this.$downPayment.val() || 0;
var loanTerm = parseInt(this.$loanTerm.val(), 10);
var annualRate = this.parseNumber(this.$interestRate.val());
var annualRate = this.$interestRate.val() || 0;
// Calculate loan amount
var loanAmount = homePrice - downPayment;
@@ -134,10 +293,10 @@
}
// Update display
this.$monthlyPayment.text(this.formatCurrencyDisplay(Math.round(monthlyPayment)));
this.$principalInterest.text(this.formatCurrencyDisplay(Math.round(monthlyPayment)));
this.$loanAmount.text(this.formatCurrencyDisplay(Math.round(loanAmount)));
this.$totalInterest.text(this.formatCurrencyDisplay(Math.round(totalInterest)));
this.$monthlyPayment.text(this.formatCurrencyDisplay(monthlyPayment));
this.$principalInterest.text(this.formatCurrencyDisplay(monthlyPayment));
this.$loanAmount.text(this.formatCurrencyDisplay(loanAmount));
this.$totalInterest.text(this.formatCurrencyDisplay(totalInterest));
}
};
@@ -161,6 +161,10 @@
font-size: 0.875rem;
color: var(--color-text-muted);
margin-bottom: 0.5rem;
@media (min-width: 1024px) {
font-size: 1.4rem;
}
}
.result-value {
@@ -173,6 +177,10 @@
@media (max-width: 640px) {
font-size: 2rem;
}
@media (min-width: 1024px) {
font-size: 3.125rem;
}
}
}