mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 09:54:30 +01:00
fc9d4f96a7
* Design system updates * Icon fix * Add new icons, replace show/hide * Icon replacements * Test fix * Icon replacements in Vault * More icon replacements * Final icon replacements, remove Font Awesome
502 lines
20 KiB
JavaScript
502 lines
20 KiB
JavaScript
const baseUrl = Object.values(document.scripts).find(s => s.src.includes('/main/site.js')).src.split('/main/site.js').shift();
|
|
|
|
const flatpickrInstances = [];
|
|
|
|
|
|
const switchTimeFormat = event => {
|
|
const curr = event.target.dataset.mode || 'localized';
|
|
const mode = curr === 'relative' ? 'localized' : 'relative';
|
|
document.querySelectorAll("time[datetime]").forEach($el => {
|
|
$el.innerText = $el.dataset[mode];
|
|
});
|
|
event.target.dataset.mode = mode;
|
|
};
|
|
|
|
async function initLabelManager (elementId) {
|
|
const element = document.getElementById(elementId);
|
|
|
|
const labelStyle = data =>
|
|
data && data.color && data.textColor
|
|
? `--label-bg:${data.color};--label-fg:${data.textColor}`
|
|
: '--label-bg:var(--btcpay-neutral-300);--label-fg:var(--btcpay-neutral-800)'
|
|
|
|
if (element) {
|
|
const { fetchUrl, updateUrl, walletId, walletObjectType, walletObjectId, labels, selectElement } = element.dataset;
|
|
const commonCallId = `walletLabels-${walletId}`;
|
|
if (!window[commonCallId]) {
|
|
window[commonCallId] = fetch(fetchUrl, {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
}).then(res => res.json());
|
|
}
|
|
const items = element.value.split(',').filter(x => !!x);
|
|
const options = await window[commonCallId].then(labels => {
|
|
const newItems = items.filter(item => !labels.find(label => label.label === item));
|
|
labels = [...labels, ...newItems.map(item => ({ label: item }))];
|
|
return labels;
|
|
});
|
|
const richInfo = labels ? JSON.parse(labels) : {};
|
|
const config = {
|
|
options,
|
|
items,
|
|
valueField: "label",
|
|
labelField: "label",
|
|
searchField: "label",
|
|
create: true,
|
|
persist: true,
|
|
allowEmptyOption: false,
|
|
closeAfterSelect: false,
|
|
render: {
|
|
dropdown (){
|
|
return '<div class="dropdown-menu"></div>';
|
|
},
|
|
option_create: function(data, escape) {
|
|
return `<div class="transaction-label create" style="${labelStyle(null)}">Add <strong>${escape(data.input)}</strong>…</div>`;
|
|
},
|
|
option (data, escape) {
|
|
return `<div class="transaction-label" style="${labelStyle(data)}"><span>${escape(data.label)}</span></div>`;
|
|
},
|
|
item (data, escape) {
|
|
const info = richInfo && richInfo[data.label];
|
|
const additionalInfo = info
|
|
? `<a href="${info.link}" target="_blank" rel="noreferrer noopener" class="transaction-label-info transaction-details-icon" title="${info.tooltip}" data-bs-html="true"
|
|
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip"><svg role="img" class="icon icon-info"><use href="/img/icon-sprite.svg#info"></use></svg></a>`
|
|
: '';
|
|
const inner = `<span>${escape(data.label)}</span>${additionalInfo}`;
|
|
return `<div class="transaction-label" style="${labelStyle(data)}">${inner}</div>`;
|
|
}
|
|
},
|
|
onItemAdd (val) {
|
|
window[commonCallId] = window[commonCallId].then(labels => {
|
|
return [...labels, { label: val }]
|
|
});
|
|
|
|
document.dispatchEvent(new CustomEvent(`${commonCallId}-option-added`, {
|
|
detail: val
|
|
}));
|
|
},
|
|
async onChange (values) {
|
|
const selectElementI = selectElement ? document.getElementById(selectElement) : null;
|
|
if (selectElementI){
|
|
while (selectElementI.options.length > 0) {
|
|
selectElementI.remove(0);
|
|
}
|
|
select.items.forEach((item) => {
|
|
selectElementI.add(new Option(item, item, true, true));
|
|
})
|
|
}
|
|
if(!updateUrl)
|
|
return;
|
|
select.lock();
|
|
try {
|
|
const response = await fetch(updateUrl, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
id: walletObjectId,
|
|
type: walletObjectType,
|
|
labels: select.items
|
|
})
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not OK');
|
|
}
|
|
} catch (error) {
|
|
console.error('There has been a problem with your fetch operation:', error);
|
|
} finally {
|
|
select.unlock();
|
|
}
|
|
}
|
|
};
|
|
const select = new TomSelect(element, config);
|
|
|
|
element.parentElement.querySelectorAll('.ts-control .transaction-label a').forEach(lbl => {
|
|
lbl.addEventListener('click', e => {
|
|
e.stopPropagation()
|
|
})
|
|
})
|
|
|
|
document.addEventListener(`${commonCallId}-option-added`, evt => {
|
|
if (!(evt.detail in select.options)) {
|
|
select.addOption({
|
|
label: evt.detail
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const initLabelManagers = () => {
|
|
// select only elements which haven't been initialized before, those without data-localized
|
|
document.querySelectorAll("input.label-manager:not(.tomselected)").forEach($el => {
|
|
initLabelManager($el.id)
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// sticky header
|
|
const stickyHeader = document.querySelector('#mainContent > section .sticky-header');
|
|
if (stickyHeader) {
|
|
const setStickyHeaderHeight = () => {
|
|
document.documentElement.style.setProperty('--sticky-header-height', `${stickyHeader.offsetHeight}px`)
|
|
}
|
|
window.addEventListener('resize', e => {
|
|
debounce('resize', setStickyHeaderHeight, 50)
|
|
});
|
|
setStickyHeaderHeight();
|
|
}
|
|
|
|
// initialize timezone offset value if field is present in page
|
|
const $timezoneOffset = document.getElementById("TimezoneOffset");
|
|
const timezoneOffset = new Date().getTimezoneOffset();
|
|
if ($timezoneOffset) $timezoneOffset.value = timezoneOffset;
|
|
|
|
// localize all elements that have localizeDate class
|
|
formatDateTimes();
|
|
|
|
initLabelManagers();
|
|
|
|
function updateTimeAgo(){
|
|
var timeagoElements = $("[data-timeago-unixms]");
|
|
timeagoElements.each(function () {
|
|
var elem = $(this);
|
|
elem.text(moment(elem.data("timeago-unixms")).fromNow());
|
|
});
|
|
setTimeout(updateTimeAgo, 1000);
|
|
}
|
|
updateTimeAgo();
|
|
|
|
// intializing date time pickers
|
|
$(".flatdtpicker").each(function () {
|
|
var element = $(this);
|
|
var fdtp = element.attr("data-fdtp");
|
|
|
|
// support for initializing with special options per instance
|
|
if (fdtp) {
|
|
var parsed = JSON.parse(fdtp);
|
|
flatpickrInstances.push(element.flatpickr(parsed));
|
|
} else {
|
|
var min = element.attr("min");
|
|
var max = element.attr("max");
|
|
var defaultDate = element.attr("value");
|
|
flatpickrInstances.push(element.flatpickr({
|
|
enableTime: true,
|
|
enableSeconds: true,
|
|
dateFormat: 'Z',
|
|
altInput: true,
|
|
altFormat: 'Y-m-d H:i:S',
|
|
minDate: min,
|
|
maxDate: max,
|
|
defaultDate: defaultDate,
|
|
time_24hr: true,
|
|
defaultHour: 0,
|
|
static: true
|
|
}));
|
|
}
|
|
});
|
|
// rich text editor
|
|
if ($.summernote) {
|
|
$('.richtext').summernote({
|
|
minHeight: 300,
|
|
tableClassName: 'table table-sm',
|
|
insertTableMaxSize: {
|
|
col: 5,
|
|
row: 10
|
|
},
|
|
codeviewFilter: true,
|
|
codeviewFilterRegex: new RegExp($.summernote.options.codeviewFilterRegex.source + '|<.*?( on\\w+?=.*?)>', 'gi'),
|
|
codeviewIframeWhitelistSrc: ['twitter.com', 'syndication.twitter.com']
|
|
});
|
|
}
|
|
|
|
$(".input-group-clear").on("click", function () {
|
|
const input = $(this).parents(".input-group").find("input");
|
|
const event = new CustomEvent('input-group-clear-input-value-cleared', { detail: input });
|
|
input.val(null);
|
|
document.dispatchEvent(event);
|
|
handleInputGroupClearButtonDisplay(this);
|
|
});
|
|
|
|
$(".input-group-clear").each(function () {
|
|
var inputGroupClearBtn = this;
|
|
handleInputGroupClearButtonDisplay(inputGroupClearBtn);
|
|
$(this).parents(".input-group").find("input").on("change input", function () {
|
|
handleInputGroupClearButtonDisplay(inputGroupClearBtn);
|
|
});
|
|
});
|
|
|
|
$('[data-bs-toggle="tooltip"]').tooltip();
|
|
|
|
function handleInputGroupClearButtonDisplay(element) {
|
|
var inputs = $(element).parents(".input-group").find("input");
|
|
|
|
$(element).hide();
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
var el = inputs.get(i);
|
|
if ($(el).val() || el.attributes.value) {
|
|
$(element).show();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
delegate('click', '[data-toggle-password]', async e => {
|
|
const $button = e.target.closest('[data-toggle-password]')
|
|
const $el = document.querySelector($button.dataset.togglePassword);
|
|
if (!$el) return;
|
|
const isPassword = $el.getAttribute('type') === 'password';
|
|
if (isPassword) {
|
|
$el.setAttribute('type', 'text')
|
|
if (!!$button.innerHTML.match('#actions-show')) $button.innerHTML = $button.innerHTML.replace('#actions-show', '#actions-hide');
|
|
} else {
|
|
$el.setAttribute('type', 'password')
|
|
if (!!$button.innerHTML.match('#actions-hide')) $button.innerHTML = $button.innerHTML.replace('#actions-hide', '#actions-show');
|
|
}
|
|
})
|
|
|
|
// Invoice Status
|
|
delegate('click', '[data-invoice-state-badge] [data-invoice-id][data-new-state]', async e => {
|
|
const $button = e.target
|
|
const $badge = $button.closest('[data-invoice-state-badge]')
|
|
const { invoiceId, newState } = $button.dataset
|
|
|
|
$badge.classList.add('pe-none'); // disable further interaction
|
|
const response = await fetch(`${baseUrl}/invoices/${invoiceId}/changestate/${newState}`, { method: 'POST' })
|
|
if (response.ok) {
|
|
const { statusString } = await response.json()
|
|
$badge.outerHTML = `<div class="badge badge-${newState}" data-invoice-state-badge="${invoiceId}">${statusString}</div>`
|
|
} else {
|
|
$badge.classList.remove('pe-none');
|
|
alert("Invoice state update failed");
|
|
}
|
|
})
|
|
|
|
// Time Format
|
|
delegate('click', '.switch-time-format', switchTimeFormat);
|
|
|
|
// Theme Switch
|
|
delegate('click', '.btcpay-theme-switch [data-theme]', e => {
|
|
e.preventDefault()
|
|
const $btn = e.target.closest('.btcpay-theme-switch [data-theme]')
|
|
setColorMode($btn.dataset.theme)
|
|
$btn.blur()
|
|
})
|
|
|
|
// Sensitive Info
|
|
const SENSITIVE_INFO_STORE_KEY = 'btcpay-hide-sensitive-info';
|
|
const SENSITIVE_INFO_DATA_ATTR = 'data-hide-sensitive-info';
|
|
delegate('change', '#HideSensitiveInfo', e => {
|
|
e.preventDefault()
|
|
const isActive = window.localStorage.getItem(SENSITIVE_INFO_STORE_KEY) === 'true';
|
|
if (isActive) {
|
|
window.localStorage.removeItem(SENSITIVE_INFO_STORE_KEY);
|
|
document.documentElement.removeAttribute(SENSITIVE_INFO_DATA_ATTR);
|
|
} else {
|
|
window.localStorage.setItem(SENSITIVE_INFO_STORE_KEY, 'true');
|
|
document.documentElement.setAttribute(SENSITIVE_INFO_DATA_ATTR, 'true');
|
|
}
|
|
});
|
|
|
|
// Currency Selection: Remove the current input value once the element is focused, so that the user gets to
|
|
// see the available options. If no selection or change is made, reset it to the previous value on blur.
|
|
// Note: Use focusin/focusout instead of focus/blur, because the latter do not bubble up and delegate won't work.
|
|
delegate('focusin', 'input[list="currency-selection-suggestion"]', e => {
|
|
e.target.setAttribute('placeholder', e.target.value)
|
|
e.target.value = '';
|
|
})
|
|
delegate('focusout', 'input[list="currency-selection-suggestion"]', e => {
|
|
if (!e.target.value) e.target.value = e.target.getAttribute('placeholder')
|
|
e.target.removeAttribute('placeholder')
|
|
})
|
|
|
|
// Offcanvas navigation
|
|
const mainMenuToggle = document.getElementById('mainMenuToggle')
|
|
if (mainMenuToggle) {
|
|
delegate('show.bs.offcanvas', '#mainNav', () => {
|
|
mainMenuToggle.setAttribute('aria-expanded', 'true')
|
|
})
|
|
delegate('hide.bs.offcanvas', '#mainNav', () => {
|
|
mainMenuToggle.setAttribute('aria-expanded', 'false')
|
|
})
|
|
}
|
|
|
|
// Menu collapses
|
|
const mainNav = document.getElementById('mainNav')
|
|
if (mainNav) {
|
|
const COLLAPSED_KEY = 'btcpay-nav-collapsed'
|
|
delegate('show.bs.collapse', '#mainNav', (e) => {
|
|
const { id } = e.target
|
|
const navCollapsed = window.localStorage.getItem(COLLAPSED_KEY)
|
|
const collapsed = navCollapsed ? JSON.parse(navCollapsed).filter(i => i !== id ) : []
|
|
window.localStorage.setItem(COLLAPSED_KEY, JSON.stringify(collapsed))
|
|
})
|
|
delegate('hide.bs.collapse', '#mainNav', (e) => {
|
|
const { id } = e.target
|
|
const navCollapsed = window.localStorage.getItem(COLLAPSED_KEY)
|
|
const collapsed = navCollapsed ? JSON.parse(navCollapsed) : []
|
|
if (!collapsed.includes(id)) collapsed.push(id)
|
|
window.localStorage.setItem(COLLAPSED_KEY, JSON.stringify(collapsed))
|
|
})
|
|
}
|
|
|
|
// Mass Action Tables
|
|
const updateSelectedCount = ($table) => {
|
|
const selectedCount = document.querySelectorAll('.mass-action-select:checked').length;
|
|
const $selectedCount = $table.querySelector('.mass-action-selected-count');
|
|
if ($selectedCount) $selectedCount.innerText = selectedCount;
|
|
if (selectedCount === 0) {
|
|
$table.removeAttribute('data-selected');
|
|
} else {
|
|
$table.setAttribute('data-selected', selectedCount.toString());
|
|
}
|
|
}
|
|
|
|
delegate('click', '.mass-action .mass-action-select-all', e => {
|
|
const $table = e.target.closest('.mass-action');
|
|
const { checked } = e.target;
|
|
$table.querySelectorAll('.mass-action-select,.mass-action-select-all').forEach($checkbox => {
|
|
$checkbox.checked = checked;
|
|
});
|
|
updateSelectedCount($table);
|
|
});
|
|
|
|
delegate('change', '.mass-action .mass-action-select', e => {
|
|
const $table = e.target.closest('.mass-action');
|
|
const selectedCount = $table.querySelectorAll('.mass-action-select:checked').length;
|
|
if (selectedCount === 0) {
|
|
$table.querySelectorAll('.mass-action-select-all').forEach(checkbox => {
|
|
checkbox.checked = false;
|
|
});
|
|
}
|
|
updateSelectedCount($table);
|
|
});
|
|
|
|
delegate('click', '.mass-action .mass-action-row', e => {
|
|
const $target = e.target
|
|
if ($target.matches('td,time,span[data-sensitive]')) {
|
|
const $row = $target.closest('.mass-action-row');
|
|
$row.querySelector('.mass-action-select').click();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Initialize Blazor
|
|
if (window.Blazor) {
|
|
let isUnloading = false;
|
|
window.addEventListener("beforeunload", () => { isUnloading = true; });
|
|
let brokenConnection = {
|
|
isConnected: false,
|
|
titleContent: 'Connection broken',
|
|
innerHTML: 'Please <a href="">refresh the page</a>.'
|
|
};
|
|
let interruptedConnection = {
|
|
isConnected: false,
|
|
titleContent: 'Connection interrupted',
|
|
innerHTML: 'Attempt to reestablish the connection in a few seconds...'
|
|
};
|
|
let successfulConnection = {
|
|
isConnected: true,
|
|
titleContent: 'Connection established',
|
|
innerHTML: '' // use empty link on purpose
|
|
};
|
|
class BlazorReconnectionHandler {
|
|
reconnecting = false;
|
|
async onConnectionDown(options, _error) {
|
|
if (this.reconnecting)
|
|
return;
|
|
this.setBlazorStatus(interruptedConnection);
|
|
this.reconnecting = true;
|
|
console.debug('Blazor hub connection lost');
|
|
await this.reconnect();
|
|
}
|
|
|
|
async reconnect() {
|
|
let delays = [500, 1000, 2000, 4000, 8000, 16000, 20000, 40000];
|
|
let i = 0;
|
|
const lastDelay = delays.length - 1;
|
|
while (i < delays.length) {
|
|
await this.delay(delays[i]);
|
|
try {
|
|
if (await Blazor.reconnect())
|
|
return;
|
|
console.warn('Error while reconnecting to Blazor hub (Broken circuit)');
|
|
break;
|
|
}
|
|
catch (err) {
|
|
this.setBlazorStatus(interruptedConnection);
|
|
console.warn(`Error while reconnecting to Blazor hub (${err})`);
|
|
}
|
|
i++;
|
|
}
|
|
this.setBlazorStatus(brokenConnection);
|
|
}
|
|
onConnectionUp() {
|
|
this.reconnecting = false;
|
|
console.debug('Blazor hub connected');
|
|
this.setBlazorStatus(successfulConnection);
|
|
}
|
|
|
|
setBlazorStatus(content) {
|
|
document.querySelectorAll('.blazor-status').forEach($status => {
|
|
const $state = $status.querySelector('.blazor-status__state');
|
|
const $title = $status.querySelector('.blazor-status__title');
|
|
const $body = $status.querySelector('.blazor-status__body');
|
|
$state.classList.remove('btcpay-status--enabled');
|
|
$state.classList.remove('btcpay-status--disabled');
|
|
$state.classList.add(content.isConnected ? 'btcpay-status--enabled' : 'btcpay-status--disabled');
|
|
$title.textContent = content.titleContent;
|
|
$body.innerHTML = content.innerHTML;
|
|
$body.classList.toggle('d-none', content.isConnected);
|
|
if (!isUnloading) {
|
|
const toast = new bootstrap.Toast($status, { autohide: false });
|
|
if (content.isConnected) {
|
|
if (toast.isShown())
|
|
toast.hide();
|
|
}
|
|
else {
|
|
if (!toast.isShown())
|
|
toast.show();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
delay(durationMilliseconds) {
|
|
return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
|
|
}
|
|
}
|
|
|
|
const handler = new BlazorReconnectionHandler();
|
|
handler.setBlazorStatus(successfulConnection);
|
|
Blazor.start({
|
|
reconnectionHandler: handler
|
|
});
|
|
}
|
|
|
|
String.prototype.noExponents= function(){
|
|
const data = String(this).split(/[eE]/);
|
|
if(data.length== 1) return data[0];
|
|
|
|
var z= '', sign= this<0? '-':'',
|
|
str= data[0].replace('.', ''),
|
|
mag= Number(data[1])+ 1;
|
|
|
|
if(mag<0){
|
|
z= sign + '0.';
|
|
while(mag++) z += '0';
|
|
return z + str.replace(/^\-/,'');
|
|
}
|
|
mag -= str.length;
|
|
while(mag--) z += '0';
|
|
return str + z;
|
|
}
|
|
|
|
Number.prototype.noExponents= function(){
|
|
return String(this).noExponents();
|
|
};
|