diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 39d8bf0fe..10a8bce74 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -8,12 +8,16 @@ + + + + diff --git a/BTCPayServer/Views/Invoice/Checkout.cshtml b/BTCPayServer/Views/Invoice/Checkout.cshtml index 817f2dcad..5ee22c5a2 100644 --- a/BTCPayServer/Views/Invoice/Checkout.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout.cshtml @@ -607,12 +607,27 @@ - + @* Not working because of nsSeparator: false, keySeparator: false, {{$t("nested.lang")}} >> *@ - English - Deutsch + + English + Deutsch + + diff --git a/BTCPayServer/bundleconfig.json b/BTCPayServer/bundleconfig.json index 64c877572..f9742337d 100644 --- a/BTCPayServer/bundleconfig.json +++ b/BTCPayServer/bundleconfig.json @@ -33,7 +33,8 @@ "inputFiles": [ "wwwroot/vendor/font-awesome/css/font-awesome.css", "wwwroot/css/css.css", - "wwwroot/css/normalizer.css" + "wwwroot/css/normalizer.css", + "wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css" ] }, { @@ -46,7 +47,8 @@ "wwwroot/vendor/i18next/i18next.js", "wwwroot/vendor/i18next/vue-i18next.js", "wwwroot/js/checkout/i18n-resources.js", - "wwwroot/js/checkout/core.js" + "wwwroot/js/checkout/core.js", + "wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js" ] } ] diff --git a/BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js b/BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js new file mode 100644 index 000000000..f5c59ca72 --- /dev/null +++ b/BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js @@ -0,0 +1,490 @@ +/*! + * jQuery Pretty Dropdowns Plugin v4.11.0 by T. H. Doan (http://thdoan.github.io/pretty-dropdowns/) + * + * jQuery Pretty Dropdowns by T. H. Doan is licensed under the MIT License. + * Read a copy of the license in the LICENSE file or at + * http://choosealicense.com/licenses/mit + */ + +(function($) { + $.fn.prettyDropdown = function(oOptions) { + + // Default options + oOptions = $.extend({ + classic: false, + customClass: 'arrow', + height: 50, + hoverIntent: 200, + multiDelimiter: '; ', + multiVerbosity: 99, + selectedMarker: '✓', + reverse: false, + afterLoad: function(){} + }, oOptions); + + oOptions.selectedMarker = ' ' + oOptions.selectedMarker + ''; + // Validate options + if (isNaN(oOptions.height) || oOptions.height<8) oOptions.height = 8; + if (isNaN(oOptions.hoverIntent) || oOptions.hoverIntent<0) oOptions.hoverIntent = 200; + if (isNaN(oOptions.multiVerbosity)) oOptions.multiVerbosity = 99; + + // Translatable strings + var MULTI_NONE = 'None selected', + MULTI_PREFIX = 'Selected: ', + MULTI_POSTFIX = ' selected'; + + // Globals + var $current, + aKeys = [ + '0','1','2','3','4','5','6','7','8','9',,,,,,,, + 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z' + ], + nCount, + nHoverIndex, + nLastIndex, + nTimer, + nTimestamp, + + // Initiate pretty drop-downs + init = function(elSel) { + var $select = $(elSel), + nSize = elSel.size, + sId = elSel.name || elSel.id || '', + sLabelId; + // Exit if widget has already been initiated + if ($select.data('loaded')) return; + // Remove 'size' attribute to it doesn't affect vertical alignment + $select.data('size', nSize).removeAttr('size'); + // Set height to reserve space for container + $select.css('visibility', 'hidden').outerHeight(oOptions.height); + nTimestamp = +new Date(); + // Test whether to add 'aria-labelledby' + if (elSel.id) { + // Look for + var $label = $('label[for=' + elSel.id + ']'); + if ($label.length) { + // Add 'id' to if necessary + if ($label.attr('id') && !/^menu\d{13,}$/.test($label.attr('id'))) sLabelId = $label.attr('id'); + else $label.attr('id', (sLabelId = 'menu' + nTimestamp)); + } + } + nCount = 0; + var $items = $('optgroup, option', $select), + $selected = $items.filter(':selected'), + bMultiple = elSel.multiple, + // Height - 2px for borders + sHtml = ''; + if (bMultiple) { + sHtml += renderItem(null, 'selected'); + $items.each(function() { + if (this.selected) { + sHtml += renderItem(this, '', true) + } else { + sHtml += renderItem(this); + } + }); + } else { + if (oOptions.classic) { + $items.each(function() { + sHtml += renderItem(this); + }); + } else { + sHtml += renderItem($selected[0], 'selected'); + $items.filter(':not(:selected)').each(function() { + sHtml += renderItem(this); + }); + } + } + sHtml += ''; + $select.wrap(' has the + // 'multiple' attribute or 'size' attribute with a value larger than 1. To fix this, we + // have to inline the height. + + ((bMultiple || nSize>1) ? ' style="height:' + oOptions.height + 'px;"' : '') + +'>').before(sHtml).data('loaded', true); + var $dropdown = $select.parent().children('ul'), + nWidth = $dropdown.outerWidth(true), + nOuterWidth; + $items = $dropdown.children(); + // Update default selected values for multi-select menu + if (bMultiple) updateSelected($dropdown); + else if (oOptions.classic) $('[data-value="' + $selected.val() + '"]', $dropdown).addClass('selected').append(oOptions.selectedMarker); + // Calculate width if initially hidden + if ($dropdown.width()<=0) { + var $clone = $dropdown.parent().clone().css({ + position: 'absolute', + top: '-100%' + }); + $('body').append($clone); + nWidth = $clone.children('ul').outerWidth(true); + $('li', $clone).width(nWidth); + nOuterWidth = $clone.children('ul').outerWidth(true); + $clone.remove(); + } + // Set dropdown width and event handler + // NOTE: Setting width using width(), then css() because width() only can return a float, + // which can result in a missing right border when there is a scrollbar. + $items.width(nWidth).css('width', $items.css('width')).click(function() { + var $li = $(this), + $selected = $dropdown.children('.selected'); + // Ignore disabled menu + if ($dropdown.parent().hasClass('disabled')) return; + // Only update if not disabled, not a label, and a different value selected + if ($dropdown.hasClass('active') && !$li.hasClass('disabled') && !$li.hasClass('label') && $li.data('value')!==$selected.data('value')) { + // Select highlighted item + if (bMultiple) { + if ($li.children('span.checked').length) $li.children('span.checked').remove(); + else $li.append(oOptions.selectedMarker); + // Sync element + $dropdown.children(':not(.selected)').each(function(nIndex) { + $('optgroup, option', $select).eq(nIndex).prop('selected', $(this).children('span.checked').length>0); + }); + // Update selected values for multi-select menu + updateSelected($dropdown); + } else { + $selected.removeClass('selected').children('span.checked').remove(); + $li.addClass('selected').append(oOptions.selectedMarker); + if (!oOptions.classic) $dropdown.prepend($li); + $dropdown.removeClass('reverse').attr('aria-activedescendant', $li.attr('id')); + if ($selected.data('group') && !oOptions.classic) $dropdown.children('.label').filter(function() { + return $(this).text()===$selected.data('group'); + }).after($selected); + // Sync element + $('optgroup, option', $select).filter(function() { + // NOTE: .data('value') can return numeric, so using == comparison instead. + return this.value==$li.data('value') || this.text===$li.contents().filter(function() { + // Filter out selected marker + return this.nodeType===3; + }).text(); + }).prop('selected', true); + } + $select.trigger('change'); + } + if ($li.hasClass('selected') || !bMultiple) { + $dropdown.toggleClass('active'); + $dropdown.attr('aria-expanded', $dropdown.hasClass('active')); + } + // Try to keep drop-down menu within viewport + if ($dropdown.hasClass('active')) { + // Close any other open menus + if ($('.prettydropdown > ul.active').length>1) resetDropdown($('.prettydropdown > ul.active').not($dropdown)[0]); + var nWinHeight = window.innerHeight, + nMaxHeight, + nOffsetTop = $dropdown.offset().top, + nScrollTop = document.body.scrollTop, + nDropdownHeight = $dropdown.outerHeight(); + if (nSize) { + nMaxHeight = nSize*(oOptions.height-2); + if (nMaxHeight nWinHeight || + oOptions.reverse) { + // Expand to direction that has the most space + if (nOffsetTop - nScrollTop > nWinHeight - (nOffsetTop - nScrollTop + oOptions.height) || + oOptions.reverse) { + $dropdown.addClass('reverse'); + if (!oOptions.classic) $dropdown.append($selected); + if (nOffsetTop-nScrollTop+oOptions.height ul.active, .prettydropdown > ul:focus'); + if (!$dropdown.length) return; + if (e.which===9) { // Tab + resetDropdown($dropdown[0]); + return; + } else { + // Intercept non-Tab keys only + e.preventDefault(); + e.stopPropagation(); + } + var $items = $dropdown.children(), + bOpen = $dropdown.hasClass('active'), + nItemsHeight = $dropdown.height()/(oOptions.height-2), + nItemsPerPage = nItemsHeight%1<0.5 ? Math.floor(nItemsHeight) : Math.ceil(nItemsHeight), + sKey; + nHoverIndex = Math.max(0, $dropdown.children('.hover').index()); + nLastIndex = $items.length-1; + $current = $items.eq(nHoverIndex); + $dropdown.data('lastKeypress', +new Date()); + switch (e.which) { + case 13: // Enter + if (!bOpen) { + $current = $items.filter('.selected'); + toggleHover($current, 1); + } + $current.click(); + break; + case 27: // Esc + if (bOpen) resetDropdown($dropdown[0]); + break; + case 32: // Space + if (bOpen) { + sKey = ' '; + } else { + $current = $items.filter('.selected'); + toggleHover($current, 1); + $current.click(); + } + break; + case 33: // Page Up + if (bOpen) { + toggleHover($current, 0); + toggleHover($items.eq(Math.max(nHoverIndex-nItemsPerPage-1, 0)), 1); + } + break; + case 34: // Page Down + if (bOpen) { + toggleHover($current, 0); + toggleHover($items.eq(Math.min(nHoverIndex+nItemsPerPage-1, nLastIndex)), 1); + } + break; + case 35: // End + if (bOpen) { + toggleHover($current, 0); + toggleHover($items.eq(nLastIndex), 1); + } + break; + case 36: // Home + if (bOpen) { + toggleHover($current, 0); + toggleHover($items.eq(0), 1); + } + break; + case 38: // Up + if (bOpen) { + toggleHover($current, 0); + // If not already key-navigated or first item is selected, cycle to the last item; or + // else select the previous item + toggleHover(nHoverIndex ? $items.eq(nHoverIndex-1) : $items.eq(nLastIndex), 1); + } + break; + case 40: // Down + if (bOpen) { + toggleHover($current, 0); + // If last item is selected, cycle to the first item; or else select the next item + toggleHover(nHoverIndex===nLastIndex ? $items.eq(0) : $items.eq(nHoverIndex+1), 1); + } + break; + default: + if (bOpen) sKey = aKeys[e.which-48]; + } + if (sKey) { // Alphanumeric key pressed + clearTimeout(nTimer); + $dropdown.data('keysPressed', $dropdown.data('keysPressed')===undefined ? sKey : $dropdown.data('keysPressed') + sKey); + nTimer = setTimeout(function() { + $dropdown.removeData('keysPressed'); + // NOTE: Windows keyboard repeat delay is 250-1000 ms. See + // https://technet.microsoft.com/en-us/library/cc978658.aspx + }, 300); + // Build index of matches + var aMatches = [], + nCurrentIndex = $current.index(); + $items.each(function(nIndex) { + if ($(this).text().toLowerCase().indexOf($dropdown.data('keysPressed'))===0) aMatches.push(nIndex); + }); + if (aMatches.length) { + // Cycle through items matching key(s) pressed + for (var i=0; inCurrentIndex) { + toggleHover($items, 0); + toggleHover($items.eq(aMatches[i]), 1); + break; + } + if (i===aMatches.length-1) { + toggleHover($items, 0); + toggleHover($items.eq(aMatches[0]), 1); + } + } + } + } + }, + + // Highlight menu item + hoverDropdownItem = function(e) { + var $dropdown = $(e.currentTarget); + if (e.target.nodeName!=='LI' || !$dropdown.hasClass('active') || new Date()-$dropdown.data('lastKeypress')<200) return; + toggleHover($dropdown.children(), 0, 1); + toggleHover($(e.target), 1, 1); + }, + + // Construct menu item + // elOpt is null for first item in multi-select menus + renderItem = function(elOpt, sClass, bSelected) { + var sGroup = '', + sText = '', + sTitle; + sClass = sClass || ''; + if (elOpt) { + switch (elOpt.nodeName) { + case 'OPTION': + if (elOpt.parentNode.nodeName==='OPTGROUP') sGroup = elOpt.parentNode.getAttribute('label'); + sText = (elOpt.getAttribute('data-prefix') || '') + elOpt.text + (elOpt.getAttribute('data-suffix') || ''); + break; + case 'OPTGROUP': + sClass += ' label'; + sText = elOpt.getAttribute('label'); + break; + } + if (elOpt.disabled || (sGroup && elOpt.parentNode.disabled)) sClass += ' disabled'; + sTitle = elOpt.title; + if (sGroup && !sTitle) sTitle = elOpt.parentNode.title; + } + ++nCount; + return '' + sText + + ((bSelected || sClass==='selected') ? oOptions.selectedMarker : '') + ''; + }, + + // Reset menu state + // @param o Event or Element object + resetDropdown = function(o) { + var $dropdown = $(o.currentTarget||o); + // NOTE: Sometimes it's possible for $dropdown to point to the wrong element when you + // quickly hover over another menu. To prevent this, we need to check for .active as a + // backup and manually reassign $dropdown. This also requires that it's not clicked on + // because in rare cases the reassignment fails and the reverse menu will not get reset. + if (o.type==='mouseleave' && !$dropdown.hasClass('active') && !$dropdown.data('clicked')) $dropdown = $('.prettydropdown > ul.active'); + $dropdown.data('hover', false); + clearTimeout(nTimer); + nTimer = setTimeout(function() { + if ($dropdown.data('hover')) return; + if ($dropdown.hasClass('reverse') && !oOptions.classic) $dropdown.prepend($dropdown.children(':last-child')); + $dropdown.removeClass('active reverse').removeData('clicked').attr('aria-expanded', 'false').css('height', ''); + $dropdown.children().removeClass('hover nohover'); + }, (o.type==='mouseleave' && !$dropdown.data('clicked')) ? oOptions.hoverIntent : 0); + }, + + // Set menu item hover state + // bNoScroll set on hoverDropdownItem() + toggleHover = function($li, bOn, bNoScroll) { + if (bOn) { + $li.removeClass('nohover').addClass('hover'); + if ($li.length===1 && $current && !bNoScroll) { + // Ensure items are always in view + var $dropdown = $li.parent(), + nDropdownHeight = $dropdown.outerHeight(), + nItemOffset = $li.offset().top-$dropdown.offset().top-1; // -1px for top border + if ($li.index()===0) { + $dropdown.scrollTop(0); + } else if ($li.index()===nLastIndex) { + $dropdown.scrollTop($dropdown.children().length*oOptions.height); + } else { + if (nItemOffset+oOptions.height>nDropdownHeight) $dropdown.scrollTop($dropdown.scrollTop()+oOptions.height+nItemOffset-nDropdownHeight); + else if (nItemOffset<0) $dropdown.scrollTop($dropdown.scrollTop()+nItemOffset); + } + } + } else { + $li.removeClass('hover').addClass('nohover'); + } + }, + + // Update selected values for multi-select menu + updateSelected = function($dropdown) { + var $select = $dropdown.parent().children('select'), + aSelected = $('option', $select).map(function() { + if (this.selected) return this.text; + }).get(), + sSelected; + if (oOptions.multiVerbosity>=aSelected.length) sSelected = aSelected.join(oOptions.multiDelimiter) || MULTI_NONE; + else sSelected = aSelected.length + '/' + $('option', $select).length + MULTI_POSTFIX; + if (sSelected) { + var sTitle = ($select.attr('title') ? $select.attr('title') : '') + (aSelected.length ? '\n' + MULTI_PREFIX + aSelected.join(oOptions.multiDelimiter) : ''); + $dropdown.children('.selected').text(sSelected); + $dropdown.attr({ + 'title': sTitle, + 'aria-label': sTitle + }); + } else { + $dropdown.children('.selected').empty(); + $dropdown.attr({ + 'title': $select.attr('title'), + 'aria-label': $select.attr('title') + }); + } + }; + + /** + * Public Functions + */ + + // Resync the menu with to reflect state changes + this.refresh = function(oOptions) { + return this.each(function() { + var $select = $(this); + $select.prevAll('ul').remove(); + $select.unwrap().data('loaded', false); + this.size = $select.data('size'); + init(this); + }); + }; + + return this.each(function() { + init(this); + }); + + }; +}(jQuery)); diff --git a/BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css b/BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css new file mode 100644 index 000000000..00b87a37f --- /dev/null +++ b/BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css @@ -0,0 +1,206 @@ +.prettydropdown { + position: relative; + min-width: 72px; /* 70px + borders */ + display: inline-block; +} + + .prettydropdown.loading { + min-width: 0; + } + + .prettydropdown > ul { + border-radius: 5px; + position: absolute; + top: 0; + left: 0; + background: #fff; + border: 1px solid #a9a9a9; + box-sizing: content-box; + color: #000; + cursor: pointer; + font: normal 18px Calibri, sans-serif; + list-style-type: none; + margin: 0; + padding: 0; + text-align: left; + -webkit-user-select: none; /* Chrome all / Safari all */ + -moz-user-select: none; /* Firefox all */ + -ms-user-select: none; /* IE 10+ */ + user-select: none; /* Likely future */ + z-index: 1; + } + + .prettydropdown.loading > ul { + visibility: hidden; + white-space: nowrap; + } + + .prettydropdown > ul:focus, .prettydropdown:not(.disabled) > ul:hover { + border-color: #7f7f7f; + } + + .prettydropdown:not(.disabled) > ul.active { + width: auto; + max-height: 400px !important; + border-color: #1e90ff; + overflow-x: hidden; + overflow-y: auto; + z-index: 99; + } + + .prettydropdown > ul.active:focus { + outline: none; + } + + .prettydropdown > ul.active.reverse { + top: auto; + bottom: 0; + } + + .prettydropdown > ul > li { + font-size: 14px; + position: relative; + min-width: 70px; + height: 48px; /* 50px - borders */ + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + box-sizing: border-box; + display: none; + line-height: 46px; /* 48px - borders */ + margin: 0; + padding-left: 0.8rem; + } + + .prettydropdown.loading > ul > li { + min-width: 0; + display: block; + padding-right: 0.8rem; + } + + .prettydropdown > ul:not(.active) > li:not(.selected):first-child { + color: transparent; /* Prevent FOUC */ + } + + .prettydropdown > ul > li:first-child, .prettydropdown > ul.active > li { + display: block; + } + + .prettydropdown > ul.active > li:not(.label):hover, .prettydropdown > ul.active > li.hover:not(.label), .prettydropdown > ul.active > li:first-child:hover:after { + background: #1e90ff; + color: #fff; + } + + .prettydropdown > ul.active > li.nohover { + background: inherit !important; + color: inherit !important; + } + + .prettydropdown > ul.active > li.hover:before, .prettydropdown > ul.active > li.nohover:after { + border-top-color: #fff !important; + } + + .prettydropdown > ul.active > li.hover:after, .prettydropdown > ul.active > li.nohover:before { + border-top-color: #1e90ff !important; + } + + .prettydropdown.arrow > ul > li.selected:before, .prettydropdown.arrow > ul > li.selected:after { + position: absolute; + top: 8px; + bottom: 0; + right: 8px; + height: 16px; + border: 8px solid transparent; /* Arrow size */ + box-sizing: border-box; + content: ''; + display: block; + margin: auto; + } + + .prettydropdown.arrow.small > ul > li.selected:before, .prettydropdown.arrow.small > ul > li.selected:after { + top: 4px; + height: 8px; + border-width: 4px; + } + + .prettydropdown.arrow > ul > li.selected:before { + border-top-color: #a9a9a9; /* Arrow color */ + } + + .prettydropdown.arrow > ul > li.selected:after { + top: 4px; /* Chevron thickness */ + border-top-color: #fff; /* Match background colour */ + } + + .prettydropdown.arrow.small > ul > li.selected:after { + top: 2px; /* Chevron thickness */ + } + + .prettydropdown.arrow.triangle > ul > li.selected:after { + content: none; + } + + .prettydropdown > ul:hover > li.selected:before { + border-top-color: #7f7f7f; + } + + .prettydropdown > ul.active > li.selected:before, + .prettydropdown > ul.active > li.selected:after { + border: none; + } + + .prettydropdown > ul:not(.active) > li > span.checked { + display: none; + } + + /* Multi-Select */ + .prettydropdown.multiple > ul > li.selected { + overflow: hidden; + padding-right: 2rem; + text-overflow: ellipsis; + white-space: nowrap; + } + + .prettydropdown > ul > li > span.checked { + clear: both; + float: right; + font-weight: bold; + margin-right: 0.8rem; + } + + /* Option Groups */ + .prettydropdown > ul > li.label { + cursor: default; + font-weight: bold; + } + + .prettydropdown > ul > li.label:first-child, + .prettydropdown.classic > ul > li.label ~ li.selected { + border-top: none; + } + + .prettydropdown > ul > li.label ~ li:not(.label):not(.selected), + .prettydropdown.classic > ul.active > li.label ~ li:not(.label) { + padding-left: 1.6rem; + } + + /* Classic Behavior */ + .prettydropdown.classic > ul:not(.active) > li.selected:not(:first-child) { + position: absolute; + top: 0; + display: block; + } + + /* Disabled */ + .prettydropdown.disabled, .prettydropdown > ul > li.disabled { + opacity: 0.3; + } + + .prettydropdown.disabled > ul > li, .prettydropdown > ul > li.disabled { + cursor: not-allowed; + } + + /* Divider Lines */ + .prettydropdown.multiple > ul > li.selected + li, .prettydropdown.multiple > ul.reverse > li.selected, + .prettydropdown > ul > li.label, .prettydropdown > ul > li.label ~ li.selected { + border-top-color: #dedede; + }