-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.js
247 lines (207 loc) · 7.7 KB
/
content.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
"use strict";
const storageKeys = ['downpayment', 'interest', 'taxRate', 'estimatedUtilities', 'period', 'currentMonthly', 'useAssessedValue'];
let cutsheet = document.documentElement.querySelector('.overview,#v3-cutsheet');
const observer = new MutationObserver((mutations, observer) => updateCosts(mutations));
if (cutsheet) {
if (cutsheet.id === 'v3-cutsheet') {
updateCosts();
} else {
// Observe changes to the cutsheet's class to see loading new listing
observer.observe(cutsheet, {
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true
});
}
// Listen for options updates
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && Object.keys(changes).some(key => storageKeys.includes(key))) {
updateCosts();
}
});
} else {
console.warn('Not cutsheet (div.overview,#v3-cutsheet) is present');
}
/**
* Updates the element containing the calculated costs
* @param {MutationRecord[]} mutations
*/
async function updateCosts(mutations) {
if (mutations) {
const wasLoading = mutations[0].oldValue.split(/\s+/).includes('show-vp-loader');
const isLoading = mutations[0].target.classList.contains('show-vp-loader');
if (!(wasLoading && !isLoading)) {
return;
}
}
const cutsheetMainListingInfoElement = cutsheet.querySelector('.cutsheet-main-listing-info');
const priceElement = cutsheet.querySelector('span.overlay-price');
let monthlyElement = cutsheet.querySelector('div.monthly-calculation');
if (!monthlyElement) {
monthlyElement = insertMonthlyElementIntoCutsheet(cutsheetMainListingInfoElement);
}
const monthlyTotal = monthlyElement.querySelector('div.monthly-total');
const monthlyMortgage = monthlyElement.querySelector('div.monthly-mortgage');
const monthlyTax = monthlyElement.querySelector('div.monthly-tax');
const monthlyUtils = monthlyElement.querySelector('div.monthly-utils');
const monthlyDiff = monthlyElement.querySelector('div.monthly-diff');
if (priceElement && priceElement.innerText !== undefined && monthlyElement) {
const priceText = priceElement.innerText.split('\n')[0];
const price = parsePrice(priceText);
if (price) {
const assessedValue = findAssessedValue(cutsheet, price);
const calculatedCost = await calculateMonthlyCost(price, assessedValue);
monthlyTotal.innerText = formatPrice(calculatedCost.total);
monthlyMortgage.innerText = formatPrice(calculatedCost.mortgage);
monthlyTax.innerText = formatPrice(calculatedCost.taxes);
monthlyUtils.innerText = formatPrice(calculatedCost.utils);
monthlyDiff.innerText = formatPrice(calculatedCost.diffFromNow);
}
}
}
/**
* Calculates the total monthly costs, mortgage (monthly) and taxes (monthly),
*
* @param {number} price
* @param {number} assessedValue
*/
async function calculateMonthlyCost(price, assessedValue) {
return new Promise((resolve, reject) => {
chrome.storage.sync.get(storageKeys, (data) => {
const { downpayment, interest, taxRate, estimatedUtilities, period, useAssessedValue, currentMonthly } = data;
// Homes over $1 million are subject to 20% downpayment
if (price > 1000000 && downpayment < 20) {
downpayment = 20;
}
let principal;
if (price > 500000 && price <= 1000000 && downpayment <= 10) {
let defaultInsurance = 0;
principal = 500000 - (500000 * 0.05);
defaultInsurance += calculateDefaultInsurance(principal, 5);
principal += (price - 500000 * 0.01);
defaultInsurance += calculateDefaultInsurance(price - 500000 * 0.01, 10);
} else {
principal = price - (price * (downpayment / 100));
principal = principal + calculateDefaultInsurance(principal, downpayment);
}
const years = period;
const mortgagePayment = calculateMortgagePayment(principal, years, interest);
const taxes = (taxRate / 100 / 12) * ( useAssessedValue ? assessedValue : price);
const utilities = estimatedUtilities;
const total = mortgagePayment + taxes + utilities;
return resolve({
total: total,
mortgage: mortgagePayment,
taxes: taxes,
utils: estimatedUtilities,
diffFromNow: total - currentMonthly
});
});
})
}
/**
* Inserts the monthly cost div after the element passed
*
* @param {Element} insertAfterElement
*/
function insertMonthlyElementIntoCutsheet(insertAfterElement) {
const element = document.createElement('div');
element.classList.add('monthly-calculation');
insertAfterElement.parentNode.insertBefore(element, insertAfterElement.nextSibling);
element.style.backgroundColor = '#8000d8';
element.style.color = '#fff';
element.style.marginBottom = '5px';
element.style.padding = '5px 10px';
element.style.fontWeight = '300';
element.style.flex = '0 0 66.6%';
insertRow(element, 'monthly-total', 'TOTAL:', '$0');
insertRow(element, 'monthly-mortgage', 'Mortage:', '$0');
insertRow(element, 'monthly-tax', 'Tax:', '$0');
insertRow(element, 'monthly-utils', 'Utilities:', '$0');
insertRow(element, 'monthly-diff', 'Diff:', '$0');
return element;
}
function insertRow(element, className, leftText, rightText) {
const row = document.createElement('div');
row.style.display = 'flex';
row.style.flexDirection = 'row';
row.style.justifyContent = 'space-between';
const left = document.createElement('div');
const right = document.createElement('div');
left.innerText = leftText;
right.innerText = rightText;
right.classList.add(className);
row.appendChild(left);
row.appendChild(right);
element.appendChild(row);
return row;
}
/**
* Finds the element with the tax assessed value in the parentElement, and then
* returns a parsed version of the value, or the default value if it can't find it.
*
* @param {Element} parentElement
* @param {number} defaultValue
* @returns {number}
*/
function findAssessedValue(parentElement, defaultValue) {
const assessmentElement = parentElement.querySelector('span.assessment');
if (assessmentElement) {
const assessedValueElement = assessmentElement.nextElementSibling.querySelector('span.separated');
if (assessedValueElement) {
return parseAssessedValue(assessedValueElement.innerText);
}
}
return defaultValue;
}
/**
* Calculate monthly mortgage payment.
*
* TODO: Add default insurance
*
* @param {number} principal
* @param {number} years
* @param {number} interestRate
*/
function calculateMortgagePayment(principal, years, interestRate) {
let months = years * 12;
let monthlyInterestRate = interestRate / 100 / 12;
return principal * monthlyInterestRate * (Math.pow(1 + monthlyInterestRate, months)) / (Math.pow(1 + monthlyInterestRate, months) - 1);
}
function formatPrice(price) {
const numbers = new String(parseInt(price)).split('');
const charArray = []
for (let i = 0; i < numbers.length; i++) {
if (i % 3 === 0 && i > 0) {
charArray.unshift(',');
}
charArray.unshift(numbers[numbers.length - (i + 1)]);
}
return '$' + charArray.join('');
}
/**
* Parses an integer from a price string with no decimals
*
* @param {string} string
* @returns {number|null}
*/
function parsePrice(string) {
let price = parseInt(string.replace(/[^0-9]/g, ''));
if (isNaN(price)) {
return null;
}
return price;
}
function parseAssessedValue(string) {
return parsePrice(string.replace(/[0-9]{4} Assessment:/, ''));
}
function calculateDefaultInsurance(price, downpayment) {
if (downpayment >= 5 && downpayment < 10) {
return price * 0.04;
} else if (downpayment >= 10 && downpayment < 15) {
return price * 0.031;
} else if (downpayment >= 15 && downpayment < 20) {
return price * 0.028;
}
return 0;
}