diff --git a/.circleci/config.yml b/.circleci/config.yml index bd0462823..7fa80eb03 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -124,7 +124,6 @@ workflows: filters: branches: only: master - publish: jobs: - amd64: @@ -134,21 +133,22 @@ workflows: ignore: /.*/ # only act on version tags v1.0.0.88 or v1.0.2-1 # OR feature tags like vlndseedbackup + # OR features on specific versions like v1.0.0.88-lndseedbackup-1 tags: - - only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/ + + only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/ - arm32v7: filters: branches: ignore: /.*/ tags: - only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/ + only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/ - arm64v8: filters: branches: ignore: /.*/ tags: - only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/ + only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/ - multiarch: requires: - amd64 @@ -158,4 +158,4 @@ workflows: branches: ignore: /.*/ tags: - only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/ + only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/ diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 000000000..da29d20f2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: File a bug report +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Your BTCPay Environment (please complete the following information):** +- BTCPay Server Version [available in the right bottom corner of footer] +- Deployment Method: [e.g. Docker, Manual, Third-Party-hoist] + - Browser [e.g. chrome, safari] + +**Logs (if applicable)** +Basic logs can be found in Server Settings > Logs. More logs https://docs.btcpayserver.org/Troubleshooting/#2-looking-through-the-logs + +**Setup Parameters** +If you're reporting a deployment issue run `. btcpay-setup.sh -i` and paste your the parameters by obscuring private information. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index f9c04659e..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: Report a problem -about: File a technical problem or report a bug ---- - -**Describe the problem/bug** -A clear and concise description of what the bug is. - -**Your environment** -* Version of BTCPay Server: -* Deployment method: -* Other relevant environment details: - -**Logs (if applicable)** -Basic logs can be found in Server Settings > Logs. - -**Setup Parameters** -If you're reporting a deployment issue run `. btcpay-setup.sh -i` and paste your the paremeters by obscuring private information. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Actual behavior** -Tell us what happens instead - -**Screenshots/Links** -If applicable, add screenshots or links to help explain your problem. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..2741e2c39 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Support Chat + url: https://chat.btcpayserver.org/ + about: Ask general questions and get community support in real-time. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index a8556d6e0..4c3216d81 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,9 @@ --- name: Feature request -about: Ideas and feature requests +about: Suggest a new feature or enhancement +title: '' +labels: '' +assignees: '' --- @@ -10,11 +13,11 @@ A clear and concise description of what the problem is. Ex. I'm always frustrate **Describe the solution you'd like** A clear and concise description of what you want to happen. +**Sketch/Image/Wireframe/Mockup** + If applicable provide examples, wireframes, sketches or images to better explain your idea. + **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. -**Provide examples** -If applicable provide examples, wireframes, sketches or images to better explain your idea. - **Additional context** Add any other context or screenshots about the feature request here. diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index b136b2a06..9988b3065 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -5,7 +5,7 @@ - + diff --git a/BTCPayServer.Common/Altcoins/Liquid/LiquidExtensions.cs b/BTCPayServer.Common/Altcoins/Liquid/LiquidExtensions.cs index c6c23b4b4..d0248ba7e 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/LiquidExtensions.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/LiquidExtensions.cs @@ -6,11 +6,11 @@ namespace BTCPayServer { public static class LiquidExtensions { - public static IEnumerable GetAllElementsSubChains(this BTCPayNetworkProvider networkProvider) + public static IEnumerable GetAllElementsSubChains(this BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider unfilteredNetworkProvider) { var elementsBased = networkProvider.GetAll().OfType(); var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct(); - return networkProvider.GetAll().OfType() + return unfilteredNetworkProvider.GetAll().OfType() .Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode.ToUpperInvariant()); } } diff --git a/BTCPayServer.Common/BTCPayServer.Common.csproj b/BTCPayServer.Common/BTCPayServer.Common.csproj index 086c93d22..23a8e57c7 100644 --- a/BTCPayServer.Common/BTCPayServer.Common.csproj +++ b/BTCPayServer.Common/BTCPayServer.Common.csproj @@ -9,4 +9,4 @@ - + \ No newline at end of file diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj index 608f6d7ea..dc67b52e9 100644 --- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj +++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj @@ -6,7 +6,7 @@ - + diff --git a/BTCPayServer.Rating/Providers/HitBTCRateProvider.cs b/BTCPayServer.Rating/Providers/HitBTCRateProvider.cs new file mode 100644 index 000000000..deaa90a0c --- /dev/null +++ b/BTCPayServer.Rating/Providers/HitBTCRateProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Services.Rates; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Rating +{ + public class HitBTCRateProvider : IRateProvider + { + private readonly HttpClient _httpClient; + public HitBTCRateProvider(HttpClient httpClient) + { + _httpClient = httpClient ?? new HttpClient(); + } + + public async Task GetRatesAsync(CancellationToken cancellationToken) + { + var response = await _httpClient.GetAsync("https://api.hitbtc.com/api/2/public/ticker", cancellationToken); + var jarray = await response.Content.ReadAsAsync(cancellationToken); + return jarray + .Children() + .Where(p => CurrencyPair.TryParse(p["symbol"].Value(), out _)) + .Select(p => new PairRate(CurrencyPair.Parse(p["symbol"].Value()), CreateBidAsk(p))) + .ToArray(); + } + + private BidAsk CreateBidAsk(JObject p) + { + var bid = p["bid"].Value(); + var ask = p["ask"].Value(); + return new BidAsk(bid, ask); + } + } +} diff --git a/BTCPayServer.Rating/Services/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs index a2671f1ba..60ed0af09 100644 --- a/BTCPayServer.Rating/Services/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -90,10 +90,10 @@ namespace BTCPayServer.Services.Rates AddExchangeSharpProviders("binance"); AddExchangeSharpProviders("bittrex"); AddExchangeSharpProviders("poloniex"); - AddExchangeSharpProviders("hitbtc"); AddExchangeSharpProviders("ndax"); // Handmade providers + Providers.Add("hitbtc", new HitBTCRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_HITBTC"))); Providers.Add("coingecko", new CoinGeckoRateProvider(_httpClientFactory)); Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") }); Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS"))); diff --git a/BTCPayServer.Tests/PaymentRequestTests.cs b/BTCPayServer.Tests/PaymentRequestTests.cs index 9c2158e98..4ef4f0659 100644 --- a/BTCPayServer.Tests/PaymentRequestTests.cs +++ b/BTCPayServer.Tests/PaymentRequestTests.cs @@ -3,9 +3,13 @@ using System.Linq; using System.Threading.Tasks; using BTCPayServer.Controllers; using BTCPayServer.Models.PaymentRequestViewModels; +using BTCPayServer.PaymentRequest; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Tests.Logging; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using NBitcoin; using NBitpayClient; using Xunit; using Xunit.Abstractions; @@ -16,7 +20,7 @@ namespace BTCPayServer.Tests { public PaymentRequestTests(ITestOutputHelper helper) { - Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; + Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; Logs.LogProvider = new XUnitLogProvider(helper); } @@ -46,8 +50,8 @@ namespace BTCPayServer.Tests Description = "description" }; var id = (Assert - .IsType(await paymentRequestController.EditPaymentRequest(null, request)).RouteValues.Values.First().ToString()); - + .IsType(await paymentRequestController.EditPaymentRequest(null, request)) + .RouteValues.Values.First().ToString()); //permission guard for guests editing @@ -57,7 +61,9 @@ namespace BTCPayServer.Tests request.Title = "update"; Assert.IsType(await paymentRequestController.EditPaymentRequest(id, request)); - Assert.Equal(request.Title, Assert.IsType(Assert.IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Title); + Assert.Equal(request.Title, + Assert.IsType(Assert + .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Title); Assert.False(string.IsNullOrEmpty(id)); @@ -68,16 +74,24 @@ namespace BTCPayServer.Tests Assert .IsType(await paymentRequestController.TogglePaymentRequestArchival(id)); - Assert.True(Assert.IsType(Assert.IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived); + Assert.True(Assert + .IsType(Assert + .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived); - Assert.Empty(Assert.IsType(Assert.IsType(await paymentRequestController.GetPaymentRequests()).Model).Items); + Assert.Empty(Assert + .IsType(Assert + .IsType(await paymentRequestController.GetPaymentRequests()).Model).Items); //unarchive Assert .IsType(await paymentRequestController.TogglePaymentRequestArchival(id)); - Assert.False(Assert.IsType(Assert.IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived); + Assert.False(Assert + .IsType(Assert + .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived); - Assert.Single(Assert.IsType(Assert.IsType(await paymentRequestController.GetPaymentRequests()).Model).Items); + Assert.Single(Assert + .IsType(Assert + .IsType(await paymentRequestController.GetPaymentRequests()).Model).Items); } } @@ -94,7 +108,8 @@ namespace BTCPayServer.Tests var paymentRequestController = user.GetController(); - Assert.IsType(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString())); + Assert.IsType( + await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString())); var request = new UpdatePaymentRequestViewModel() @@ -110,15 +125,18 @@ namespace BTCPayServer.Tests .RouteValues.First(); var invoiceId = Assert - .IsType(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value + .IsType( + await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value .ToString(); var actionResult = Assert - .IsType(await paymentRequestController.PayPaymentRequest(response.Value.ToString())); + .IsType( + await paymentRequestController.PayPaymentRequest(response.Value.ToString())); Assert.Equal("Checkout", actionResult.ActionName); Assert.Equal("Invoice", actionResult.ControllerName); - Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); + Assert.Contains(actionResult.RouteValues, + pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); Assert.Equal(1, invoice.Price); @@ -138,8 +156,8 @@ namespace BTCPayServer.Tests .RouteValues.First(); Assert - .IsType(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)); - + .IsType( + await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)); } } @@ -156,11 +174,9 @@ namespace BTCPayServer.Tests var paymentRequestController = user.GetController(); - Assert.IsType(await paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false)); - var request = new UpdatePaymentRequestViewModel() { Title = "original juice", @@ -176,15 +192,18 @@ namespace BTCPayServer.Tests var paymentRequestId = response.Value.ToString(); var invoiceId = Assert - .IsType(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)).Value + .IsType(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)) + .Value .ToString(); var actionResult = Assert - .IsType(await paymentRequestController.PayPaymentRequest(response.Value.ToString())); + .IsType( + await paymentRequestController.PayPaymentRequest(response.Value.ToString())); Assert.Equal("Checkout", actionResult.ActionName); Assert.Equal("Invoice", actionResult.ControllerName); - Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); + Assert.Contains(actionResult.RouteValues, + pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); Assert.Equal(InvoiceState.ToString(InvoiceStatus.New), invoice.Status); @@ -194,11 +213,24 @@ namespace BTCPayServer.Tests invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); Assert.Equal(InvoiceState.ToString(InvoiceStatus.Invalid), invoice.Status); - Assert.IsType(await paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false)); + invoiceId = Assert + .IsType(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)) + .Value + .ToString(); + invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); + + //a hack to generate invoices for the payment request is to manually create an invocie with an order id that matches: + user.BitPay.CreateInvoice(new Invoice(1, "USD") + { + OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(paymentRequestId) + }); + //shouldnt crash + await paymentRequestController.ViewPaymentRequest(paymentRequestId); + await paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId); } } } diff --git a/BTCPayServer.Tests/README.md b/BTCPayServer.Tests/README.md index 16e31a81b..978327c49 100644 --- a/BTCPayServer.Tests/README.md +++ b/BTCPayServer.Tests/README.md @@ -52,6 +52,9 @@ If you get this message: Please, run the test `CanSetLightningServer`, this will establish a channel between the customer and the merchant, then, retry. +Alternatively you can run the `./docker-lightning-channel-setup.sh` script to establish the channel connection. +The `./docker-lightning-channel-teardown.sh` script closes any existing lightning channels. + ## FAQ `docker-compose up dev` failed or tests are not passing, what should I do? diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 3750381a2..f4d02f370 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -157,7 +157,7 @@ namespace BTCPayServer.Tests { string connectionString = null; if (connectionType == LightningConnectionType.Charge) - connectionString = "type=charge;server=" + Server.MerchantCharge.Client.Uri.AbsoluteUri; + connectionString = $"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true"; else if (connectionType == LightningConnectionType.CLightning) connectionString = "type=clightning;server=" + ((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri; else if (connectionType == LightningConnectionType.LndREST) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index d214abb64..4145dba4f 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -664,6 +664,7 @@ namespace BTCPayServer.Tests Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value")); s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings); + var walletUrl = s.Driver.Url; s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick(); s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click(); @@ -671,7 +672,12 @@ namespace BTCPayServer.Tests // Seed backup page var recoveryPhrase = s.Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic"); Assert.Equal(mnemonic.ToString(), recoveryPhrase); - Assert.Contains("The recovery phrase will also be stored on a server as a hot wallet.", s.Driver.PageSource); + Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.", s.Driver.PageSource); + + // No confirmation, just a link to return to the wallet + Assert.Empty(s.Driver.FindElements(By.Id("confirm"))); + s.Driver.FindElement(By.Id("proceed")).Click(); + Assert.Equal(walletUrl, s.Driver.Url); } } void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false) diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 698122649..b0694e3b8 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -84,7 +84,7 @@ namespace BTCPayServer.Tests var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork; CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc); MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc); - MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc); + MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify;allowinsecure=true", "merchant_lightningd", btc); MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:35531/", "merchant_lnd", btc); PayTester.UseLightning = true; PayTester.IntegratedLightning = MerchantCharge.Client.Uri; diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 6cfc58431..7b3c1d9d8 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -260,7 +260,7 @@ namespace BTCPayServer.Tests if (connectionType == LightningConnectionType.Charge) { if (isMerchant) - connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri; + connectionString = $"type=charge;server={parent.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true"; else throw new NotSupportedException(); } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 248ad9d66..f0eced4c0 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -735,7 +735,7 @@ namespace BTCPayServer.Tests var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel() { - ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri, + ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true", SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :( }, "test", "BTC").GetAwaiter().GetResult(); Assert.False(storeController.TempData.ContainsKey(WellKnownTempData.ErrorMessage)); @@ -745,7 +745,7 @@ namespace BTCPayServer.Tests Assert.IsType(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel() { - ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri + ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true" }, "save", "BTC").GetAwaiter().GetResult()); // Make sure old connection string format does not work diff --git a/BTCPayServer.Tests/docker-compose.altcoins.yml b/BTCPayServer.Tests/docker-compose.altcoins.yml index 326b1e9f2..8435994c2 100644 --- a/BTCPayServer.Tests/docker-compose.altcoins.yml +++ b/BTCPayServer.Tests/docker-compose.altcoins.yml @@ -23,7 +23,7 @@ services: TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none} TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc" TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc" - TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify" + TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true" TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/" TESTS_INCONTAINER: "true" TESTS_SSHCONNECTION: "root@sshd:22" @@ -81,7 +81,7 @@ services: - customer_lnd - merchant_lnd nbxplorer: - image: nicolasdorier/nbxplorer:2.1.35 + image: nicolasdorier/nbxplorer:2.1.40 restart: unless-stopped ports: - "32838:32838" @@ -140,7 +140,7 @@ services: - "bitcoin_datadir:/data" customer_lightningd: - image: btcpayserver/lightning:v0.8.2-dev + image: btcpayserver/lightning:v0.9.0-1-dev stop_signal: SIGKILL restart: unless-stopped environment: @@ -187,7 +187,7 @@ services: - merchant_lightningd merchant_lightningd: - image: btcpayserver/lightning:v0.8.2-dev + image: btcpayserver/lightning:v0.9.0-1-dev stop_signal: SIGKILL environment: EXPOSE_TCP: "true" diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index dc90f8358..e471932a7 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -21,7 +21,7 @@ services: TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none} TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc" TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc" - TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify" + TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true" TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/" TESTS_INCONTAINER: "true" TESTS_SSHCONNECTION: "root@sshd:22" @@ -78,7 +78,7 @@ services: - customer_lnd - merchant_lnd nbxplorer: - image: nicolasdorier/nbxplorer:2.1.37 + image: nicolasdorier/nbxplorer:2.1.40 restart: unless-stopped ports: - "32838:32838" @@ -127,7 +127,7 @@ services: - "bitcoin_datadir:/data" customer_lightningd: - image: btcpayserver/lightning:v0.8.2-dev + image: btcpayserver/lightning:v0.9.0-1-dev stop_signal: SIGKILL restart: unless-stopped environment: @@ -174,7 +174,7 @@ services: - merchant_lightningd merchant_lightningd: - image: btcpayserver/lightning:v0.8.2-dev + image: btcpayserver/lightning:v0.9.0-1-dev stop_signal: SIGKILL environment: EXPOSE_TCP: "true" diff --git a/BTCPayServer.Tests/docker-lightning-channel-setup.sh b/BTCPayServer.Tests/docker-lightning-channel-setup.sh new file mode 100755 index 000000000..562953d03 --- /dev/null +++ b/BTCPayServer.Tests/docker-lightning-channel-setup.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Commands +BCMD=./docker-bitcoin-cli.sh +GCMD=./docker-bitcoin-generate.sh +CCMD=./docker-customer-lightning-cli.sh +MCMD=./docker-merchant-lightning-cli.sh + +function channel_count () { + local cmd=$1; local id=$2; + local count=$($cmd listchannels | jq -r ".channels | map(select(.destination == \"$id\")) | length | tonumber") 2>/dev/null + return $count +} + +function create_channel () { + local cmd=$1; local id=$2; + local btcaddr=$($cmd newaddr | jq -r '.address') + $BCMD sendtoaddress $btcaddr 0.15 >/dev/null + $GCMD 10 >/dev/null + local fundres=$($cmd fundchannel $id 14500000 5000 | jq -r '.channel_id') + $GCMD 20 >/dev/null + sleep 2 + channel_count $cmd $id + local count=$? + return $count +} + +# General information +cinfo=$($CCMD getinfo | jq '.' 2>/dev/null) +minfo=$($MCMD getinfo | jq '.' 2>/dev/null) +cid=$(echo $cinfo | jq -r '.id') +mid=$(echo $minfo | jq -r '.id') +caddr=$(echo $cinfo | jq -r '.address[] | "\(.address):\(.port)"') +maddr=$(echo $minfo | jq -r '.address[] | "\(.address):\(.port)"') + +printf "Customer ID: %s@%s\n\r" $cid $caddr +printf "Merchant ID: %s@%s\n\r" $mid $maddr + +# Connections +printf "\n\rConnecting both parties …\n\r" + +cconnid=$($CCMD connect "$mid@$maddr" | jq -r '.id' 2>/dev/null) +mconnid=$($MCMD connect "$cid@$caddr" | jq -r '.id' 2>/dev/null) + +printf "Customer to merchant %s\n\r" $([[ $cconnid == $mid ]] && echo "succeeded" || echo "failed") +printf "Merchant to customer %s\n\r" $([[ $mconnid == $cid ]] && echo "succeeded" || echo "failed") + +# Channels +printf "\n\rChecking channels …\n\r" +channel_count $CCMD $mid +cchanscount=$? +channel_count $MCMD $cid +mchanscount=$? + +printf "Customer channel count to merchant: %d\n\r" $cchanscount +printf "Merchant channel count to customer: %d\n\r" $mchanscount + +# Open channels if there are none, details: https://github.com/ElementsProject/lightning#opening-a-channel +if [[ $cchanscount -eq 0 ]]; then + create_channel $CCMD $mid + cchanres=$? + printf "Establishing channel from customer to merchant %s\n\r" $([[ $cchanres -gt 0 ]] && echo "succeeded" || echo "failed") +fi + +if [[ $mchanscount -eq 0 ]]; then + create_channel $MCMD $cid + mchanres=$? + printf "Establishing channel from merchant to customer %s\n\r" $([[ $mchanres -gt 0 ]] && echo "succeeded" || echo "failed") +fi diff --git a/BTCPayServer.Tests/docker-lightning-channel-teardown.sh b/BTCPayServer.Tests/docker-lightning-channel-teardown.sh new file mode 100755 index 000000000..c67281a55 --- /dev/null +++ b/BTCPayServer.Tests/docker-lightning-channel-teardown.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +channels=$(./docker-merchant-lightning-cli.sh listchannels | jq -cr '.channels | map(.short_channel_id) | unique') +printf "Channels: %s\n\r" $channels + +for chanid in $(echo "${channels}" | jq -cr '.[]') +do + printf "Closing channel ID: %s\n\r" $chanid + ./docker-merchant-lightning-cli.sh close $chanid + ./docker-bitcoin-generate.sh 20 > /dev/null +done diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 0d0e4a041..134ac4af5 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -1,4 +1,4 @@ - + @@ -46,7 +46,7 @@ - + diff --git a/BTCPayServer/Views/Shared/Components/NotificationsDropdown/Default.cshtml b/BTCPayServer/Components/NotificationsDropdown/Default.cshtml similarity index 56% rename from BTCPayServer/Views/Shared/Components/NotificationsDropdown/Default.cshtml rename to BTCPayServer/Components/NotificationsDropdown/Default.cshtml index 3e8a80093..2671854bb 100644 --- a/BTCPayServer/Views/Shared/Components/NotificationsDropdown/Default.cshtml +++ b/BTCPayServer/Components/NotificationsDropdown/Default.cshtml @@ -1,4 +1,5 @@ -@model BTCPayServer.Services.Notifications.NotificationSummaryViewModel +@inject LinkGenerator linkGenerator +@model BTCPayServer.Components.NotificationsDropdown.NotificationSummaryViewModel @if (Model.UnseenCount > 0) { @@ -31,3 +32,36 @@ else } + diff --git a/BTCPayServer/Components/NotificationsDropdown/NoticationsDropdown.cs b/BTCPayServer/Components/NotificationsDropdown/NoticationsDropdown.cs new file mode 100644 index 000000000..3cff8e623 --- /dev/null +++ b/BTCPayServer/Components/NotificationsDropdown/NoticationsDropdown.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services.Notifications; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Components.NotificationsDropdown +{ + public class NotificationsDropdown : ViewComponent + { + private readonly NotificationManager _notificationManager; + + public NotificationsDropdown(NotificationManager notificationManager) + { + _notificationManager = notificationManager; + } + + public async Task InvokeAsync() + { + return View(await _notificationManager.GetSummaryNotifications(UserClaimsPrincipal)); + } + } +} diff --git a/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs b/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs new file mode 100644 index 000000000..41eaf556c --- /dev/null +++ b/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Models.NotificationViewModels; + +namespace BTCPayServer.Components.NotificationsDropdown +{ + public class NotificationSummaryViewModel + { + public int UnseenCount { get; set; } + public List Last5 { get; set; } + } +} diff --git a/BTCPayServer/Views/Shared/_TableFooterPager.cshtml b/BTCPayServer/Components/Pager/Default.cshtml similarity index 100% rename from BTCPayServer/Views/Shared/_TableFooterPager.cshtml rename to BTCPayServer/Components/Pager/Default.cshtml diff --git a/BTCPayServer/Components/Pager/Pager.cs b/BTCPayServer/Components/Pager/Pager.cs new file mode 100644 index 000000000..9cb40cda5 --- /dev/null +++ b/BTCPayServer/Components/Pager/Pager.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Components +{ + public class Pager : ViewComponent + { + public Pager() + { + } + public IViewComponentResult Invoke(BasePagingViewModel viewModel) + { + return View(viewModel); + } + } +} diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 3cd189a7b..9e94cce2b 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -92,7 +92,7 @@ namespace BTCPayServer.Configuration var networkProvider = new BTCPayNetworkProvider(NetworkType); var filtered = networkProvider.Filter(supportedChains.ToArray()); #if ALTCOINS - supportedChains.AddRange(filtered.GetAllElementsSubChains()); + supportedChains.AddRange(filtered.GetAllElementsSubChains(networkProvider)); #endif #if !ALTCOINS var onlyBTC = supportedChains.Count == 1 && supportedChains.First() == "BTC"; diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 802b47cac..2a8ae27da 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -135,7 +135,7 @@ namespace BTCPayServer.Controllers else { var paymentMethods = invoice.GetBlob(_NetworkProvider).GetPaymentMethods(); - var options = invoice.GetBlob(_NetworkProvider).GetPaymentMethods() + var options = paymentMethods .Select(o => o.GetId()) .Select(o => o.CryptoCode) .Where(o => _NetworkProvider.GetNetwork(o) is BTCPayNetwork n && !n.ReadonlyWallet) @@ -143,14 +143,15 @@ namespace BTCPayServer.Controllers .OrderBy(o => o) .Select(o => new PaymentMethodId(o, PaymentTypes.BTCLike)) .ToList(); - var defaultRefund = invoice.Payments.Select(p => p.GetBlob(_NetworkProvider)) - .Select(p => p.GetPaymentMethodId().CryptoCode) - .FirstOrDefault(); + var defaultRefund = invoice.Payments + .Select(p => p.GetBlob(_NetworkProvider)) + .Select(p => p?.GetPaymentMethodId()) + .FirstOrDefault(p => p != null && p.PaymentType == BitcoinPaymentType.Instance); // TODO: What if no option? var refund = new RefundModel(); refund.Title = "Select a payment method"; refund.AvailablePaymentMethods = new SelectList(options, nameof(PaymentMethodId.CryptoCode), nameof(PaymentMethodId.CryptoCode)); - refund.SelectedPaymentMethod = defaultRefund ?? options.Select(o => o.CryptoCode).First(); + refund.SelectedPaymentMethod = defaultRefund?.ToString() ?? options.Select(o => o.CryptoCode).First(); // Nothing to select, skip to next if (refund.AvailablePaymentMethods.Count() == 1) diff --git a/BTCPayServer/Controllers/NotificationsController.cs b/BTCPayServer/Controllers/NotificationsController.cs index 4a664d9e9..b8c01f687 100644 --- a/BTCPayServer/Controllers/NotificationsController.cs +++ b/BTCPayServer/Controllers/NotificationsController.cs @@ -15,22 +15,6 @@ using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers { - - public class NotificationsDropdown : ViewComponent - { - private readonly NotificationManager _notificationManager; - - public NotificationsDropdown(NotificationManager notificationManager) - { - _notificationManager = notificationManager; - } - - public async Task InvokeAsync(int noOfEmployee) - { - return View(await _notificationManager.GetSummaryNotifications(UserClaimsPrincipal)); - } - } - [BitpayAPIConstraint(false)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Route("[controller]/[action]")] diff --git a/BTCPayServer/Controllers/PaymentRequestController.cs b/BTCPayServer/Controllers/PaymentRequestController.cs index a2491ea61..ebbcd790b 100644 --- a/BTCPayServer/Controllers/PaymentRequestController.cs +++ b/BTCPayServer/Controllers/PaymentRequestController.cs @@ -292,18 +292,21 @@ namespace BTCPayServer.Controllers return NotFound(); } - var invoice = result.Invoices.SingleOrDefault(requestInvoice => + var invoices = result.Invoices.Where(requestInvoice => requestInvoice.Status.Equals(InvoiceState.ToString(InvoiceStatus.New), StringComparison.InvariantCulture) && !requestInvoice.Payments.Any()); - if (invoice == null) + if (!invoices.Any()) { return BadRequest("No unpaid pending invoice to cancel"); } - await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id); - _EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008, - InvoiceEvent.MarkedInvalid)); + foreach (var invoice in invoices) + { + await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id); + _EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008, + InvoiceEvent.MarkedInvalid)); + } if (redirect) { diff --git a/BTCPayServer/Controllers/PullPaymentController.cs b/BTCPayServer/Controllers/PullPaymentController.cs index 034bb5345..fe8bebd5c 100644 --- a/BTCPayServer/Controllers/PullPaymentController.cs +++ b/BTCPayServer/Controllers/PullPaymentController.cs @@ -133,7 +133,7 @@ namespace BTCPayServer.Controllers { TempData.SetStatusMessageModel(new StatusMessageModel() { - Message = $"You posted a claim of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination}, this will get fullfilled later.", + Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting approval.", Severity = StatusMessageModel.StatusSeverity.Success }); } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 6a017b42e..8bb990711 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -285,14 +285,15 @@ namespace BTCPayServer.Controllers return NotFound(); var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin); - if (!viewModel.IsAdmin && admins.Count == 1) + var roles = await _UserManager.GetRolesAsync(user); + var wasAdmin = IsAdmin(roles); + if (!viewModel.IsAdmin && admins.Count == 1 && wasAdmin) { TempData[WellKnownTempData.ErrorMessage] = "This is the only Admin, so their role can't be removed until another Admin is added."; return View(viewModel); // return } - var roles = await _UserManager.GetRolesAsync(user); - if (viewModel.IsAdmin != IsAdmin(roles)) + if (viewModel.IsAdmin != wasAdmin) { if (viewModel.IsAdmin) await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin); @@ -571,15 +572,14 @@ namespace BTCPayServer.Controllers [Route("server/services/{serviceName}/{cryptoCode?}")] public async Task Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null) { - if (!string.IsNullOrEmpty(cryptoCode) && !_dashBoard.IsFullySynched(cryptoCode, out _)) + var service = GetService(serviceName, cryptoCode); + if (service == null) + return NotFound(); + if (!string.IsNullOrEmpty(cryptoCode) && !_dashBoard.IsFullySynched(cryptoCode, out _) && service.Type != ExternalServiceTypes.RPC) { TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched"; return RedirectToAction(nameof(Services)); } - var service = GetService(serviceName, cryptoCode); - if (service == null) - return NotFound(); - try { diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 5d73f04c4..b165add39 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -1160,6 +1160,7 @@ namespace BTCPayServer.Controllers CryptoCode = walletId.CryptoCode, Mnemonic = seed, IsStored = true, + RequireConfirm = false, ReturnUrl = Url.Action(nameof(WalletSettings), new { walletId }) }; return this.RedirectToRecoverySeedBackup(recoveryVm); diff --git a/BTCPayServer/Data/PaymentDataExtensions.cs b/BTCPayServer/Data/PaymentDataExtensions.cs index 3423b88a4..45c267b75 100644 --- a/BTCPayServer/Data/PaymentDataExtensions.cs +++ b/BTCPayServer/Data/PaymentDataExtensions.cs @@ -15,7 +15,7 @@ namespace BTCPayServer.Data PaymentEntity paymentEntity = null; if (network == null) { - paymentEntity = NBitcoin.JsonConverters.Serializer.ToObject(unziped, null); + return null; } else { diff --git a/BTCPayServer/Data/StoreDataExtensions.cs b/BTCPayServer/Data/StoreDataExtensions.cs index 20f67fe1f..c2cde2fe2 100644 --- a/BTCPayServer/Data/StoreDataExtensions.cs +++ b/BTCPayServer/Data/StoreDataExtensions.cs @@ -15,8 +15,7 @@ namespace BTCPayServer.Data public static PaymentMethodId GetDefaultPaymentId(this StoreData storeData, BTCPayNetworkProvider networks) { PaymentMethodId[] paymentMethodIds = storeData.GetEnabledPaymentIds(networks); - - var defaultPaymentId = string.IsNullOrEmpty(storeData.DefaultCrypto) ? null : PaymentMethodId.Parse(storeData.DefaultCrypto); + PaymentMethodId.TryParse(storeData.DefaultCrypto, out var defaultPaymentId); var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ?? paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ?? paymentMethodIds.FirstOrDefault(); @@ -80,7 +79,10 @@ namespace BTCPayServer.Data JObject strategies = JObject.Parse(storeData.DerivationStrategies); foreach (var strat in strategies.Properties()) { - var paymentMethodId = PaymentMethodId.Parse(strat.Name); + if (!PaymentMethodId.TryParse(strat.Name, out var paymentMethodId)) + { + continue; + } var network = networks.GetNetwork(paymentMethodId.CryptoCode); if (network != null) { diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index eb4ad0f44..8be823410 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -117,8 +117,9 @@ namespace BTCPayServer public static IEnumerable GetAllBitcoinPaymentData(this InvoiceEntity invoice) { return invoice.GetPayments() - .Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike) - .Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData()); + .Where(p => p.GetPaymentMethodId()?.PaymentType == PaymentTypes.BTCLike) + .Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData()) + .Where(data => data != null); } public static async Task> GetTransactions(this BTCPayWallet client, uint256[] hashes, bool includeOffchain = false, CancellationToken cts = default(CancellationToken)) @@ -445,6 +446,7 @@ namespace BTCPayServer new KeyValuePair("mnemonic", vm.Mnemonic), new KeyValuePair("passphrase", vm.Passphrase), new KeyValuePair("isStored", vm.IsStored ? "true" : "false"), + new KeyValuePair("requireConfirm", vm.RequireConfirm ? "true" : "false"), new KeyValuePair("returnUrl", vm.ReturnUrl) } }; diff --git a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs index 53ef9d782..f5b9cfea0 100644 --- a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs +++ b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs @@ -33,7 +33,7 @@ namespace BTCPayServer.HostedServices protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) { if (evt is InvoiceEvent invoiceEvent && invoiceEvent.Name == InvoiceEvent.ReceivedPayment && - invoiceEvent.Payment.GetPaymentMethodId().PaymentType == BitcoinPaymentType.Instance && + invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance && invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData) { var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode()); diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index ec7765212..dfe61389c 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -81,6 +81,12 @@ namespace BTCPayServer.Hosting return builtInFactory(context); }; }) + .AddRazorOptions(o => + { + // /Components/{View Component Name}/{View Name}.cshtml + o.ViewLocationFormats.Add("/{0}.cshtml"); + o.PageViewLocationFormats.Add("/{0}.cshtml"); + }) .AddNewtonsoftJson() #if RAZOR_RUNTIME_COMPILE .AddRazorRuntimeCompilation() diff --git a/BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs b/BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs index 545e8dc90..d0b3d89c5 100644 --- a/BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs @@ -55,7 +55,7 @@ namespace BTCPayServer.Models.AppViewModels } public class Contribution { - public PaymentMethodId PaymentMehtodId { get; set; } + public PaymentMethodId PaymentMethodId { get; set; } public decimal Value { get; set; } public decimal CurrencyValue { get; set; } } diff --git a/BTCPayServer/Models/StoreViewModels/RecoverySeedBackupViewModel.cs b/BTCPayServer/Models/StoreViewModels/RecoverySeedBackupViewModel.cs index 265d37f27..a00c7a20f 100644 --- a/BTCPayServer/Models/StoreViewModels/RecoverySeedBackupViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/RecoverySeedBackupViewModel.cs @@ -10,8 +10,9 @@ namespace BTCPayServer.Models.StoreViewModels public string CryptoCode { get; set; } public string Mnemonic { get; set; } public string Passphrase { get; set; } - public bool IsStored { get; set; } public string ReturnUrl { get; set; } + public bool IsStored { get; set; } + public bool RequireConfirm { get; set; } = true; public string[] Words { diff --git a/BTCPayServer/PaymentRequest/PaymentRequestHub.cs b/BTCPayServer/PaymentRequest/PaymentRequestHub.cs index 3f9093160..66ab2ad6c 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestHub.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestHub.cs @@ -159,7 +159,7 @@ namespace BTCPayServer.PaymentRequest { data.GetValue(), invoiceEvent.Payment.GetCryptoCode(), - invoiceEvent.Payment.GetPaymentMethodId().PaymentType.ToString() + invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString() }); } diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index e684fe7f3..da6f7cbaf 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -80,7 +80,8 @@ namespace BTCPayServer.PaymentRequest var paymentStats = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true); var amountDue = blob.Amount - paymentStats.TotalCurrency; - var pendingInvoice = invoices.SingleOrDefault(entity => entity.Status == InvoiceStatus.New); + var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime) + .FirstOrDefault(entity => entity.Status == InvoiceStatus.New); return new ViewPaymentRequestViewModel(pr) { @@ -103,10 +104,16 @@ namespace BTCPayServer.PaymentRequest Currency = entity.ProductInformation.Currency, ExpiryDate = entity.ExpirationTime.DateTime, Status = entity.GetInvoiceState().ToString(), - Payments = entity.GetPayments().Select(paymentEntity => + Payments = entity + .GetPayments() + .Select(paymentEntity => { var paymentData = paymentEntity.GetCryptoPaymentData(); var paymentMethodId = paymentEntity.GetPaymentMethodId(); + if (paymentData is null || paymentMethodId is null) + { + return null; + } string txId = paymentData.GetPaymentId(); string link = GetTransactionLink(paymentMethodId, txId); @@ -117,7 +124,9 @@ namespace BTCPayServer.PaymentRequest Link = link, Id = txId }; - }).ToList() + }) + .Where(payment => payment != null) + .ToList() }).ToList() }; } diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 6f6e470d0..243ab0f55 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -222,7 +222,7 @@ namespace BTCPayServer.Payments.Bitcoin var paymentEntitiesByPrevOut = new Dictionary(); foreach (var payment in invoice.GetPayments(wallet.Network)) { - if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike) + if (payment.GetPaymentMethodId()?.PaymentType != PaymentTypes.BTCLike) continue; var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData(); if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx)) diff --git a/BTCPayServer/Payments/PaymentMethodId.cs b/BTCPayServer/Payments/PaymentMethodId.cs index e136f9b59..77a047af5 100644 --- a/BTCPayServer/Payments/PaymentMethodId.cs +++ b/BTCPayServer/Payments/PaymentMethodId.cs @@ -72,6 +72,7 @@ namespace BTCPayServer.Payments public static bool TryParse(string str, out PaymentMethodId paymentMethodId) { + str ??= ""; paymentMethodId = null; var parts = str.Split('_', StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0 || parts.Length > 2) diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index 384b02a0e..a6fe91f54 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -22,7 +22,7 @@ namespace BTCPayServer.Payments public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { - return ((BTCPayNetwork)network).ToObject(str); + return ((BTCPayNetwork)network)?.ToObject(str); } public override string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData) diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs index 0e6b4044a..9236d39f7 100644 --- a/BTCPayServer/Payments/PaymentTypes.Lightning.cs +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -20,7 +20,7 @@ namespace BTCPayServer.Payments public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { - return ((BTCPayNetwork)network).ToObject(str); + return ((BTCPayNetwork)network)?.ToObject(str); } public override string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData) diff --git a/BTCPayServer/Services/Apps/AppHubStreamer.cs b/BTCPayServer/Services/Apps/AppHubStreamer.cs index e27e953f0..ee43a8255 100644 --- a/BTCPayServer/Services/Apps/AppHubStreamer.cs +++ b/BTCPayServer/Services/Apps/AppHubStreamer.cs @@ -39,7 +39,7 @@ namespace BTCPayServer.Services.Apps { data.GetValue(), invoiceEvent.Payment.GetCryptoCode(), - invoiceEvent.Payment.GetPaymentMethodId().PaymentType.ToString() + invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString() }, cancellationToken); } await InfoUpdated(appId); diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 87c4fe239..2a1f92abd 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -182,7 +182,7 @@ namespace BTCPayServer.Services.Apps }); // Old invoices may have invoices which were not tagged - invoices = invoices.Where(inv => inv.Version < InvoiceEntity.InternalTagSupport_Version || + invoices = invoices.Where(inv => appData.TagAllInvoices || inv.Version < InvoiceEntity.InternalTagSupport_Version || inv.InternalTags.Contains(GetAppInternalTag(appData.Id))).ToArray(); return invoices; } @@ -333,7 +333,7 @@ namespace BTCPayServer.Services.Apps .SelectMany(p => { var contribution = new Contribution(); - contribution.PaymentMehtodId = new PaymentMethodId(p.ProductInformation.Currency, PaymentTypes.BTCLike); + contribution.PaymentMethodId = new PaymentMethodId(p.ProductInformation.Currency, PaymentTypes.BTCLike); contribution.CurrencyValue = p.ProductInformation.Price; contribution.Value = contribution.CurrencyValue; @@ -363,18 +363,18 @@ namespace BTCPayServer.Services.Apps .Select(pay => { var paymentMethodContribution = new Contribution(); - paymentMethodContribution.PaymentMehtodId = pay.GetPaymentMethodId(); + paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId(); paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee; - var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMehtodId).Rate; + var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMethodId).Rate; paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value; return paymentMethodContribution; }) .ToArray(); }) - .GroupBy(p => p.PaymentMehtodId) + .GroupBy(p => p.PaymentMethodId) .ToDictionary(p => p.Key, p => new Contribution() { - PaymentMehtodId = p.Key, + PaymentMethodId = p.Key, Value = p.Select(v => v.Value).Sum(), CurrencyValue = p.Select(v => v.CurrencyValue).Sum() }); diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index d9357924e..81a122a45 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -47,10 +47,8 @@ namespace BTCPayServer.Services.Invoices.Export using StringWriter writer = new StringWriter(); using var csvWriter = new CsvHelper.CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture), true); csvWriter.WriteHeader(); - foreach (var invoice in invoices) - { - csvWriter.WriteRecord(invoice); - } + csvWriter.NextRecord(); + csvWriter.WriteRecords(invoices); csvWriter.Flush(); return writer.ToString(); } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index ca7bb61dc..30948bc94 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -8,6 +8,7 @@ using BTCPayServer.JsonConverters; using BTCPayServer.Models; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; +using Microsoft.AspNetCore.Http.Extensions; using NBitcoin; using NBitcoin.DataEncoders; using NBitpayClient; @@ -266,11 +267,11 @@ namespace BTCPayServer.Services.Invoices #pragma warning disable CS0618 public List GetPayments() { - return Payments?.ToList() ?? new List(); + return Payments?.Where(entity => entity.GetPaymentMethodId() != null).ToList() ?? new List(); } public List GetPayments(string cryptoCode) { - return Payments.Where(p => p.CryptoCode == cryptoCode).ToList(); + return GetPayments().Where(p => p.CryptoCode == cryptoCode).ToList(); } public List GetPayments(BTCPayNetworkBase network) { @@ -299,8 +300,8 @@ namespace BTCPayServer.Services.Invoices private Uri FillPlaceholdersUri(string v) { - var uriStr = (v ?? string.Empty).Replace("{OrderId}", OrderId ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{InvoiceId}", Id ?? "", StringComparison.OrdinalIgnoreCase); + var uriStr = (v ?? string.Empty).Replace("{OrderId}", System.Web.HttpUtility.UrlEncode(OrderId) ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{InvoiceId}", System.Web.HttpUtility.UrlEncode(Id) ?? "", StringComparison.OrdinalIgnoreCase); if (Uri.TryCreate(uriStr, UriKind.Absolute, out var uri) && (uri.Scheme == "http" || uri.Scheme == "https")) return uri; return null; @@ -550,7 +551,10 @@ namespace BTCPayServer.Services.Invoices foreach (var prop in PaymentMethod.Properties()) { var r = serializer.ToObject(prop.Value.ToString()); - var paymentMethodId = PaymentMethodId.Parse(prop.Name); + if (!PaymentMethodId.TryParse(prop.Name, out var paymentMethodId)) + { + continue; + } r.CryptoCode = paymentMethodId.CryptoCode; r.PaymentType = paymentMethodId.PaymentType.ToString(); r.ParentEntity = this; @@ -1005,7 +1009,18 @@ namespace BTCPayServer.Services.Invoices } else { - paymentData = GetPaymentMethodId().PaymentType.DeserializePaymentData(Network, CryptoPaymentData); + var paymentMethodId = GetPaymentMethodId(); + if (paymentMethodId is null) + { + return null; + } + + paymentData = paymentMethodId.PaymentType.DeserializePaymentData(Network, CryptoPaymentData); + if (paymentData is null) + { + return null; + } + paymentData.Network = Network; if (paymentData is BitcoinLikePaymentData bitcoin) { @@ -1050,7 +1065,16 @@ namespace BTCPayServer.Services.Invoices public PaymentMethodId GetPaymentMethodId() { #pragma warning disable CS0618 // Type or member is obsolete - return new PaymentMethodId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(CryptoPaymentDataType)); + PaymentType paymentType; + if (string.IsNullOrEmpty(CryptoPaymentDataType)) + { + paymentType = BitcoinPaymentType.Instance;; + } + else if(!PaymentTypes.TryParse(CryptoPaymentDataType, out paymentType)) + { + return null; + } + return new PaymentMethodId(CryptoCode ?? "BTC", paymentType); #pragma warning restore CS0618 // Type or member is obsolete } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 15aedefcd..674de619a 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -484,6 +484,8 @@ retry: entity.Payments = invoice.Payments.Select(p => { var paymentEntity = p.GetBlob(_Networks); + if (paymentEntity is null) + return null; // PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee. // We want to hide this legacy detail in InvoiceRepository, so we fetch the fee from the PaymentMethod and assign it to the PaymentEntity. if (paymentEntity.Version == 0) @@ -497,6 +499,7 @@ retry: return paymentEntity; }) + .Where(p => p != null) .OrderBy(a => a.ReceivedTime).ToList(); #pragma warning restore CS0618 var state = invoice.GetInvoiceState(); @@ -701,7 +704,7 @@ retry: Accounted = accounted }; - context.Payments.Add(data); + await context.Payments.AddAsync(data); try { diff --git a/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs b/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs index bec68b5f7..df70ac3fe 100644 --- a/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs @@ -20,7 +20,7 @@ namespace BTCPayServer.Services.Notifications.Blobs public override string NotificationType => "payout"; protected override void FillViewModel(PayoutNotification notification, NotificationViewModel vm) { - vm.Body = "A new payout is awaiting for payment"; + vm.Body = "A new payout is awaiting for approval"; vm.ActionLink = _linkGenerator.GetPathByAction(nameof(WalletsController.Payouts), "Wallets", new { walletId = new WalletId(notification.StoreId, notification.PaymentMethod) }, _options.RootPath); diff --git a/BTCPayServer/Services/Notifications/NotificationManager.cs b/BTCPayServer/Services/Notifications/NotificationManager.cs index b3965d524..36379732a 100644 --- a/BTCPayServer/Services/Notifications/NotificationManager.cs +++ b/BTCPayServer/Services/Notifications/NotificationManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using BTCPayServer.Components.NotificationsDropdown; using BTCPayServer.Data; using BTCPayServer.Models.NotificationViewModels; using Microsoft.AspNetCore.Identity; @@ -124,10 +125,4 @@ namespace BTCPayServer.Services.Notifications throw new InvalidOperationException($"No INotificationHandler found for {blobType.Name}"); } } - - public class NotificationSummaryViewModel - { - public int UnseenCount { get; set; } - public List Last5 { get; set; } - } } diff --git a/BTCPayServer/Views/Account/Login.cshtml b/BTCPayServer/Views/Account/Login.cshtml index fa3356075..61d30087f 100644 --- a/BTCPayServer/Views/Account/Login.cshtml +++ b/BTCPayServer/Views/Account/Login.cshtml @@ -29,7 +29,7 @@ @if (env.OnionUrl != null) {
- + Copy Tor URL diff --git a/BTCPayServer/Views/Account/Register.cshtml b/BTCPayServer/Views/Account/Register.cshtml index c50395687..6016cf658 100644 --- a/BTCPayServer/Views/Account/Register.cshtml +++ b/BTCPayServer/Views/Account/Register.cshtml @@ -33,7 +33,7 @@ @if (env.OnionUrl != null) {
- + Copy Tor URL diff --git a/BTCPayServer/Views/Home/RecoverySeedBackup.cshtml b/BTCPayServer/Views/Home/RecoverySeedBackup.cshtml index 519a3e53f..f9f12af33 100644 --- a/BTCPayServer/Views/Home/RecoverySeedBackup.cshtml +++ b/BTCPayServer/Views/Home/RecoverySeedBackup.cshtml @@ -46,7 +46,7 @@ Do not photograph it. Do not store it digitally.

- The recovery phrase will also be stored on a server as a hot wallet. + The recovery phrase will also be stored on the server as a hot wallet.

} else @@ -65,11 +65,18 @@

