mirror of
https://github.com/basicswap/basicswap.git
synced 2026-01-01 10:01:38 +01:00
Refactor + Optimizations
This commit is contained in:
608
basicswap/static/js/pages/offer-new-page.js
Normal file
608
basicswap/static/js/pages/offer-new-page.js
Normal file
@@ -0,0 +1,608 @@
|
||||
const DOM = {
|
||||
get: (id) => document.getElementById(id),
|
||||
getValue: (id) => {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value : '';
|
||||
},
|
||||
setValue: (id, value) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = value;
|
||||
},
|
||||
addEvent: (id, event, handler) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener(event, handler);
|
||||
},
|
||||
query: (selector) => document.querySelector(selector),
|
||||
queryAll: (selector) => document.querySelectorAll(selector)
|
||||
};
|
||||
|
||||
const ErrorModal = {
|
||||
show: function(title, message) {
|
||||
const errorTitle = document.getElementById('errorTitle');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const modal = document.getElementById('errorModal');
|
||||
|
||||
if (errorTitle) errorTitle.textContent = title || 'Error';
|
||||
if (errorMessage) errorMessage.textContent = message || 'An error occurred';
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
},
|
||||
|
||||
init: function() {
|
||||
const errorOkBtn = document.getElementById('errorOk');
|
||||
if (errorOkBtn) {
|
||||
errorOkBtn.addEventListener('click', this.hide.bind(this));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Storage = {
|
||||
get: (key) => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(key));
|
||||
} catch(e) {
|
||||
console.warn(`Failed to retrieve item from storage: ${key}`, e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
set: (key, value) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch(e) {
|
||||
console.error(`Failed to save item to storage: ${key}`, e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
setRaw: (key, value) => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
return true;
|
||||
} catch(e) {
|
||||
console.error(`Failed to save raw item to storage: ${key}`, e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getRaw: (key) => {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch(e) {
|
||||
console.warn(`Failed to retrieve raw item from storage: ${key}`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Ajax = {
|
||||
post: (url, data, onSuccess, onError) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) return;
|
||||
if (xhr.status === 200) {
|
||||
if (onSuccess) {
|
||||
try {
|
||||
const response = xhr.responseText.startsWith('{') ?
|
||||
JSON.parse(xhr.responseText) : xhr.responseText;
|
||||
onSuccess(response);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response:', e);
|
||||
if (onError) onError('Invalid response format');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('Request failed:', xhr.statusText);
|
||||
if (onError) onError(xhr.statusText);
|
||||
}
|
||||
};
|
||||
xhr.open('POST', url);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send(data);
|
||||
return xhr;
|
||||
}
|
||||
};
|
||||
|
||||
function handleNewOfferAddress() {
|
||||
const STORAGE_KEY = 'lastUsedAddressNewOffer';
|
||||
const selectElement = DOM.query('select[name="addr_from"]');
|
||||
const form = selectElement?.closest('form');
|
||||
|
||||
if (!selectElement || !form) return;
|
||||
|
||||
function loadInitialAddress() {
|
||||
const savedAddress = Storage.get(STORAGE_KEY);
|
||||
if (savedAddress) {
|
||||
try {
|
||||
selectElement.value = savedAddress.value;
|
||||
} catch (e) {
|
||||
selectFirstAddress();
|
||||
}
|
||||
} else {
|
||||
selectFirstAddress();
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirstAddress() {
|
||||
if (selectElement.options.length > 1) {
|
||||
const firstOption = selectElement.options[1];
|
||||
if (firstOption) {
|
||||
selectElement.value = firstOption.value;
|
||||
saveAddress(firstOption.value, firstOption.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveAddress(value, text) {
|
||||
Storage.set(STORAGE_KEY, { value, text });
|
||||
}
|
||||
|
||||
form.addEventListener('submit', () => {
|
||||
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
selectElement.addEventListener('change', (event) => {
|
||||
saveAddress(event.target.value, event.target.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
loadInitialAddress();
|
||||
}
|
||||
|
||||
const RateManager = {
|
||||
lookupRates: () => {
|
||||
const coinFrom = DOM.getValue('coin_from');
|
||||
const coinTo = DOM.getValue('coin_to');
|
||||
const ratesDisplay = DOM.get('rates_display');
|
||||
|
||||
if (!coinFrom || !coinTo || !ratesDisplay) {
|
||||
console.log('Required elements for lookup_rates not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (coinFrom === '-1' || coinTo === '-1') {
|
||||
alert('Coins from and to must be set first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCoin = (coinFrom === '15') ? '3' : coinFrom;
|
||||
|
||||
ratesDisplay.innerHTML = '<p>Updating...</p>';
|
||||
|
||||
const priceJsonElement = DOM.query(".pricejsonhidden");
|
||||
if (priceJsonElement) {
|
||||
priceJsonElement.classList.remove("hidden");
|
||||
}
|
||||
|
||||
const params = 'coin_from=' + selectedCoin + '&coin_to=' + coinTo;
|
||||
|
||||
Ajax.post('/json/rates', params,
|
||||
(response) => {
|
||||
if (ratesDisplay) {
|
||||
ratesDisplay.innerHTML = typeof response === 'string' ?
|
||||
response : '<pre><code>' + JSON.stringify(response, null, ' ') + '</code></pre>';
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (ratesDisplay) {
|
||||
ratesDisplay.innerHTML = '<p>Error loading rates: ' + error + '</p>';
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getRateInferred: (event) => {
|
||||
if (event) event.preventDefault();
|
||||
|
||||
const coinFrom = DOM.getValue('coin_from');
|
||||
const coinTo = DOM.getValue('coin_to');
|
||||
const rateElement = DOM.get('rate');
|
||||
|
||||
if (!coinFrom || !coinTo || !rateElement) {
|
||||
console.log('Required elements for getRateInferred not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = 'coin_from=' + encodeURIComponent(coinFrom) +
|
||||
'&coin_to=' + encodeURIComponent(coinTo);
|
||||
|
||||
DOM.setValue('rate', 'Loading...');
|
||||
|
||||
Ajax.post('/json/rates', params,
|
||||
(response) => {
|
||||
if (response.coingecko && response.coingecko.rate_inferred) {
|
||||
DOM.setValue('rate', response.coingecko.rate_inferred);
|
||||
RateManager.setRate('rate');
|
||||
} else {
|
||||
DOM.setValue('rate', 'Error: No rate available');
|
||||
console.error('Rate not available in response');
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
DOM.setValue('rate', 'Error: Rate lookup failed');
|
||||
console.error('Error fetching rate data:', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
setRate: (valueChanged) => {
|
||||
const elements = {
|
||||
coinFrom: DOM.get('coin_from'),
|
||||
coinTo: DOM.get('coin_to'),
|
||||
amtFrom: DOM.get('amt_from'),
|
||||
amtTo: DOM.get('amt_to'),
|
||||
rate: DOM.get('rate'),
|
||||
rateLock: DOM.get('rate_lock'),
|
||||
swapType: DOM.get('swap_type')
|
||||
};
|
||||
|
||||
if (!elements.coinFrom || !elements.coinTo ||
|
||||
!elements.amtFrom || !elements.amtTo || !elements.rate) {
|
||||
console.log('Required elements for setRate not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const values = {
|
||||
coinFrom: elements.coinFrom.value,
|
||||
coinTo: elements.coinTo.value,
|
||||
amtFrom: elements.amtFrom.value,
|
||||
amtTo: elements.amtTo.value,
|
||||
rate: elements.rate.value,
|
||||
lockRate: elements.rate.value == '' ? false :
|
||||
(elements.rateLock ? elements.rateLock.checked : false)
|
||||
};
|
||||
|
||||
if (valueChanged === 'coin_from' || valueChanged === 'coin_to') {
|
||||
DOM.setValue('rate', '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (elements.swapType) {
|
||||
SwapTypeManager.setSwapTypeEnabled(
|
||||
values.coinFrom,
|
||||
values.coinTo,
|
||||
elements.swapType
|
||||
);
|
||||
}
|
||||
|
||||
if (values.coinFrom == '-1' || values.coinTo == '-1') {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = 'coin_from=' + values.coinFrom + '&coin_to=' + values.coinTo;
|
||||
|
||||
if (valueChanged == 'rate' ||
|
||||
(values.lockRate && valueChanged == 'amt_from') ||
|
||||
(values.amtTo == '' && valueChanged == 'amt_from')) {
|
||||
|
||||
if (values.rate == '' || (values.amtFrom == '' && values.amtTo == '')) {
|
||||
return;
|
||||
} else if (values.amtFrom == '' && values.amtTo != '') {
|
||||
if (valueChanged == 'amt_from') {
|
||||
return;
|
||||
}
|
||||
params += '&rate=' + values.rate + '&amt_to=' + values.amtTo;
|
||||
} else {
|
||||
params += '&rate=' + values.rate + '&amt_from=' + values.amtFrom;
|
||||
}
|
||||
} else if (values.lockRate && valueChanged == 'amt_to') {
|
||||
if (values.amtTo == '' || values.rate == '') {
|
||||
return;
|
||||
}
|
||||
params += '&amt_to=' + values.amtTo + '&rate=' + values.rate;
|
||||
} else {
|
||||
if (values.amtFrom == '' || values.amtTo == '') {
|
||||
return;
|
||||
}
|
||||
params += '&amt_from=' + values.amtFrom + '&amt_to=' + values.amtTo;
|
||||
}
|
||||
|
||||
Ajax.post('/json/rate', params,
|
||||
(response) => {
|
||||
if (response.hasOwnProperty('rate')) {
|
||||
DOM.setValue('rate', response.rate);
|
||||
} else if (response.hasOwnProperty('amount_to')) {
|
||||
DOM.setValue('amt_to', response.amount_to);
|
||||
} else if (response.hasOwnProperty('amount_from')) {
|
||||
DOM.setValue('amt_from', response.amount_from);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Rate calculation failed:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function set_rate(valueChanged) {
|
||||
RateManager.setRate(valueChanged);
|
||||
}
|
||||
|
||||
function lookup_rates() {
|
||||
RateManager.lookupRates();
|
||||
}
|
||||
|
||||
function getRateInferred(event) {
|
||||
RateManager.getRateInferred(event);
|
||||
}
|
||||
|
||||
const SwapTypeManager = {
|
||||
adaptor_sig_only_coins: ['6', '9', '8', '7', '13', '18', '17'],
|
||||
secret_hash_only_coins: ['11', '12'],
|
||||
|
||||
setSwapTypeEnabled: (coinFrom, coinTo, swapTypeElement) => {
|
||||
if (!swapTypeElement) return;
|
||||
|
||||
let makeHidden = false;
|
||||
coinFrom = String(coinFrom);
|
||||
coinTo = String(coinTo);
|
||||
|
||||
if (SwapTypeManager.adaptor_sig_only_coins.includes(coinFrom) ||
|
||||
SwapTypeManager.adaptor_sig_only_coins.includes(coinTo)) {
|
||||
swapTypeElement.disabled = true;
|
||||
swapTypeElement.value = 'xmr_swap';
|
||||
makeHidden = true;
|
||||
swapTypeElement.classList.add('select-disabled');
|
||||
} else if (SwapTypeManager.secret_hash_only_coins.includes(coinFrom) ||
|
||||
SwapTypeManager.secret_hash_only_coins.includes(coinTo)) {
|
||||
swapTypeElement.disabled = true;
|
||||
swapTypeElement.value = 'seller_first';
|
||||
makeHidden = true;
|
||||
swapTypeElement.classList.add('select-disabled');
|
||||
} else {
|
||||
swapTypeElement.disabled = false;
|
||||
swapTypeElement.classList.remove('select-disabled');
|
||||
if (['xmr_swap', 'seller_first'].includes(swapTypeElement.value) == false) {
|
||||
swapTypeElement.value = 'xmr_swap';
|
||||
}
|
||||
}
|
||||
|
||||
let swapTypeHidden = DOM.get('swap_type_hidden');
|
||||
if (makeHidden) {
|
||||
if (!swapTypeHidden) {
|
||||
const form = DOM.get('form');
|
||||
if (form) {
|
||||
swapTypeHidden = document.createElement('input');
|
||||
swapTypeHidden.setAttribute('id', 'swap_type_hidden');
|
||||
swapTypeHidden.setAttribute('type', 'hidden');
|
||||
swapTypeHidden.setAttribute('name', 'swap_type');
|
||||
form.appendChild(swapTypeHidden);
|
||||
}
|
||||
}
|
||||
if (swapTypeHidden) {
|
||||
swapTypeHidden.setAttribute('value', swapTypeElement.value);
|
||||
}
|
||||
} else if (swapTypeHidden) {
|
||||
swapTypeHidden.parentNode.removeChild(swapTypeHidden);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const UIEnhancer = {
|
||||
handleErrorHighlighting: () => {
|
||||
const errMsgs = document.querySelectorAll('p.error_msg');
|
||||
|
||||
const errorFieldMap = {
|
||||
'coin_to': ['coin_to', 'Coin To'],
|
||||
'coin_from': ['Coin From'],
|
||||
'amt_from': ['Amount From'],
|
||||
'amt_to': ['Amount To'],
|
||||
'amt_bid_min': ['Minimum Bid Amount'],
|
||||
'Select coin you send': ['coin_from', 'parentNode']
|
||||
};
|
||||
|
||||
errMsgs.forEach(errMsg => {
|
||||
const text = errMsg.innerText;
|
||||
|
||||
Object.entries(errorFieldMap).forEach(([field, keywords]) => {
|
||||
if (keywords.some(keyword => text.includes(keyword))) {
|
||||
let element = DOM.get(field);
|
||||
|
||||
if (field === 'Select coin you send' && element) {
|
||||
element = element.parentNode;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.classList.add('error');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('input.error, select.error').forEach(element => {
|
||||
element.addEventListener('focus', event => {
|
||||
event.target.classList.remove('error');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
updateDisabledStyles: () => {
|
||||
document.querySelectorAll('select.disabled-select').forEach(select => {
|
||||
if (select.disabled) {
|
||||
select.classList.add('disabled-select-enabled');
|
||||
} else {
|
||||
select.classList.remove('disabled-select-enabled');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input').forEach(input => {
|
||||
if (input.readOnly) {
|
||||
input.classList.add('disabled-input-enabled');
|
||||
} else {
|
||||
input.classList.remove('disabled-input-enabled');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupCustomSelects: () => {
|
||||
const selectCache = {};
|
||||
|
||||
function updateSelectCache(select) {
|
||||
if (!select || !select.options || select.selectedIndex === undefined) return;
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
if (!selectedOption) return;
|
||||
|
||||
const image = selectedOption.getAttribute('data-image');
|
||||
const name = selectedOption.textContent.trim();
|
||||
selectCache[select.id] = { image, name };
|
||||
}
|
||||
|
||||
function setSelectData(select) {
|
||||
if (!select || !select.options || select.selectedIndex === undefined) return;
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
if (!selectedOption) return;
|
||||
|
||||
const image = selectedOption.getAttribute('data-image') || '';
|
||||
const name = selectedOption.textContent.trim();
|
||||
|
||||
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
|
||||
|
||||
const selectImage = select.nextElementSibling?.querySelector('.select-image');
|
||||
if (selectImage) {
|
||||
selectImage.src = image;
|
||||
}
|
||||
|
||||
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
|
||||
if (selectNameElement) {
|
||||
|
||||
if (select.id === 'coin_from' && name.includes(' - Balance: ')) {
|
||||
|
||||
const parts = name.split(' - Balance: ');
|
||||
const coinName = parts[0];
|
||||
const balanceInfo = parts[1] || '';
|
||||
|
||||
selectNameElement.innerHTML = '';
|
||||
selectNameElement.style.display = 'flex';
|
||||
selectNameElement.style.flexDirection = 'column';
|
||||
selectNameElement.style.alignItems = 'flex-start';
|
||||
selectNameElement.style.lineHeight = '1.2';
|
||||
|
||||
const coinNameDiv = document.createElement('div');
|
||||
coinNameDiv.textContent = coinName;
|
||||
coinNameDiv.style.fontWeight = 'normal';
|
||||
coinNameDiv.style.color = 'inherit';
|
||||
|
||||
const balanceDiv = document.createElement('div');
|
||||
balanceDiv.textContent = `Balance: ${balanceInfo}`;
|
||||
balanceDiv.style.fontSize = '0.75rem';
|
||||
balanceDiv.style.color = '#6b7280';
|
||||
balanceDiv.style.marginTop = '1px';
|
||||
|
||||
selectNameElement.appendChild(coinNameDiv);
|
||||
selectNameElement.appendChild(balanceDiv);
|
||||
|
||||
} else {
|
||||
|
||||
selectNameElement.textContent = name;
|
||||
selectNameElement.style.display = 'block';
|
||||
selectNameElement.style.flexDirection = '';
|
||||
selectNameElement.style.alignItems = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectCache(select);
|
||||
}
|
||||
|
||||
function setupCustomSelect(select) {
|
||||
if (!select) return;
|
||||
|
||||
const options = select.querySelectorAll('option');
|
||||
const selectIcon = select.parentElement?.querySelector('.select-icon');
|
||||
const selectImage = select.parentElement?.querySelector('.select-image');
|
||||
|
||||
if (!options || !selectIcon || !selectImage) return;
|
||||
|
||||
options.forEach(option => {
|
||||
const image = option.getAttribute('data-image');
|
||||
if (image) {
|
||||
option.style.backgroundImage = `url(${image})`;
|
||||
}
|
||||
});
|
||||
|
||||
const storedValue = Storage.getRaw(select.name);
|
||||
if (storedValue && select.value == '-1') {
|
||||
select.value = storedValue;
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
setSelectData(select);
|
||||
Storage.setRaw(select.name, select.value);
|
||||
});
|
||||
|
||||
setSelectData(select);
|
||||
selectIcon.style.display = 'none';
|
||||
selectImage.style.display = 'none';
|
||||
}
|
||||
|
||||
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
|
||||
const selectImages = document.querySelectorAll('.custom-select .select-image');
|
||||
const selectNames = document.querySelectorAll('.custom-select .select-name');
|
||||
|
||||
selectIcons.forEach(icon => icon.style.display = 'none');
|
||||
selectImages.forEach(image => image.style.display = 'none');
|
||||
selectNames.forEach(name => name.style.display = 'none');
|
||||
|
||||
const customSelects = document.querySelectorAll('.custom-select select');
|
||||
customSelects.forEach(setupCustomSelect);
|
||||
}
|
||||
};
|
||||
|
||||
function initializeApp() {
|
||||
handleNewOfferAddress();
|
||||
|
||||
DOM.addEvent('get_rate_inferred_button', 'click', RateManager.getRateInferred);
|
||||
|
||||
const coinFrom = DOM.get('coin_from');
|
||||
const coinTo = DOM.get('coin_to');
|
||||
const swapType = DOM.get('swap_type');
|
||||
|
||||
if (coinFrom && coinTo && swapType) {
|
||||
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, coinTo.value, swapType);
|
||||
|
||||
coinFrom.addEventListener('change', function() {
|
||||
SwapTypeManager.setSwapTypeEnabled(this.value, coinTo.value, swapType);
|
||||
RateManager.setRate('coin_from');
|
||||
});
|
||||
|
||||
coinTo.addEventListener('change', function() {
|
||||
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, this.value, swapType);
|
||||
RateManager.setRate('coin_to');
|
||||
});
|
||||
}
|
||||
|
||||
['amt_from', 'amt_to', 'rate'].forEach(id => {
|
||||
DOM.addEvent(id, 'change', function() {
|
||||
RateManager.setRate(id);
|
||||
});
|
||||
|
||||
DOM.addEvent(id, 'input', function() {
|
||||
RateManager.setRate(id);
|
||||
});
|
||||
});
|
||||
|
||||
DOM.addEvent('rate_lock', 'change', function() {
|
||||
if (DOM.getValue('rate')) {
|
||||
RateManager.setRate('rate');
|
||||
}
|
||||
});
|
||||
|
||||
DOM.addEvent('lookup_rates_button', 'click', RateManager.lookupRates);
|
||||
|
||||
UIEnhancer.handleErrorHighlighting();
|
||||
UIEnhancer.updateDisabledStyles();
|
||||
UIEnhancer.setupCustomSelects();
|
||||
|
||||
ErrorModal.init();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
||||
|
||||
window.showErrorModal = ErrorModal.show.bind(ErrorModal);
|
||||
window.hideErrorModal = ErrorModal.hide.bind(ErrorModal);
|
||||
Reference in New Issue
Block a user