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:
@@ -0,0 +1 @@
|
||||
55392
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user