mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 18:11:36 +01:00
418 lines
17 KiB
JavaScript
418 lines
17 KiB
JavaScript
const flatpickrInstances = [];
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
|
const dtFormatOpts = { dateStyle: 'short', timeStyle: 'short' };
|
|
|
|
const formatDateTimes = format => {
|
|
// select only elements which haven't been initialized before, those without data-localized
|
|
document.querySelectorAll("time[datetime]:not([data-localized])").forEach($el => {
|
|
const date = new Date($el.getAttribute("datetime"));
|
|
// initialize and set localized attribute
|
|
$el.dataset.localized = new Intl.DateTimeFormat('default', dtFormatOpts).format(date);
|
|
// set text to chosen mode
|
|
const mode = format || $el.dataset.initial;
|
|
if ($el.dataset[mode]) $el.innerText = $el.dataset[mode];
|
|
});
|
|
};
|
|
|
|
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) {
|
|
document.documentElement.style.scrollPaddingTop = `calc(${stickyHeader.offsetHeight}px + var(--btcpay-space-m))`;
|
|
}
|
|
|
|
// initialize timezone offset value if field is present in page
|
|
var timezoneOffset = new Date().getTimezoneOffset();
|
|
$("#TimezoneOffset").val(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
$('[data-toggle="password"]').each(function () {
|
|
var input = $(this);
|
|
var eye_btn = $(this).parent().find('.input-group-text');
|
|
eye_btn.css('cursor', 'pointer').addClass('input-password-hide');
|
|
eye_btn.on('click', function () {
|
|
if (eye_btn.hasClass('input-password-hide')) {
|
|
eye_btn.removeClass('input-password-hide').addClass('input-password-show');
|
|
eye_btn.find('.fa').removeClass('fa-eye').addClass('fa-eye-slash')
|
|
input.attr('type', 'text');
|
|
} else {
|
|
eye_btn.removeClass('input-password-show').addClass('input-password-hide');
|
|
eye_btn.find('.fa').removeClass('fa-eye-slash').addClass('fa-eye')
|
|
input.attr('type', 'password');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Time Format
|
|
delegate('click', '.switch-time-format', switchTimeFormat);
|
|
|
|
// Theme Switch
|
|
delegate('click', '.btcpay-theme-switch', e => {
|
|
e.preventDefault()
|
|
const current = document.documentElement.getAttribute(THEME_ATTR) || COLOR_MODES[0]
|
|
const mode = current === COLOR_MODES[0] ? COLOR_MODES[1] : COLOR_MODES[0]
|
|
setColorMode(mode)
|
|
e.target.closest('.btcpay-theme-switch').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))
|
|
})
|
|
}
|
|
});
|
|
|
|
// Initialize Blazor
|
|
if (window.Blazor) {
|
|
let isUnloading = false;
|
|
window.addEventListener("beforeunload", () => { isUnloading = true; });
|
|
class BlazorReconnectionHandler {
|
|
reconnecting = false;
|
|
async onConnectionDown(options, _error) {
|
|
if (this.reconnecting)
|
|
return;
|
|
this.setBlazorStatus(false);
|
|
this.reconnecting = true;
|
|
console.debug('Blazor hub connection lost');
|
|
await this.reconnect();
|
|
}
|
|
async reconnect() {
|
|
let delays = [500, 1000, 2000, 4000, 8000, 16000, 20000];
|
|
let i = 0;
|
|
const lastDelay = delays.length - 1;
|
|
while (true) {
|
|
await this.delay(delays[i]);
|
|
try {
|
|
if (await Blazor.reconnect())
|
|
break;
|
|
|
|
this.setBlazorStatus(false);
|
|
console.warn('Error while reconnecting to Blazor hub (Broken circuit)');
|
|
}
|
|
catch (err) {
|
|
this.setBlazorStatus(false);
|
|
console.warn(`Error while reconnecting to Blazor hub (${err})`);
|
|
}
|
|
i++;
|
|
if (i > lastDelay)
|
|
i = lastDelay;
|
|
}
|
|
}
|
|
onConnectionUp() {
|
|
this.reconnecting = false;
|
|
console.debug('Blazor hub connected');
|
|
this.setBlazorStatus(true);
|
|
}
|
|
|
|
setBlazorStatus(isConnected) {
|
|
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('btcpay-status--' + (isConnected ? 'enabled' : 'disabled'));
|
|
$title.textContent = isConnected ? 'Connection established' : 'Connection interrupted';
|
|
$body.innerHTML = isConnected ? '' : 'Please <a href="">refresh the page</a>.'; // use empty link on purpose
|
|
$body.classList.toggle('d-none', isConnected);
|
|
if (!isUnloading) {
|
|
const toast = new bootstrap.Toast($status, { autohide: false });
|
|
if (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(true);
|
|
Blazor.start({
|
|
reconnectionHandler: handler
|
|
});
|
|
}
|