Please make sure to also write down your passphrase.

}
-
- - - - -
+ @if (Model.RequireConfirm) + { +
+ + + + +
+ } + else + { + Done + }
diff --git a/BTCPayServer/Views/Home/SwaggerDocs.cshtml b/BTCPayServer/Views/Home/SwaggerDocs.cshtml index 094eb643e..dfd5742c5 100644 --- a/BTCPayServer/Views/Home/SwaggerDocs.cshtml +++ b/BTCPayServer/Views/Home/SwaggerDocs.cshtml @@ -8,7 +8,8 @@ - + + ', '
', '', '', '
', '
', ''].join('') : '') + (foreColor ? ['
', '
' + this.lang.color.foreground + '
', '
', '', '
', '
', '
', '', '', '
', // Fix missing Div, Commented to find easily if it's wrong + '
', '
'].join('') : ''), + callback: function callback($dropdown) { + $dropdown.find('.note-holder').each(function (idx, item) { + var $holder = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(item); + $holder.append(_this.ui.palette({ + colors: _this.options.colors, + colorsName: _this.options.colorsName, + eventName: $holder.data('event'), + container: _this.options.container, + tooltip: _this.options.tooltip + }).render()); + }); + /* TODO: do we have to record recent custom colors within cookies? */ + + var customColors = [['#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF']]; + $dropdown.find('.note-holder-custom').each(function (idx, item) { + var $holder = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(item); + $holder.append(_this.ui.palette({ + colors: customColors, + colorsName: customColors, + eventName: $holder.data('event'), + container: _this.options.container, + tooltip: _this.options.tooltip + }).render()); + }); + $dropdown.find('input[type=color]').each(function (idx, item) { + external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(item).change(function () { + var $chip = $dropdown.find('#' + external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(this).data('event')).find('.note-color-btn').first(); + var color = this.value.toUpperCase(); + $chip.css('background-color', color).attr('aria-label', color).attr('data-value', color).attr('data-original-title', color); + $chip.click(); + }); + }); + }, + click: function click(event) { + event.stopPropagation(); + var $parent = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()('.' + className).find('.note-dropdown-menu'); + var $button = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(event.target); + var eventName = $button.data('event'); + var value = $button.attr('data-value'); + + if (eventName === 'openPalette') { + var $picker = $parent.find('#' + value); + var $palette = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()($parent.find('#' + $picker.data('event')).find('.note-color-row')[0]); // Shift palette chips + + var $chip = $palette.find('.note-color-btn').last().detach(); // Set chip attributes + + var color = $picker.val(); + $chip.css('background-color', color).attr('aria-label', color).attr('data-value', color).attr('data-original-title', color); + $palette.prepend($chip); + $picker.click(); + } else { + if (lists.contains(['backColor', 'foreColor'], eventName)) { + var key = eventName === 'backColor' ? 'background-color' : 'color'; + var $color = $button.closest('.note-color').find('.note-recent-color'); + var $currentButton = $button.closest('.note-color').find('.note-current-color-button'); + $color.css(key, value); + $currentButton.attr('data-' + eventName, value); + } + + _this.context.invoke('editor.' + eventName, value); + } + } + })] + }).render(); + } + }, { + key: "addToolbarButtons", + value: function addToolbarButtons() { + var _this2 = this; + + this.context.memo('button.style', function () { + return _this2.ui.buttonGroup([_this2.button({ + className: 'dropdown-toggle', + contents: _this2.ui.dropdownButtonContents(_this2.ui.icon(_this2.options.icons.magic), _this2.options), + tooltip: _this2.lang.style.style, + data: { + toggle: 'dropdown' + } + }), _this2.ui.dropdown({ + className: 'dropdown-style', + items: _this2.options.styleTags, + title: _this2.lang.style.style, + template: function template(item) { + // TBD: need to be simplified + if (typeof item === 'string') { + item = { + tag: item, + title: Object.prototype.hasOwnProperty.call(_this2.lang.style, item) ? _this2.lang.style[item] : item + }; + } + + var tag = item.tag; + var title = item.title; + var style = item.style ? ' style="' + item.style + '" ' : ''; + var className = item.className ? ' class="' + item.className + '"' : ''; + return '<' + tag + style + className + '>' + title + ''; + }, + click: _this2.context.createInvokeHandler('editor.formatBlock') + })]).render(); + }); + + var _loop = function _loop(styleIdx, styleLen) { + var item = _this2.options.styleTags[styleIdx]; + + _this2.context.memo('button.style.' + item, function () { + return _this2.button({ + className: 'note-btn-style-' + item, + contents: '
' + item.toUpperCase() + '
', + tooltip: _this2.lang.style[item], + click: _this2.context.createInvokeHandler('editor.formatBlock') + }).render(); + }); + }; + + for (var styleIdx = 0, styleLen = this.options.styleTags.length; styleIdx < styleLen; styleIdx++) { + _loop(styleIdx, styleLen); + } + + this.context.memo('button.bold', function () { + return _this2.button({ + className: 'note-btn-bold', + contents: _this2.ui.icon(_this2.options.icons.bold), + tooltip: _this2.lang.font.bold + _this2.representShortcut('bold'), + click: _this2.context.createInvokeHandlerAndUpdateState('editor.bold') + }).render(); + }); + this.context.memo('button.italic', function () { + return _this2.button({ + className: 'note-btn-italic', + contents: _this2.ui.icon(_this2.options.icons.italic), + tooltip: _this2.lang.font.italic + _this2.representShortcut('italic'), + click: _this2.context.createInvokeHandlerAndUpdateState('editor.italic') + }).render(); + }); + this.context.memo('button.underline', function () { + return _this2.button({ + className: 'note-btn-underline', + contents: _this2.ui.icon(_this2.options.icons.underline), + tooltip: _this2.lang.font.underline + _this2.representShortcut('underline'), + click: _this2.context.createInvokeHandlerAndUpdateState('editor.underline') + }).render(); + }); + this.context.memo('button.clear', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.eraser), + tooltip: _this2.lang.font.clear + _this2.representShortcut('removeFormat'), + click: _this2.context.createInvokeHandler('editor.removeFormat') + }).render(); + }); + this.context.memo('button.strikethrough', function () { + return _this2.button({ + className: 'note-btn-strikethrough', + contents: _this2.ui.icon(_this2.options.icons.strikethrough), + tooltip: _this2.lang.font.strikethrough + _this2.representShortcut('strikethrough'), + click: _this2.context.createInvokeHandlerAndUpdateState('editor.strikethrough') + }).render(); + }); + this.context.memo('button.superscript', function () { + return _this2.button({ + className: 'note-btn-superscript', + contents: _this2.ui.icon(_this2.options.icons.superscript), + tooltip: _this2.lang.font.superscript, + click: _this2.context.createInvokeHandlerAndUpdateState('editor.superscript') + }).render(); + }); + this.context.memo('button.subscript', function () { + return _this2.button({ + className: 'note-btn-subscript', + contents: _this2.ui.icon(_this2.options.icons.subscript), + tooltip: _this2.lang.font.subscript, + click: _this2.context.createInvokeHandlerAndUpdateState('editor.subscript') + }).render(); + }); + this.context.memo('button.fontname', function () { + var styleInfo = _this2.context.invoke('editor.currentStyle'); + + if (_this2.options.addDefaultFonts) { + // Add 'default' fonts into the fontnames array if not exist + external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.each(styleInfo['font-family'].split(','), function (idx, fontname) { + fontname = fontname.trim().replace(/['"]+/g, ''); + + if (_this2.isFontDeservedToAdd(fontname)) { + if (_this2.options.fontNames.indexOf(fontname) === -1) { + _this2.options.fontNames.push(fontname); + } + } + }); + } + + return _this2.ui.buttonGroup([_this2.button({ + className: 'dropdown-toggle', + contents: _this2.ui.dropdownButtonContents('', _this2.options), + tooltip: _this2.lang.font.name, + data: { + toggle: 'dropdown' + } + }), _this2.ui.dropdownCheck({ + className: 'dropdown-fontname', + checkClassName: _this2.options.icons.menuCheck, + items: _this2.options.fontNames.filter(_this2.isFontInstalled.bind(_this2)), + title: _this2.lang.font.name, + template: function template(item) { + return '' + item + ''; + }, + click: _this2.context.createInvokeHandlerAndUpdateState('editor.fontName') + })]).render(); + }); + this.context.memo('button.fontsize', function () { + return _this2.ui.buttonGroup([_this2.button({ + className: 'dropdown-toggle', + contents: _this2.ui.dropdownButtonContents('', _this2.options), + tooltip: _this2.lang.font.size, + data: { + toggle: 'dropdown' + } + }), _this2.ui.dropdownCheck({ + className: 'dropdown-fontsize', + checkClassName: _this2.options.icons.menuCheck, + items: _this2.options.fontSizes, + title: _this2.lang.font.size, + click: _this2.context.createInvokeHandlerAndUpdateState('editor.fontSize') + })]).render(); + }); + this.context.memo('button.fontsizeunit', function () { + return _this2.ui.buttonGroup([_this2.button({ + className: 'dropdown-toggle', + contents: _this2.ui.dropdownButtonContents('', _this2.options), + tooltip: _this2.lang.font.sizeunit, + data: { + toggle: 'dropdown' + } + }), _this2.ui.dropdownCheck({ + className: 'dropdown-fontsizeunit', + checkClassName: _this2.options.icons.menuCheck, + items: _this2.options.fontSizeUnits, + title: _this2.lang.font.sizeunit, + click: _this2.context.createInvokeHandlerAndUpdateState('editor.fontSizeUnit') + })]).render(); + }); + this.context.memo('button.color', function () { + return _this2.colorPalette('note-color-all', _this2.lang.color.recent, true, true); + }); + this.context.memo('button.forecolor', function () { + return _this2.colorPalette('note-color-fore', _this2.lang.color.foreground, false, true); + }); + this.context.memo('button.backcolor', function () { + return _this2.colorPalette('note-color-back', _this2.lang.color.background, true, false); + }); + this.context.memo('button.ul', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.unorderedlist), + tooltip: _this2.lang.lists.unordered + _this2.representShortcut('insertUnorderedList'), + click: _this2.context.createInvokeHandler('editor.insertUnorderedList') + }).render(); + }); + this.context.memo('button.ol', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.orderedlist), + tooltip: _this2.lang.lists.ordered + _this2.representShortcut('insertOrderedList'), + click: _this2.context.createInvokeHandler('editor.insertOrderedList') + }).render(); + }); + var justifyLeft = this.button({ + contents: this.ui.icon(this.options.icons.alignLeft), + tooltip: this.lang.paragraph.left + this.representShortcut('justifyLeft'), + click: this.context.createInvokeHandler('editor.justifyLeft') + }); + var justifyCenter = this.button({ + contents: this.ui.icon(this.options.icons.alignCenter), + tooltip: this.lang.paragraph.center + this.representShortcut('justifyCenter'), + click: this.context.createInvokeHandler('editor.justifyCenter') + }); + var justifyRight = this.button({ + contents: this.ui.icon(this.options.icons.alignRight), + tooltip: this.lang.paragraph.right + this.representShortcut('justifyRight'), + click: this.context.createInvokeHandler('editor.justifyRight') + }); + var justifyFull = this.button({ + contents: this.ui.icon(this.options.icons.alignJustify), + tooltip: this.lang.paragraph.justify + this.representShortcut('justifyFull'), + click: this.context.createInvokeHandler('editor.justifyFull') + }); + var outdent = this.button({ + contents: this.ui.icon(this.options.icons.outdent), + tooltip: this.lang.paragraph.outdent + this.representShortcut('outdent'), + click: this.context.createInvokeHandler('editor.outdent') + }); + var indent = this.button({ + contents: this.ui.icon(this.options.icons.indent), + tooltip: this.lang.paragraph.indent + this.representShortcut('indent'), + click: this.context.createInvokeHandler('editor.indent') + }); + this.context.memo('button.justifyLeft', func.invoke(justifyLeft, 'render')); + this.context.memo('button.justifyCenter', func.invoke(justifyCenter, 'render')); + this.context.memo('button.justifyRight', func.invoke(justifyRight, 'render')); + this.context.memo('button.justifyFull', func.invoke(justifyFull, 'render')); + this.context.memo('button.outdent', func.invoke(outdent, 'render')); + this.context.memo('button.indent', func.invoke(indent, 'render')); + this.context.memo('button.paragraph', function () { + return _this2.ui.buttonGroup([_this2.button({ + className: 'dropdown-toggle', + contents: _this2.ui.dropdownButtonContents(_this2.ui.icon(_this2.options.icons.alignLeft), _this2.options), + tooltip: _this2.lang.paragraph.paragraph, + data: { + toggle: 'dropdown' + } + }), _this2.ui.dropdown([_this2.ui.buttonGroup({ + className: 'note-align', + children: [justifyLeft, justifyCenter, justifyRight, justifyFull] + }), _this2.ui.buttonGroup({ + className: 'note-list', + children: [outdent, indent] + })])]).render(); + }); + this.context.memo('button.height', function () { + return _this2.ui.buttonGroup([_this2.button({ + className: 'dropdown-toggle', + contents: _this2.ui.dropdownButtonContents(_this2.ui.icon(_this2.options.icons.textHeight), _this2.options), + tooltip: _this2.lang.font.height, + data: { + toggle: 'dropdown' + } + }), _this2.ui.dropdownCheck({ + items: _this2.options.lineHeights, + checkClassName: _this2.options.icons.menuCheck, + className: 'dropdown-line-height', + title: _this2.lang.font.height, + click: _this2.context.createInvokeHandler('editor.lineHeight') + })]).render(); + }); + this.context.memo('button.table', function () { + return _this2.ui.buttonGroup([_this2.button({ + className: 'dropdown-toggle', + contents: _this2.ui.dropdownButtonContents(_this2.ui.icon(_this2.options.icons.table), _this2.options), + tooltip: _this2.lang.table.table, + data: { + toggle: 'dropdown' + } + }), _this2.ui.dropdown({ + title: _this2.lang.table.table, + className: 'note-table', + items: ['
', '
', '
', '
', '
', '
1 x 1
'].join('') + })], { + callback: function callback($node) { + var $catcher = $node.find('.note-dimension-picker-mousecatcher'); + $catcher.css({ + width: _this2.options.insertTableMaxSize.col + 'em', + height: _this2.options.insertTableMaxSize.row + 'em' + }).mousedown(_this2.context.createInvokeHandler('editor.insertTable')).on('mousemove', _this2.tableMoveHandler.bind(_this2)); + } + }).render(); + }); + this.context.memo('button.link', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.link), + tooltip: _this2.lang.link.link + _this2.representShortcut('linkDialog.show'), + click: _this2.context.createInvokeHandler('linkDialog.show') + }).render(); + }); + this.context.memo('button.picture', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.picture), + tooltip: _this2.lang.image.image, + click: _this2.context.createInvokeHandler('imageDialog.show') + }).render(); + }); + this.context.memo('button.video', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.video), + tooltip: _this2.lang.video.video, + click: _this2.context.createInvokeHandler('videoDialog.show') + }).render(); + }); + this.context.memo('button.hr', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.minus), + tooltip: _this2.lang.hr.insert + _this2.representShortcut('insertHorizontalRule'), + click: _this2.context.createInvokeHandler('editor.insertHorizontalRule') + }).render(); + }); + this.context.memo('button.fullscreen', function () { + return _this2.button({ + className: 'btn-fullscreen note-codeview-keep', + contents: _this2.ui.icon(_this2.options.icons.arrowsAlt), + tooltip: _this2.lang.options.fullscreen, + click: _this2.context.createInvokeHandler('fullscreen.toggle') + }).render(); + }); + this.context.memo('button.codeview', function () { + return _this2.button({ + className: 'btn-codeview note-codeview-keep', + contents: _this2.ui.icon(_this2.options.icons.code), + tooltip: _this2.lang.options.codeview, + click: _this2.context.createInvokeHandler('codeview.toggle') + }).render(); + }); + this.context.memo('button.redo', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.redo), + tooltip: _this2.lang.history.redo + _this2.representShortcut('redo'), + click: _this2.context.createInvokeHandler('editor.redo') + }).render(); + }); + this.context.memo('button.undo', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.undo), + tooltip: _this2.lang.history.undo + _this2.representShortcut('undo'), + click: _this2.context.createInvokeHandler('editor.undo') + }).render(); + }); + this.context.memo('button.help', function () { + return _this2.button({ + contents: _this2.ui.icon(_this2.options.icons.question), + tooltip: _this2.lang.options.help, + click: _this2.context.createInvokeHandler('helpDialog.show') + }).render(); + }); + } + /** + * image: [ + * ['imageResize', ['resizeFull', 'resizeHalf', 'resizeQuarter', 'resizeNone']], + * ['float', ['floatLeft', 'floatRight', 'floatNone']], + * ['remove', ['removeMedia']], + * ], + */ + + }, { + key: "addImagePopoverButtons", + value: function addImagePopoverButtons() { + var _this3 = this; + + // Image Size Buttons + this.context.memo('button.resizeFull', function () { + return _this3.button({ + contents: '100%', + tooltip: _this3.lang.image.resizeFull, + click: _this3.context.createInvokeHandler('editor.resize', '1') + }).render(); + }); + this.context.memo('button.resizeHalf', function () { + return _this3.button({ + contents: '50%', + tooltip: _this3.lang.image.resizeHalf, + click: _this3.context.createInvokeHandler('editor.resize', '0.5') + }).render(); + }); + this.context.memo('button.resizeQuarter', function () { + return _this3.button({ + contents: '25%', + tooltip: _this3.lang.image.resizeQuarter, + click: _this3.context.createInvokeHandler('editor.resize', '0.25') + }).render(); + }); + this.context.memo('button.resizeNone', function () { + return _this3.button({ + contents: _this3.ui.icon(_this3.options.icons.rollback), + tooltip: _this3.lang.image.resizeNone, + click: _this3.context.createInvokeHandler('editor.resize', '0') + }).render(); + }); // Float Buttons + + this.context.memo('button.floatLeft', function () { + return _this3.button({ + contents: _this3.ui.icon(_this3.options.icons.floatLeft), + tooltip: _this3.lang.image.floatLeft, + click: _this3.context.createInvokeHandler('editor.floatMe', 'left') + }).render(); + }); + this.context.memo('button.floatRight', function () { + return _this3.button({ + contents: _this3.ui.icon(_this3.options.icons.floatRight), + tooltip: _this3.lang.image.floatRight, + click: _this3.context.createInvokeHandler('editor.floatMe', 'right') + }).render(); + }); + this.context.memo('button.floatNone', function () { + return _this3.button({ + contents: _this3.ui.icon(_this3.options.icons.rollback), + tooltip: _this3.lang.image.floatNone, + click: _this3.context.createInvokeHandler('editor.floatMe', 'none') + }).render(); + }); // Remove Buttons + + this.context.memo('button.removeMedia', function () { + return _this3.button({ + contents: _this3.ui.icon(_this3.options.icons.trash), + tooltip: _this3.lang.image.remove, + click: _this3.context.createInvokeHandler('editor.removeMedia') + }).render(); + }); + } + }, { + key: "addLinkPopoverButtons", + value: function addLinkPopoverButtons() { + var _this4 = this; + + this.context.memo('button.linkDialogShow', function () { + return _this4.button({ + contents: _this4.ui.icon(_this4.options.icons.link), + tooltip: _this4.lang.link.edit, + click: _this4.context.createInvokeHandler('linkDialog.show') + }).render(); + }); + this.context.memo('button.unlink', function () { + return _this4.button({ + contents: _this4.ui.icon(_this4.options.icons.unlink), + tooltip: _this4.lang.link.unlink, + click: _this4.context.createInvokeHandler('editor.unlink') + }).render(); + }); + } + /** + * table : [ + * ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']], + * ['delete', ['deleteRow', 'deleteCol', 'deleteTable']] + * ], + */ + + }, { + key: "addTablePopoverButtons", + value: function addTablePopoverButtons() { + var _this5 = this; + + this.context.memo('button.addRowUp', function () { + return _this5.button({ + className: 'btn-md', + contents: _this5.ui.icon(_this5.options.icons.rowAbove), + tooltip: _this5.lang.table.addRowAbove, + click: _this5.context.createInvokeHandler('editor.addRow', 'top') + }).render(); + }); + this.context.memo('button.addRowDown', function () { + return _this5.button({ + className: 'btn-md', + contents: _this5.ui.icon(_this5.options.icons.rowBelow), + tooltip: _this5.lang.table.addRowBelow, + click: _this5.context.createInvokeHandler('editor.addRow', 'bottom') + }).render(); + }); + this.context.memo('button.addColLeft', function () { + return _this5.button({ + className: 'btn-md', + contents: _this5.ui.icon(_this5.options.icons.colBefore), + tooltip: _this5.lang.table.addColLeft, + click: _this5.context.createInvokeHandler('editor.addCol', 'left') + }).render(); + }); + this.context.memo('button.addColRight', function () { + return _this5.button({ + className: 'btn-md', + contents: _this5.ui.icon(_this5.options.icons.colAfter), + tooltip: _this5.lang.table.addColRight, + click: _this5.context.createInvokeHandler('editor.addCol', 'right') + }).render(); + }); + this.context.memo('button.deleteRow', function () { + return _this5.button({ + className: 'btn-md', + contents: _this5.ui.icon(_this5.options.icons.rowRemove), + tooltip: _this5.lang.table.delRow, + click: _this5.context.createInvokeHandler('editor.deleteRow') + }).render(); + }); + this.context.memo('button.deleteCol', function () { + return _this5.button({ + className: 'btn-md', + contents: _this5.ui.icon(_this5.options.icons.colRemove), + tooltip: _this5.lang.table.delCol, + click: _this5.context.createInvokeHandler('editor.deleteCol') + }).render(); + }); + this.context.memo('button.deleteTable', function () { + return _this5.button({ + className: 'btn-md', + contents: _this5.ui.icon(_this5.options.icons.trash), + tooltip: _this5.lang.table.delTable, + click: _this5.context.createInvokeHandler('editor.deleteTable') + }).render(); + }); + } + }, { + key: "build", + value: function build($container, groups) { + for (var groupIdx = 0, groupLen = groups.length; groupIdx < groupLen; groupIdx++) { + var group = groups[groupIdx]; + var groupName = Array.isArray(group) ? group[0] : group; + var buttons = Array.isArray(group) ? group.length === 1 ? [group[0]] : group[1] : [group]; + var $group = this.ui.buttonGroup({ + className: 'note-' + groupName + }).render(); + + for (var idx = 0, len = buttons.length; idx < len; idx++) { + var btn = this.context.memo('button.' + buttons[idx]); + + if (btn) { + $group.append(typeof btn === 'function' ? btn(this.context) : btn); + } + } + + $group.appendTo($container); + } + } + /** + * @param {jQuery} [$container] + */ + + }, { + key: "updateCurrentStyle", + value: function updateCurrentStyle($container) { + var _this6 = this; + + var $cont = $container || this.$toolbar; + var styleInfo = this.context.invoke('editor.currentStyle'); + this.updateBtnStates($cont, { + '.note-btn-bold': function noteBtnBold() { + return styleInfo['font-bold'] === 'bold'; + }, + '.note-btn-italic': function noteBtnItalic() { + return styleInfo['font-italic'] === 'italic'; + }, + '.note-btn-underline': function noteBtnUnderline() { + return styleInfo['font-underline'] === 'underline'; + }, + '.note-btn-subscript': function noteBtnSubscript() { + return styleInfo['font-subscript'] === 'subscript'; + }, + '.note-btn-superscript': function noteBtnSuperscript() { + return styleInfo['font-superscript'] === 'superscript'; + }, + '.note-btn-strikethrough': function noteBtnStrikethrough() { + return styleInfo['font-strikethrough'] === 'strikethrough'; + } + }); + + if (styleInfo['font-family']) { + var fontNames = styleInfo['font-family'].split(',').map(function (name) { + return name.replace(/[\'\"]/g, '').replace(/\s+$/, '').replace(/^\s+/, ''); + }); + var fontName = lists.find(fontNames, this.isFontInstalled.bind(this)); + $cont.find('.dropdown-fontname a').each(function (idx, item) { + var $item = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(item); // always compare string to avoid creating another func. + + var isChecked = $item.data('value') + '' === fontName + ''; + $item.toggleClass('checked', isChecked); + }); + $cont.find('.note-current-fontname').text(fontName).css('font-family', fontName); + } + + if (styleInfo['font-size']) { + var fontSize = styleInfo['font-size']; + $cont.find('.dropdown-fontsize a').each(function (idx, item) { + var $item = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(item); // always compare with string to avoid creating another func. + + var isChecked = $item.data('value') + '' === fontSize + ''; + $item.toggleClass('checked', isChecked); + }); + $cont.find('.note-current-fontsize').text(fontSize); + var fontSizeUnit = styleInfo['font-size-unit']; + $cont.find('.dropdown-fontsizeunit a').each(function (idx, item) { + var $item = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(item); + var isChecked = $item.data('value') + '' === fontSizeUnit + ''; + $item.toggleClass('checked', isChecked); + }); + $cont.find('.note-current-fontsizeunit').text(fontSizeUnit); + } + + if (styleInfo['line-height']) { + var lineHeight = styleInfo['line-height']; + $cont.find('.dropdown-line-height li a').each(function (idx, item) { + // always compare with string to avoid creating another func. + var isChecked = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(item).data('value') + '' === lineHeight + ''; + _this6.className = isChecked ? 'checked' : ''; + }); + } + } + }, { + key: "updateBtnStates", + value: function updateBtnStates($container, infos) { + var _this7 = this; + + external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.each(infos, function (selector, pred) { + _this7.ui.toggleBtnActive($container.find(selector), pred()); + }); + } + }, { + key: "tableMoveHandler", + value: function tableMoveHandler(event) { + var PX_PER_EM = 18; + var $picker = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(event.target.parentNode); // target is mousecatcher + + var $dimensionDisplay = $picker.next(); + var $catcher = $picker.find('.note-dimension-picker-mousecatcher'); + var $highlighted = $picker.find('.note-dimension-picker-highlighted'); + var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted'); + var posOffset; // HTML5 with jQuery - e.offsetX is undefined in Firefox + + if (event.offsetX === undefined) { + var posCatcher = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(event.target).offset(); + posOffset = { + x: event.pageX - posCatcher.left, + y: event.pageY - posCatcher.top + }; + } else { + posOffset = { + x: event.offsetX, + y: event.offsetY + }; + } + + var dim = { + c: Math.ceil(posOffset.x / PX_PER_EM) || 1, + r: Math.ceil(posOffset.y / PX_PER_EM) || 1 + }; + $highlighted.css({ + width: dim.c + 'em', + height: dim.r + 'em' + }); + $catcher.data('value', dim.c + 'x' + dim.r); + + if (dim.c > 3 && dim.c < this.options.insertTableMaxSize.col) { + $unhighlighted.css({ + width: dim.c + 1 + 'em' + }); + } + + if (dim.r > 3 && dim.r < this.options.insertTableMaxSize.row) { + $unhighlighted.css({ + height: dim.r + 1 + 'em' + }); + } + + $dimensionDisplay.html(dim.c + ' x ' + dim.r); + } + }]); + + return Buttons; +}(); + + +// CONCATENATED MODULE: ./src/js/base/module/Toolbar.js +function Toolbar_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function Toolbar_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function Toolbar_createClass(Constructor, protoProps, staticProps) { if (protoProps) Toolbar_defineProperties(Constructor.prototype, protoProps); if (staticProps) Toolbar_defineProperties(Constructor, staticProps); return Constructor; } + + + +var Toolbar_Toolbar = /*#__PURE__*/function () { + function Toolbar(context) { + Toolbar_classCallCheck(this, Toolbar); + + this.context = context; + this.$window = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(window); + this.$document = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(document); + this.ui = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.summernote.ui; + this.$note = context.layoutInfo.note; + this.$editor = context.layoutInfo.editor; + this.$toolbar = context.layoutInfo.toolbar; + this.$editable = context.layoutInfo.editable; + this.$statusbar = context.layoutInfo.statusbar; + this.options = context.options; + this.isFollowing = false; + this.followScroll = this.followScroll.bind(this); + } + + Toolbar_createClass(Toolbar, [{ + key: "shouldInitialize", + value: function shouldInitialize() { + return !this.options.airMode; + } + }, { + key: "initialize", + value: function initialize() { + var _this = this; + + this.options.toolbar = this.options.toolbar || []; + + if (!this.options.toolbar.length) { + this.$toolbar.hide(); + } else { + this.context.invoke('buttons.build', this.$toolbar, this.options.toolbar); + } + + if (this.options.toolbarContainer) { + this.$toolbar.appendTo(this.options.toolbarContainer); + } + + this.changeContainer(false); + this.$note.on('summernote.keyup summernote.mouseup summernote.change', function () { + _this.context.invoke('buttons.updateCurrentStyle'); + }); + this.context.invoke('buttons.updateCurrentStyle'); + + if (this.options.followingToolbar) { + this.$window.on('scroll resize', this.followScroll); + } + } + }, { + key: "destroy", + value: function destroy() { + this.$toolbar.children().remove(); + + if (this.options.followingToolbar) { + this.$window.off('scroll resize', this.followScroll); + } + } + }, { + key: "followScroll", + value: function followScroll() { + if (this.$editor.hasClass('fullscreen')) { + return false; + } + + var editorHeight = this.$editor.outerHeight(); + var editorWidth = this.$editor.width(); + var toolbarHeight = this.$toolbar.height(); + var statusbarHeight = this.$statusbar.height(); // check if the web app is currently using another static bar + + var otherBarHeight = 0; + + if (this.options.otherStaticBar) { + otherBarHeight = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(this.options.otherStaticBar).outerHeight(); + } + + var currentOffset = this.$document.scrollTop(); + var editorOffsetTop = this.$editor.offset().top; + var editorOffsetBottom = editorOffsetTop + editorHeight; + var activateOffset = editorOffsetTop - otherBarHeight; + var deactivateOffsetBottom = editorOffsetBottom - otherBarHeight - toolbarHeight - statusbarHeight; + + if (!this.isFollowing && currentOffset > activateOffset && currentOffset < deactivateOffsetBottom - toolbarHeight) { + this.isFollowing = true; + this.$editable.css({ + marginTop: this.$toolbar.outerHeight() + }); + this.$toolbar.css({ + position: 'fixed', + top: otherBarHeight, + width: editorWidth, + zIndex: 1000 + }); + } else if (this.isFollowing && (currentOffset < activateOffset || currentOffset > deactivateOffsetBottom)) { + this.isFollowing = false; + this.$toolbar.css({ + position: 'relative', + top: 0, + width: '100%', + zIndex: 'auto' + }); + this.$editable.css({ + marginTop: '' + }); + } + } + }, { + key: "changeContainer", + value: function changeContainer(isFullscreen) { + if (isFullscreen) { + this.$toolbar.prependTo(this.$editor); + } else { + if (this.options.toolbarContainer) { + this.$toolbar.appendTo(this.options.toolbarContainer); + } + } + + if (this.options.followingToolbar) { + this.followScroll(); + } + } + }, { + key: "updateFullscreen", + value: function updateFullscreen(isFullscreen) { + this.ui.toggleBtnActive(this.$toolbar.find('.btn-fullscreen'), isFullscreen); + this.changeContainer(isFullscreen); + } + }, { + key: "updateCodeview", + value: function updateCodeview(isCodeview) { + this.ui.toggleBtnActive(this.$toolbar.find('.btn-codeview'), isCodeview); + + if (isCodeview) { + this.deactivate(); + } else { + this.activate(); + } + } + }, { + key: "activate", + value: function activate(isIncludeCodeview) { + var $btn = this.$toolbar.find('button'); + + if (!isIncludeCodeview) { + $btn = $btn.not('.note-codeview-keep'); + } + + this.ui.toggleBtn($btn, true); + } + }, { + key: "deactivate", + value: function deactivate(isIncludeCodeview) { + var $btn = this.$toolbar.find('button'); + + if (!isIncludeCodeview) { + $btn = $btn.not('.note-codeview-keep'); + } + + this.ui.toggleBtn($btn, false); + } + }]); + + return Toolbar; +}(); + + +// CONCATENATED MODULE: ./src/js/base/module/LinkDialog.js +function LinkDialog_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function LinkDialog_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function LinkDialog_createClass(Constructor, protoProps, staticProps) { if (protoProps) LinkDialog_defineProperties(Constructor.prototype, protoProps); if (staticProps) LinkDialog_defineProperties(Constructor, staticProps); return Constructor; } + + + + + + +var LinkDialog_LinkDialog = /*#__PURE__*/function () { + function LinkDialog(context) { + LinkDialog_classCallCheck(this, LinkDialog); + + this.context = context; + this.ui = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.summernote.ui; + this.$body = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(document.body); + this.$editor = context.layoutInfo.editor; + this.options = context.options; + this.lang = this.options.langInfo; + context.memo('help.linkDialog.show', this.options.langInfo.help['linkDialog.show']); + } + + LinkDialog_createClass(LinkDialog, [{ + key: "initialize", + value: function initialize() { + var $container = this.options.dialogsInBody ? this.$body : this.options.container; + var body = ['
', ""), ""), '
', '
', ""), ""), '
', !this.options.disableLinkTarget ? external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()('
').append(this.ui.checkbox({ + className: 'sn-checkbox-open-in-new-window', + text: this.lang.link.openInNewWindow, + checked: true + }).render()).html() : '', external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()('
').append(this.ui.checkbox({ + className: 'sn-checkbox-use-protocol', + text: this.lang.link.useProtocol, + checked: true + }).render()).html()].join(''); + var buttonClass = 'btn btn-primary note-btn note-btn-primary note-link-btn'; + var footer = ""); + this.$dialog = this.ui.dialog({ + className: 'link-dialog', + title: this.lang.link.insert, + fade: this.options.dialogsFade, + body: body, + footer: footer + }).render().appendTo($container); + } + }, { + key: "destroy", + value: function destroy() { + this.ui.hideDialog(this.$dialog); + this.$dialog.remove(); + } + }, { + key: "bindEnterKey", + value: function bindEnterKey($input, $btn) { + $input.on('keypress', function (event) { + if (event.keyCode === core_key.code.ENTER) { + event.preventDefault(); + $btn.trigger('click'); + } + }); + } + /** + * toggle update button + */ + + }, { + key: "toggleLinkBtn", + value: function toggleLinkBtn($linkBtn, $linkText, $linkUrl) { + this.ui.toggleBtn($linkBtn, $linkText.val() && $linkUrl.val()); + } + /** + * Show link dialog and set event handlers on dialog controls. + * + * @param {Object} linkInfo + * @return {Promise} + */ + + }, { + key: "showLinkDialog", + value: function showLinkDialog(linkInfo) { + var _this = this; + + return external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.Deferred(function (deferred) { + var $linkText = _this.$dialog.find('.note-link-text'); + + var $linkUrl = _this.$dialog.find('.note-link-url'); + + var $linkBtn = _this.$dialog.find('.note-link-btn'); + + var $openInNewWindow = _this.$dialog.find('.sn-checkbox-open-in-new-window input[type=checkbox]'); + + var $useProtocol = _this.$dialog.find('.sn-checkbox-use-protocol input[type=checkbox]'); + + _this.ui.onDialogShown(_this.$dialog, function () { + _this.context.triggerEvent('dialog.shown'); // If no url was given and given text is valid URL then copy that into URL Field + + + if (!linkInfo.url && func.isValidUrl(linkInfo.text)) { + linkInfo.url = linkInfo.text; + } + + $linkText.on('input paste propertychange', function () { + // If linktext was modified by input events, + // cloning text from linkUrl will be stopped. + linkInfo.text = $linkText.val(); + + _this.toggleLinkBtn($linkBtn, $linkText, $linkUrl); + }).val(linkInfo.text); + $linkUrl.on('input paste propertychange', function () { + // Display same text on `Text to display` as default + // when linktext has no text + if (!linkInfo.text) { + $linkText.val($linkUrl.val()); + } + + _this.toggleLinkBtn($linkBtn, $linkText, $linkUrl); + }).val(linkInfo.url); + + if (!env.isSupportTouch) { + $linkUrl.trigger('focus'); + } + + _this.toggleLinkBtn($linkBtn, $linkText, $linkUrl); + + _this.bindEnterKey($linkUrl, $linkBtn); + + _this.bindEnterKey($linkText, $linkBtn); + + var isNewWindowChecked = linkInfo.isNewWindow !== undefined ? linkInfo.isNewWindow : _this.context.options.linkTargetBlank; + $openInNewWindow.prop('checked', isNewWindowChecked); + var useProtocolChecked = linkInfo.url ? false : _this.context.options.useProtocol; + $useProtocol.prop('checked', useProtocolChecked); + $linkBtn.one('click', function (event) { + event.preventDefault(); + deferred.resolve({ + range: linkInfo.range, + url: $linkUrl.val(), + text: $linkText.val(), + isNewWindow: $openInNewWindow.is(':checked'), + checkProtocol: $useProtocol.is(':checked') + }); + + _this.ui.hideDialog(_this.$dialog); + }); + }); + + _this.ui.onDialogHidden(_this.$dialog, function () { + // detach events + $linkText.off(); + $linkUrl.off(); + $linkBtn.off(); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }); + + _this.ui.showDialog(_this.$dialog); + }).promise(); + } + /** + * @param {Object} layoutInfo + */ + + }, { + key: "show", + value: function show() { + var _this2 = this; + + var linkInfo = this.context.invoke('editor.getLinkInfo'); + this.context.invoke('editor.saveRange'); + this.showLinkDialog(linkInfo).then(function (linkInfo) { + _this2.context.invoke('editor.restoreRange'); + + _this2.context.invoke('editor.createLink', linkInfo); + }).fail(function () { + _this2.context.invoke('editor.restoreRange'); + }); + } + }]); + + return LinkDialog; +}(); + + +// CONCATENATED MODULE: ./src/js/base/module/LinkPopover.js +function LinkPopover_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function LinkPopover_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function LinkPopover_createClass(Constructor, protoProps, staticProps) { if (protoProps) LinkPopover_defineProperties(Constructor.prototype, protoProps); if (staticProps) LinkPopover_defineProperties(Constructor, staticProps); return Constructor; } + + + + + +var LinkPopover_LinkPopover = /*#__PURE__*/function () { + function LinkPopover(context) { + var _this = this; + + LinkPopover_classCallCheck(this, LinkPopover); + + this.context = context; + this.ui = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.summernote.ui; + this.options = context.options; + this.events = { + 'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function summernoteKeyupSummernoteMouseupSummernoteChangeSummernoteScroll() { + _this.update(); + }, + 'summernote.disable summernote.dialog.shown summernote.blur': function summernoteDisableSummernoteDialogShownSummernoteBlur() { + _this.hide(); + } + }; + } + + LinkPopover_createClass(LinkPopover, [{ + key: "shouldInitialize", + value: function shouldInitialize() { + return !lists.isEmpty(this.options.popover.link); + } + }, { + key: "initialize", + value: function initialize() { + this.$popover = this.ui.popover({ + className: 'note-link-popover', + callback: function callback($node) { + var $content = $node.find('.popover-content,.note-popover-content'); + $content.prepend(' '); + } + }).render().appendTo(this.options.container); + var $content = this.$popover.find('.popover-content,.note-popover-content'); + this.context.invoke('buttons.build', $content, this.options.popover.link); + this.$popover.on('mousedown', function (e) { + e.preventDefault(); + }); + } + }, { + key: "destroy", + value: function destroy() { + this.$popover.remove(); + } + }, { + key: "update", + value: function update() { + // Prevent focusing on editable when invoke('code') is executed + if (!this.context.invoke('editor.hasFocus')) { + this.hide(); + return; + } + + var rng = this.context.invoke('editor.getLastRange'); + + if (rng.isCollapsed() && rng.isOnAnchor()) { + var anchor = dom.ancestor(rng.sc, dom.isAnchor); + var href = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(anchor).attr('href'); + this.$popover.find('a').attr('href', href).text(href); + var pos = dom.posFromPlaceholder(anchor); + var containerOffset = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(this.options.container).offset(); + pos.top -= containerOffset.top; + pos.left -= containerOffset.left; + this.$popover.css({ + display: 'block', + left: pos.left, + top: pos.top + }); + } else { + this.hide(); + } + } + }, { + key: "hide", + value: function hide() { + this.$popover.hide(); + } + }]); + + return LinkPopover; +}(); + + +// CONCATENATED MODULE: ./src/js/base/module/ImageDialog.js +function ImageDialog_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function ImageDialog_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function ImageDialog_createClass(Constructor, protoProps, staticProps) { if (protoProps) ImageDialog_defineProperties(Constructor.prototype, protoProps); if (staticProps) ImageDialog_defineProperties(Constructor, staticProps); return Constructor; } + + + + + +var ImageDialog_ImageDialog = /*#__PURE__*/function () { + function ImageDialog(context) { + ImageDialog_classCallCheck(this, ImageDialog); + + this.context = context; + this.ui = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.summernote.ui; + this.$body = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(document.body); + this.$editor = context.layoutInfo.editor; + this.options = context.options; + this.lang = this.options.langInfo; + } + + ImageDialog_createClass(ImageDialog, [{ + key: "initialize", + value: function initialize() { + var imageLimitation = ''; + + if (this.options.maximumImageFileSize) { + var unit = Math.floor(Math.log(this.options.maximumImageFileSize) / Math.log(1024)); + var readableSize = (this.options.maximumImageFileSize / Math.pow(1024, unit)).toFixed(2) * 1 + ' ' + ' KMGTP'[unit] + 'B'; + imageLimitation = "".concat(this.lang.image.maximumFileSize + ' : ' + readableSize, ""); + } + + var $container = this.options.dialogsInBody ? this.$body : this.options.container; + var body = ['
', '', '', imageLimitation, '
', '
', '', '', '
'].join(''); + var buttonClass = 'btn btn-primary note-btn note-btn-primary note-image-btn'; + var footer = ""); + this.$dialog = this.ui.dialog({ + title: this.lang.image.insert, + fade: this.options.dialogsFade, + body: body, + footer: footer + }).render().appendTo($container); + } + }, { + key: "destroy", + value: function destroy() { + this.ui.hideDialog(this.$dialog); + this.$dialog.remove(); + } + }, { + key: "bindEnterKey", + value: function bindEnterKey($input, $btn) { + $input.on('keypress', function (event) { + if (event.keyCode === core_key.code.ENTER) { + event.preventDefault(); + $btn.trigger('click'); + } + }); + } + }, { + key: "show", + value: function show() { + var _this = this; + + this.context.invoke('editor.saveRange'); + this.showImageDialog().then(function (data) { + // [workaround] hide dialog before restore range for IE range focus + _this.ui.hideDialog(_this.$dialog); + + _this.context.invoke('editor.restoreRange'); + + if (typeof data === 'string') { + // image url + // If onImageLinkInsert set, + if (_this.options.callbacks.onImageLinkInsert) { + _this.context.triggerEvent('image.link.insert', data); + } else { + _this.context.invoke('editor.insertImage', data); + } + } else { + // array of files + _this.context.invoke('editor.insertImagesOrCallback', data); + } + }).fail(function () { + _this.context.invoke('editor.restoreRange'); + }); + } + /** + * show image dialog + * + * @param {jQuery} $dialog + * @return {Promise} + */ + + }, { + key: "showImageDialog", + value: function showImageDialog() { + var _this2 = this; + + return external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.Deferred(function (deferred) { + var $imageInput = _this2.$dialog.find('.note-image-input'); + + var $imageUrl = _this2.$dialog.find('.note-image-url'); + + var $imageBtn = _this2.$dialog.find('.note-image-btn'); + + _this2.ui.onDialogShown(_this2.$dialog, function () { + _this2.context.triggerEvent('dialog.shown'); // Cloning imageInput to clear element. + + + $imageInput.replaceWith($imageInput.clone().on('change', function (event) { + deferred.resolve(event.target.files || event.target.value); + }).val('')); + $imageUrl.on('input paste propertychange', function () { + _this2.ui.toggleBtn($imageBtn, $imageUrl.val()); + }).val(''); + + if (!env.isSupportTouch) { + $imageUrl.trigger('focus'); + } + + $imageBtn.click(function (event) { + event.preventDefault(); + deferred.resolve($imageUrl.val()); + }); + + _this2.bindEnterKey($imageUrl, $imageBtn); + }); + + _this2.ui.onDialogHidden(_this2.$dialog, function () { + $imageInput.off(); + $imageUrl.off(); + $imageBtn.off(); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }); + + _this2.ui.showDialog(_this2.$dialog); + }); + } + }]); + + return ImageDialog; +}(); + + +// CONCATENATED MODULE: ./src/js/base/module/ImagePopover.js +function ImagePopover_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function ImagePopover_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function ImagePopover_createClass(Constructor, protoProps, staticProps) { if (protoProps) ImagePopover_defineProperties(Constructor.prototype, protoProps); if (staticProps) ImagePopover_defineProperties(Constructor, staticProps); return Constructor; } + + + + +/** + * Image popover module + * mouse events that show/hide popover will be handled by Handle.js. + * Handle.js will receive the events and invoke 'imagePopover.update'. + */ + +var ImagePopover_ImagePopover = /*#__PURE__*/function () { + function ImagePopover(context) { + var _this = this; + + ImagePopover_classCallCheck(this, ImagePopover); + + this.context = context; + this.ui = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.summernote.ui; + this.editable = context.layoutInfo.editable[0]; + this.options = context.options; + this.events = { + 'summernote.disable summernote.blur': function summernoteDisableSummernoteBlur() { + _this.hide(); + } + }; + } + + ImagePopover_createClass(ImagePopover, [{ + key: "shouldInitialize", + value: function shouldInitialize() { + return !lists.isEmpty(this.options.popover.image); + } + }, { + key: "initialize", + value: function initialize() { + this.$popover = this.ui.popover({ + className: 'note-image-popover' + }).render().appendTo(this.options.container); + var $content = this.$popover.find('.popover-content,.note-popover-content'); + this.context.invoke('buttons.build', $content, this.options.popover.image); + this.$popover.on('mousedown', function (e) { + e.preventDefault(); + }); + } + }, { + key: "destroy", + value: function destroy() { + this.$popover.remove(); + } + }, { + key: "update", + value: function update(target, event) { + if (dom.isImg(target)) { + var position = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(target).offset(); + var containerOffset = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(this.options.container).offset(); + var pos = {}; + + if (this.options.popatmouse) { + pos.left = event.pageX - 20; + pos.top = event.pageY; + } else { + pos = position; + } + + pos.top -= containerOffset.top; + pos.left -= containerOffset.left; + this.$popover.css({ + display: 'block', + left: pos.left, + top: pos.top + }); + } else { + this.hide(); + } + } + }, { + key: "hide", + value: function hide() { + this.$popover.hide(); + } + }]); + + return ImagePopover; +}(); + + +// CONCATENATED MODULE: ./src/js/base/module/TablePopover.js +function TablePopover_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function TablePopover_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function TablePopover_createClass(Constructor, protoProps, staticProps) { if (protoProps) TablePopover_defineProperties(Constructor.prototype, protoProps); if (staticProps) TablePopover_defineProperties(Constructor, staticProps); return Constructor; } + + + + + + +var TablePopover_TablePopover = /*#__PURE__*/function () { + function TablePopover(context) { + var _this = this; + + TablePopover_classCallCheck(this, TablePopover); + + this.context = context; + this.ui = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.summernote.ui; + this.options = context.options; + this.events = { + 'summernote.mousedown': function summernoteMousedown(we, e) { + _this.update(e.target); + }, + 'summernote.keyup summernote.scroll summernote.change': function summernoteKeyupSummernoteScrollSummernoteChange() { + _this.update(); + }, + 'summernote.disable summernote.blur': function summernoteDisableSummernoteBlur() { + _this.hide(); + } + }; + } + + TablePopover_createClass(TablePopover, [{ + key: "shouldInitialize", + value: function shouldInitialize() { + return !lists.isEmpty(this.options.popover.table); + } + }, { + key: "initialize", + value: function initialize() { + this.$popover = this.ui.popover({ + className: 'note-table-popover' + }).render().appendTo(this.options.container); + var $content = this.$popover.find('.popover-content,.note-popover-content'); + this.context.invoke('buttons.build', $content, this.options.popover.table); // [workaround] Disable Firefox's default table editor + + if (env.isFF) { + document.execCommand('enableInlineTableEditing', false, false); + } + + this.$popover.on('mousedown', function (e) { + e.preventDefault(); + }); + } + }, { + key: "destroy", + value: function destroy() { + this.$popover.remove(); + } + }, { + key: "update", + value: function update(target) { + if (this.context.isDisabled()) { + return false; + } + + var isCell = dom.isCell(target); + + if (isCell) { + var pos = dom.posFromPlaceholder(target); + var containerOffset = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(this.options.container).offset(); + pos.top -= containerOffset.top; + pos.left -= containerOffset.left; + this.$popover.css({ + display: 'block', + left: pos.left, + top: pos.top + }); + } else { + this.hide(); + } + + return isCell; + } + }, { + key: "hide", + value: function hide() { + this.$popover.hide(); + } + }]); + + return TablePopover; +}(); + + +// CONCATENATED MODULE: ./src/js/base/module/VideoDialog.js +function VideoDialog_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function VideoDialog_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function VideoDialog_createClass(Constructor, protoProps, staticProps) { if (protoProps) VideoDialog_defineProperties(Constructor.prototype, protoProps); if (staticProps) VideoDialog_defineProperties(Constructor, staticProps); return Constructor; } + + + + + +var VideoDialog_VideoDialog = /*#__PURE__*/function () { + function VideoDialog(context) { + VideoDialog_classCallCheck(this, VideoDialog); + + this.context = context; + this.ui = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default.a.summernote.ui; + this.$body = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()(document.body); + this.$editor = context.layoutInfo.editor; + this.options = context.options; + this.lang = this.options.langInfo; + } + + VideoDialog_createClass(VideoDialog, [{ + key: "initialize", + value: function initialize() { + var $container = this.options.dialogsInBody ? this.$body : this.options.container; + var body = ['
', ""), ""), '
'].join(''); + var buttonClass = 'btn btn-primary note-btn note-btn-primary note-video-btn'; + var footer = ""); + this.$dialog = this.ui.dialog({ + title: this.lang.video.insert, + fade: this.options.dialogsFade, + body: body, + footer: footer + }).render().appendTo($container); + } + }, { + key: "destroy", + value: function destroy() { + this.ui.hideDialog(this.$dialog); + this.$dialog.remove(); + } + }, { + key: "bindEnterKey", + value: function bindEnterKey($input, $btn) { + $input.on('keypress', function (event) { + if (event.keyCode === core_key.code.ENTER) { + event.preventDefault(); + $btn.trigger('click'); + } + }); + } + }, { + key: "createVideoNode", + value: function createVideoNode(url) { + // video url patterns(youtube, instagram, vimeo, dailymotion, youku, mp4, ogg, webm) + var ytRegExp = /\/\/(?:(?:www|m)\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w|-]{11})(?:(?:[\?&]t=)(\S+))?$/; + var ytRegExpForStart = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/; + var ytMatch = url.match(ytRegExp); + var igRegExp = /(?:www\.|\/\/)instagram\.com\/p\/(.[a-zA-Z0-9_-]*)/; + var igMatch = url.match(igRegExp); + var vRegExp = /\/\/vine\.co\/v\/([a-zA-Z0-9]+)/; + var vMatch = url.match(vRegExp); + var vimRegExp = /\/\/(player\.)?vimeo\.com\/([a-z]*\/)*(\d+)[?]?.*/; + var vimMatch = url.match(vimRegExp); + var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/; + var dmMatch = url.match(dmRegExp); + var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/; + var youkuMatch = url.match(youkuRegExp); + var qqRegExp = /\/\/v\.qq\.com.*?vid=(.+)/; + var qqMatch = url.match(qqRegExp); + var qqRegExp2 = /\/\/v\.qq\.com\/x?\/?(page|cover).*?\/([^\/]+)\.html\??.*/; + var qqMatch2 = url.match(qqRegExp2); + var mp4RegExp = /^.+.(mp4|m4v)$/; + var mp4Match = url.match(mp4RegExp); + var oggRegExp = /^.+.(ogg|ogv)$/; + var oggMatch = url.match(oggRegExp); + var webmRegExp = /^.+.(webm)$/; + var webmMatch = url.match(webmRegExp); + var fbRegExp = /(?:www\.|\/\/)facebook\.com\/([^\/]+)\/videos\/([0-9]+)/; + var fbMatch = url.match(fbRegExp); + var $video; + + if (ytMatch && ytMatch[1].length === 11) { + var youtubeId = ytMatch[1]; + var start = 0; + + if (typeof ytMatch[2] !== 'undefined') { + var ytMatchForStart = ytMatch[2].match(ytRegExpForStart); + + if (ytMatchForStart) { + for (var n = [3600, 60, 1], i = 0, r = n.length; i < r; i++) { + start += typeof ytMatchForStart[i + 1] !== 'undefined' ? n[i] * parseInt(ytMatchForStart[i + 1], 10) : 0; + } + } + } + + $video = external_root_jQuery_commonjs2_jquery_commonjs_jquery_amd_jquery_default()('