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>
|
<label for="home-price">Home Price</label>
|
||||||
<div class="input-wrapper input-currency">
|
<div class="input-wrapper input-currency">
|
||||||
<span class="input-prefix">$</span>
|
<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>
|
||||||
</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-row">
|
||||||
<div class="input-wrapper input-currency">
|
<div class="input-wrapper input-currency">
|
||||||
<span class="input-prefix">$</span>
|
<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>
|
||||||
<div class="input-wrapper input-percent">
|
<div class="input-wrapper input-percent">
|
||||||
<input type="text" id="down-payment-percent" name="down-payment-percent" value="20" inputmode="decimal">
|
<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;
|
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 = {
|
var MortgageCalculator = {
|
||||||
init: function() {
|
init: function() {
|
||||||
this.$form = $('#mortgage-calculator-form');
|
this.$form = $('#mortgage-calculator-form');
|
||||||
@@ -22,6 +194,12 @@
|
|||||||
this.$loanAmount = $('#loan-amount');
|
this.$loanAmount = $('#loan-amount');
|
||||||
this.$totalInterest = $('#total-interest');
|
this.$totalInterest = $('#total-interest');
|
||||||
|
|
||||||
|
// Initialize input plugins
|
||||||
|
this.$homePrice.currencyInput(false);
|
||||||
|
this.$downPayment.currencyInput(false);
|
||||||
|
this.$downPaymentPercent.percentInput();
|
||||||
|
this.$interestRate.percentInput();
|
||||||
|
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.calculate();
|
this.calculate();
|
||||||
},
|
},
|
||||||
@@ -29,88 +207,69 @@
|
|||||||
bindEvents: function() {
|
bindEvents: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// Format inputs on blur
|
// Sync down payment when home price changes
|
||||||
this.$homePrice.on('blur', function() {
|
this.$homePrice.on('input', function() {
|
||||||
self.formatCurrency($(this));
|
|
||||||
self.syncDownPaymentFromPercent();
|
self.syncDownPaymentFromPercent();
|
||||||
self.calculate();
|
self.calculate();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$downPayment.on('blur', function() {
|
// Sync percentage when down payment dollar amount changes
|
||||||
self.formatCurrency($(this));
|
this.$downPayment.on('input', function() {
|
||||||
self.syncDownPaymentPercent();
|
self.syncDownPaymentPercent();
|
||||||
self.calculate();
|
self.calculate();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$downPaymentPercent.on('blur', function() {
|
// Sync dollar amount when percentage changes
|
||||||
|
this.$downPaymentPercent.on('input', function() {
|
||||||
self.syncDownPaymentFromPercent();
|
self.syncDownPaymentFromPercent();
|
||||||
self.calculate();
|
self.calculate();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$interestRate.on('blur', function() {
|
// Calculate on interest rate change
|
||||||
|
this.$interestRate.on('input', function() {
|
||||||
self.calculate();
|
self.calculate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Calculate on loan term change
|
||||||
this.$loanTerm.on('change', function() {
|
this.$loanTerm.on('change', function() {
|
||||||
self.calculate();
|
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
|
// Prevent form submission
|
||||||
this.$form.on('submit', function(e) {
|
this.$form.on('submit', function(e) {
|
||||||
e.preventDefault();
|
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) {
|
formatCurrencyDisplay: function(value) {
|
||||||
return '$' + value.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
return '$' + Math.round(value).toLocaleString('en-US');
|
||||||
},
|
},
|
||||||
|
|
||||||
syncDownPaymentPercent: function() {
|
syncDownPaymentPercent: function() {
|
||||||
var homePrice = this.parseNumber(this.$homePrice.val());
|
var homePrice = this.$homePrice.val();
|
||||||
var downPayment = this.parseNumber(this.$downPayment.val());
|
var downPayment = this.$downPayment.val();
|
||||||
|
|
||||||
if (homePrice > 0) {
|
if (homePrice && homePrice > 0) {
|
||||||
var percent = (downPayment / homePrice) * 100;
|
var percent = (downPayment / homePrice) * 100;
|
||||||
this.$downPaymentPercent.val(percent.toFixed(1));
|
this.$downPaymentPercent._CIOriginalVal(percent.toFixed(1));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
syncDownPaymentFromPercent: function() {
|
syncDownPaymentFromPercent: function() {
|
||||||
var homePrice = this.parseNumber(this.$homePrice.val());
|
var homePrice = this.$homePrice.val();
|
||||||
var percent = this.parseNumber(this.$downPaymentPercent.val());
|
var percent = this.$downPaymentPercent.val();
|
||||||
|
|
||||||
if (homePrice > 0 && percent >= 0) {
|
if (homePrice && homePrice > 0 && percent !== '' && percent >= 0) {
|
||||||
var downPayment = (homePrice * percent) / 100;
|
var downPayment = Math.round((homePrice * percent) / 100);
|
||||||
this.$downPayment.val(downPayment.toLocaleString('en-US', { maximumFractionDigits: 0 }));
|
this.$downPayment.val(downPayment);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
calculate: function() {
|
calculate: function() {
|
||||||
var homePrice = this.parseNumber(this.$homePrice.val());
|
var homePrice = this.$homePrice.val() || 0;
|
||||||
var downPayment = this.parseNumber(this.$downPayment.val());
|
var downPayment = this.$downPayment.val() || 0;
|
||||||
var loanTerm = parseInt(this.$loanTerm.val(), 10);
|
var loanTerm = parseInt(this.$loanTerm.val(), 10);
|
||||||
var annualRate = this.parseNumber(this.$interestRate.val());
|
var annualRate = this.$interestRate.val() || 0;
|
||||||
|
|
||||||
// Calculate loan amount
|
// Calculate loan amount
|
||||||
var loanAmount = homePrice - downPayment;
|
var loanAmount = homePrice - downPayment;
|
||||||
@@ -134,10 +293,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update display
|
// Update display
|
||||||
this.$monthlyPayment.text(this.formatCurrencyDisplay(Math.round(monthlyPayment)));
|
this.$monthlyPayment.text(this.formatCurrencyDisplay(monthlyPayment));
|
||||||
this.$principalInterest.text(this.formatCurrencyDisplay(Math.round(monthlyPayment)));
|
this.$principalInterest.text(this.formatCurrencyDisplay(monthlyPayment));
|
||||||
this.$loanAmount.text(this.formatCurrencyDisplay(Math.round(loanAmount)));
|
this.$loanAmount.text(this.formatCurrencyDisplay(loanAmount));
|
||||||
this.$totalInterest.text(this.formatCurrencyDisplay(Math.round(totalInterest)));
|
this.$totalInterest.text(this.formatCurrencyDisplay(totalInterest));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,10 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-value {
|
.result-value {
|
||||||
@@ -173,6 +177,10 @@
|
|||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
font-size: 3.125rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user