diff --git a/src/Business/Grand.Business.Checkout/Queries/Handlers/Orders/GetGiftVoucherQueryHandler.cs b/src/Business/Grand.Business.Checkout/Queries/Handlers/Orders/GetGiftVoucherQueryHandler.cs index 68da368e9a..1842e318ea 100644 --- a/src/Business/Grand.Business.Checkout/Queries/Handlers/Orders/GetGiftVoucherQueryHandler.cs +++ b/src/Business/Grand.Business.Checkout/Queries/Handlers/Orders/GetGiftVoucherQueryHandler.cs @@ -27,7 +27,8 @@ public async Task> Handle(GetGiftVoucherQuery request, C query = query.Where(gc => gc.StoreId == request.StoreId || gc.StoreId == null || gc.StoreId == ""); if (!string.IsNullOrEmpty(request.PurchasedWithOrderItemId)) - query = query.Where(gc => gc.PurchasedWithOrderItem.Id == request.PurchasedWithOrderItemId); + query = query.Where(gc => + gc.PurchasedWithOrderItem != null && gc.PurchasedWithOrderItem.Id == request.PurchasedWithOrderItemId); if (request.CreatedFromUtc.HasValue) query = query.Where(gc => request.CreatedFromUtc.Value <= gc.CreatedOnUtc); diff --git a/src/Business/Grand.Business.Checkout/Queries/Handlers/Orders/GetOrderQueryHandler.cs b/src/Business/Grand.Business.Checkout/Queries/Handlers/Orders/GetOrderQueryHandler.cs index 960891442a..be9fa67c09 100644 --- a/src/Business/Grand.Business.Checkout/Queries/Handlers/Orders/GetOrderQueryHandler.cs +++ b/src/Business/Grand.Business.Checkout/Queries/Handlers/Orders/GetOrderQueryHandler.cs @@ -81,7 +81,8 @@ public async Task> Handle(GetOrderQuery request, CancellationT if (!string.IsNullOrEmpty(request.BillingLastName)) query = query.Where(o => - o.BillingAddress != null && o.BillingAddress.LastName.Contains(request.BillingLastName)); + o.BillingAddress != null && !string.IsNullOrEmpty(o.BillingAddress.LastName) && + o.BillingAddress.LastName.Contains(request.BillingLastName)); if (!string.IsNullOrEmpty(request.OrderGuid)) if (Guid.TryParse(request.OrderGuid, out var orderGuid)) diff --git a/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs b/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs new file mode 100644 index 0000000000..5ad147add3 --- /dev/null +++ b/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs @@ -0,0 +1,60 @@ +using LiteDB; +using System.Reflection; + +namespace Grand.Data.LiteDb; + +/// +/// Builds the entity mappers before the application starts handling requests and running tasks. +/// LiteDB publishes a mapper before filling its members and reads published mappers without a lock +/// (BsonMapper.BuildAddEntityMapper), so threads meeting a type for the first time together either +/// throw "Collection was modified" or silently read an object with fields missing. +/// +public static class LiteDBMapperWarmup +{ + private static readonly string[] FrameworkNamespaces = ["System", "Microsoft", "LiteDB", "MongoDB"]; + + //Entity is the public way to reach the internal BsonMapper.GetEntityMapper + private static readonly MethodInfo BuildMapper = typeof(BsonMapper).GetMethod(nameof(BsonMapper.Entity))!; + + public static void Warm(IEnumerable entityTypes) + { + Warm(BsonMapper.Global, entityTypes); + } + + public static void Warm(BsonMapper mapper, IEnumerable entityTypes) + { + var visited = new HashSet(); + var pending = new Stack(entityTypes); + + while (pending.TryPop(out var type)) + { + if (!visited.Add(type)) continue; + + if (type.IsArray) + { + pending.Push(type.GetElementType()!); + continue; + } + + //lists, dictionaries and nullables hold their mapped types in the type arguments + foreach (var argument in type.GetGenericArguments()) pending.Push(argument); + + if (type.IsEnum || type.ContainsGenericParameters || IsFrameworkType(type)) continue; + + BuildMapper.MakeGenericMethod(type).Invoke(mapper, null); + + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + pending.Push(property.PropertyType); + + //LINQ looks a member up on the type declaring it: x.Id goes through ParentEntity, not the entity + if (type.BaseType != null) pending.Push(type.BaseType); + foreach (var contract in type.GetInterfaces()) pending.Push(contract); + } + } + + private static bool IsFrameworkType(Type type) + { + return type.Namespace != null && + FrameworkNamespaces.Any(name => type.Namespace == name || type.Namespace.StartsWith(name + ".")); + } +} diff --git a/src/Core/Grand.Infrastructure/Configuration/DatabaseConfig.cs b/src/Core/Grand.Infrastructure/Configuration/DatabaseConfig.cs index 7338e85e27..273ab7a4a4 100644 --- a/src/Core/Grand.Infrastructure/Configuration/DatabaseConfig.cs +++ b/src/Core/Grand.Infrastructure/Configuration/DatabaseConfig.cs @@ -7,11 +7,6 @@ public class DatabaseConfig /// public bool UseLiteDb { get; set; } - /// - /// Gets or sets a value indicating whether use LiteDB database as a singleton service in the application - /// - public bool Singleton { get; set; } - /// /// Gets or sets a value indicating whether use LiteDB database (only for installation process) /// diff --git a/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs index 6a070a3929..277f1e0cc8 100644 --- a/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs +++ b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs @@ -2,8 +2,11 @@ using Grand.Data.Audit; using Grand.Data.LiteDb; using Grand.Data.Mongo; +using Grand.Domain; using Grand.Infrastructure.Audit; using Grand.Infrastructure.Configuration; +using Grand.Infrastructure.Plugins; +using Grand.Infrastructure.TypeSearch; using LiteDB; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -63,12 +66,9 @@ private void RegisterDataLayer(IServiceCollection serviceCollection, IConfigurat } else { - if (dbConfig.Singleton) - serviceCollection.AddSingleton(_ => new LiteDatabase(dataProviderSettings.ConnectionString) - { UtcDate = true }); - else - serviceCollection.AddScoped(_ => new LiteDatabase(dataProviderSettings.ConnectionString) - { UtcDate = true }); + //one shared instance: LiteDB locks the file, so a second one in the process cannot open it + serviceCollection.AddSingleton(_ => new LiteDatabase(dataProviderSettings.ConnectionString) + { UtcDate = true }); } } @@ -87,6 +87,13 @@ private void RegisterDataLayer(IServiceCollection serviceCollection, IConfigurat } else { + //the mappers are not safe to build concurrently - startup is still single-threaded here + var typeSearcher = new TypeSearcher(); + //an uninstalled plugin counts too: installing it uses its entities on the running site + var plugins = PluginManager.ReferencedPlugins?.Where(x => x.PluginType != null) + .Select(x => x.ReferencedAssembly) ?? []; + LiteDBMapperWarmup.Warm( + typeSearcher.ClassesOfType(typeof(BaseEntity), typeSearcher.GetAssemblies().Union(plugins))); //database context serviceCollection.AddScoped(); //store files context - grid fs diff --git a/src/Modules/Grand.Module.Api/Infrastructure/LiteDbMapperStartup.cs b/src/Modules/Grand.Module.Api/Infrastructure/LiteDbMapperStartup.cs new file mode 100644 index 0000000000..ad59a40534 --- /dev/null +++ b/src/Modules/Grand.Module.Api/Infrastructure/LiteDbMapperStartup.cs @@ -0,0 +1,31 @@ +using Grand.Data.LiteDb; +using Grand.Infrastructure; +using Grand.Infrastructure.Configuration; +using Grand.Infrastructure.TypeSearch; +using Grand.Module.Api.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Grand.Module.Api.Infrastructure; + +public class LiteDbMapperStartup : IStartupApplication +{ + public void Configure(WebApplication application, IWebHostEnvironment webHostEnvironment) + { + } + + public void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + var dbConfig = new DatabaseConfig(); + configuration.GetSection("Database").Bind(dbConfig); + + //API queries read these models through TableCollection, from the mappers the entities are read from + if (dbConfig.UseLiteDb) + LiteDBMapperWarmup.Warm(new TypeSearcher().ClassesOfType()); + } + + public int Priority => 0; + public bool BeforeConfigure => false; +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetGiftVoucherQueryHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetGiftVoucherQueryHandlerTests.cs new file mode 100644 index 0000000000..ce19780562 --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetGiftVoucherQueryHandlerTests.cs @@ -0,0 +1,34 @@ +using Grand.Business.Checkout.Queries.Handlers.Orders; +using Grand.Business.Core.Queries.Checkout.Orders; +using Grand.Data.Tests.LiteDb; +using Grand.Domain.Orders; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Business.Checkout.Tests.Queries.Handlers.Orders; + +[TestClass] +public class GetGiftVoucherQueryHandlerTests +{ + private GetGiftVoucherQueryHandler _handler; + private LiteDBRepositoryMock _repository; + + [TestInitialize] + public void Init() + { + _repository = new LiteDBRepositoryMock(); + _handler = new GetGiftVoucherQueryHandler(_repository); + } + + [TestMethod] + public async Task Handle_ByPurchasedOrderItem_SkipsVouchersCreatedByHand() + { + await _repository.InsertAsync(new GiftVoucher { Code = "manual" }); + await _repository.InsertAsync(new GiftVoucher { Code = "bought", PurchasedWithOrderItem = new OrderItem { Id = "item-1" } }); + await _repository.InsertAsync(new GiftVoucher { Code = "other", PurchasedWithOrderItem = new OrderItem { Id = "item-2" } }); + + var result = await _handler.Handle(new GetGiftVoucherQuery { PurchasedWithOrderItemId = "item-1" }, CancellationToken.None); + + Assert.HasCount(1, result); + Assert.AreEqual("bought", result[0].Code); + } +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetOrderQueryHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetOrderQueryHandlerTests.cs index acbdf3a113..0a4e907564 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetOrderQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetOrderQueryHandlerTests.cs @@ -1,6 +1,8 @@ using Grand.Business.Checkout.Queries.Handlers.Orders; using Grand.Business.Core.Queries.Checkout.Orders; +using Grand.Data.Tests.LiteDb; using Grand.Data.Tests.MongoDb; +using Grand.Domain.Common; using Grand.Domain.Orders; using Grand.Domain.Payments; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -91,4 +93,31 @@ await _repository.Collection.InsertOneAsync(new Order { Assert.AreEqual(0, result.Count); } + + [TestMethod] + public async Task Handle_ByBillingLastName_SkipsAddressesWithoutOne() + { + await _repository.Collection.InsertOneAsync(new Order { Code = "NONAME", BillingAddress = new Address() }); + await _repository.Collection.InsertOneAsync(new Order { Code = "NAMED", BillingAddress = new Address { LastName = "Kowalski" } }); + + var result = await _handler.Handle(new GetOrderQuery { BillingLastName = "Kowal" }, CancellationToken.None); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("NAMED", result[0].Code); + } + + [TestMethod] + public async Task Handle_ByBillingLastName_OnLiteDb_SkipsAddressesWithoutOne() + { + //Table on LiteDB is LINQ over objects, so a null here throws instead of not matching + var repository = new LiteDBRepositoryMock(); + await repository.InsertAsync(new Order { Code = "NONAME", BillingAddress = new Address() }); + await repository.InsertAsync(new Order { Code = "NAMED", BillingAddress = new Address { LastName = "Kowalski" } }); + + var result = await new GetOrderQueryHandler(repository) + .Handle(new GetOrderQuery { BillingLastName = "Kowal" }, CancellationToken.None); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("NAMED", result[0].Code); + } } diff --git a/src/Tests/Grand.Data.Tests/LiteDb/LiteDBMapperWarmupTests.cs b/src/Tests/Grand.Data.Tests/LiteDb/LiteDBMapperWarmupTests.cs new file mode 100644 index 0000000000..e0fb5b3d8b --- /dev/null +++ b/src/Tests/Grand.Data.Tests/LiteDb/LiteDBMapperWarmupTests.cs @@ -0,0 +1,199 @@ +using Grand.Data.LiteDb; +using Grand.Domain; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using LiteDB; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Concurrent; +using System.Reflection; + +namespace Grand.Data.Tests.LiteDb; + +[TestClass] +public class LiteDBMapperWarmupTests +{ + private const int Rounds = 40; + + [TestMethod] + public void Warm_FirstConcurrentReadsOfAType_ReadTheWholeObject() + { + var document = new BsonMapper().ToDocument(new Order { + OrderNumber = 7, + CustomerEmail = "buyer@firm.test", + OrderItems = { new OrderItem { Sku = "SKU-1", Quantity = 2 } } + }); + var failures = new ConcurrentBag(); + var threadCount = Math.Max(8, Environment.ProcessorCount); + + for (var round = 0; round < Rounds; round++) + { + var mapper = new BsonMapper(); + LiteDBMapperWarmup.Warm(mapper, [typeof(Order)]); + + using var barrier = new Barrier(threadCount); + var threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(() => + { + barrier.SignalAndWait(); + try + { + var order = mapper.ToObject(document); + if (order.OrderNumber != 7 || order.CustomerEmail != "buyer@firm.test" || + order.OrderItems.Count != 1 || order.OrderItems.First().Sku != "SKU-1") + failures.Add("incomplete object"); + } + catch (Exception exc) + { + failures.Add(exc.Message); + } + })).ToList(); + + threads.ForEach(thread => thread.Start()); + threads.ForEach(thread => thread.Join()); + } + + Assert.IsEmpty(failures, string.Join(" | ", failures.Distinct())); + } + + [TestMethod] + public void Warm_ReachesEveryTypeNestedInAnEntity() + { + var mapper = new BsonMapper(); + + LiteDBMapperWarmup.Warm(mapper, [typeof(WarmRoot)]); + + var mapped = MappedTypes(mapper); + CollectionAssert.IsSubsetOf( + new[] { + typeof(WarmRoot), typeof(WarmListItem), typeof(WarmArrayItem), typeof(WarmDictionaryValue), + typeof(WarmStruct), typeof(WarmDeep) + }, + mapped); + } + + [TestMethod] + public void Warm_ReachesBaseTypesAndInterfaces_WhichLinqResolvesMembersOn() + { + var mapper = new BsonMapper(); + + LiteDBMapperWarmup.Warm(mapper, [typeof(WarmRoot)]); + + var mapped = MappedTypes(mapper); + CollectionAssert.Contains(mapped, typeof(WarmBase)); + CollectionAssert.Contains(mapped, typeof(IWarmContract)); + } + + [TestMethod] + public void Warm_FirstConcurrentQueriesByInheritedId_DoNotFail() + { + var failures = new ConcurrentBag(); + var threadCount = Math.Max(8, Environment.ProcessorCount); + + for (var round = 0; round < Rounds; round++) + { + var mapper = new BsonMapper(); + LiteDBMapperWarmup.Warm(mapper, [typeof(Order)]); + + using var barrier = new Barrier(threadCount); + var threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(() => + { + barrier.SignalAndWait(); + try + { + mapper.GetExpression(x => x.Id == "1"); + } + catch (Exception exc) + { + failures.Add(exc.Message); + } + })).ToList(); + + threads.ForEach(thread => thread.Start()); + threads.ForEach(thread => thread.Join()); + } + + Assert.IsEmpty(failures, string.Join(" | ", failures.Distinct())); + } + + [TestMethod] + public void Warm_LeavesSimpleAndFrameworkTypesAlone() + { + var mapper = new BsonMapper(); + + LiteDBMapperWarmup.Warm(mapper, [typeof(WarmRoot)]); + + var mapped = MappedTypes(mapper); + CollectionAssert.DoesNotContain(mapped, typeof(string)); + CollectionAssert.DoesNotContain(mapped, typeof(DateTime)); + CollectionAssert.DoesNotContain(mapped, typeof(WarmKind)); + CollectionAssert.DoesNotContain(mapped, typeof(List)); + CollectionAssert.DoesNotContain(mapped, typeof(Uri)); + CollectionAssert.DoesNotContain(mapped, typeof(object)); + CollectionAssert.DoesNotContain(mapped, typeof(IDisposable)); + } + + [TestMethod] + public void Warm_EveryDomainEntity_BuildsTheirNestedTypes() + { + var mapper = new BsonMapper(); + var entities = typeof(BaseEntity).Assembly.GetTypes() + .Where(type => typeof(BaseEntity).IsAssignableFrom(type) && !type.IsAbstract); + + LiteDBMapperWarmup.Warm(mapper, entities); + + var mapped = MappedTypes(mapper); + CollectionAssert.Contains(mapped, typeof(Order)); + CollectionAssert.Contains(mapped, typeof(OrderItem)); + CollectionAssert.Contains(mapped, typeof(TranslationEntity)); + CollectionAssert.Contains(mapped, typeof(ParentEntity)); + } + + private static List MappedTypes(BsonMapper mapper) + { + var entities = typeof(BsonMapper).GetField("_entities", BindingFlags.NonPublic | BindingFlags.Instance)!; + return ((System.Collections.IDictionary)entities.GetValue(mapper)!).Keys.Cast().ToList(); + } + + private enum WarmKind + { + One + } + + private interface IWarmContract + { + string Code { get; set; } + } + + private abstract class WarmBase + { + public string Key { get; set; } + } + + private class WarmRoot : WarmBase, IWarmContract, IDisposable + { + public string Code { get; set; } + public void Dispose() { } + public string Name { get; set; } + public DateTime Created { get; set; } + public WarmKind Kind { get; set; } + public Uri Link { get; set; } + public IList Items { get; set; } = new List(); + public WarmArrayItem[] Array { get; set; } + public Dictionary Values { get; set; } + public WarmStruct? Maybe { get; set; } + //a type that contains itself must not loop forever + public WarmRoot Parent { get; set; } + } + + private class WarmListItem + { + public WarmDeep Deep { get; set; } + } + + private class WarmArrayItem; + + private class WarmDictionaryValue; + + private struct WarmStruct; + + private class WarmDeep; +} diff --git a/src/Tests/Grand.Infrastructure.Tests/Startup/StartupApplicationTests.cs b/src/Tests/Grand.Infrastructure.Tests/Startup/StartupApplicationTests.cs new file mode 100644 index 0000000000..9cae2e28b9 --- /dev/null +++ b/src/Tests/Grand.Infrastructure.Tests/Startup/StartupApplicationTests.cs @@ -0,0 +1,50 @@ +using Grand.Data; +using Grand.Domain; +using Grand.Infrastructure.Plugins; +using Grand.Infrastructure.Startup; +using Grand.Infrastructure.Tests.Plugins; +using LiteDB; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections; +using System.Reflection; + +namespace Grand.Infrastructure.Tests.Startup; + +[TestClass] +public class StartupApplicationTests +{ + [TestMethod] + [DoNotParallelize] + public void ConfigureServices_OnLiteDb_BuildsTheMappersOfAnUninstalledPlugin() + { + //installing a plugin uses its entities on the running site, among requests reading the same mappers + DataSettingsManager.Initialize(Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.cfg")); + var referencedPlugins = PluginManager.ReferencedPlugins; + try + { + PluginManager.ReferencedPlugins = [ + new PluginInfo(null, typeof(SampleBasePlugin).Assembly, typeof(SampleBasePlugin)) { + SystemName = "SamplePlugin", + Installed = false + } + ]; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["Database:UseLiteDb"] = "true" }) + .Build(); + + new StartupApplication().ConfigureServices(new ServiceCollection(), configuration); + + var entities = typeof(BsonMapper).GetField("_entities", BindingFlags.NonPublic | BindingFlags.Instance)!; + var mapped = ((IDictionary)entities.GetValue(BsonMapper.Global)!).Keys.Cast().ToList(); + CollectionAssert.Contains(mapped, typeof(SamplePluginEntity)); + } + finally + { + PluginManager.ReferencedPlugins = referencedPlugins; + } + } + + private class SamplePluginEntity : BaseEntity; +} diff --git a/src/Tests/Grand.Module.Api.Tests/Grand.Module.Api.Tests.csproj b/src/Tests/Grand.Module.Api.Tests/Grand.Module.Api.Tests.csproj index ea169c0d9b..0cc055eb31 100644 --- a/src/Tests/Grand.Module.Api.Tests/Grand.Module.Api.Tests.csproj +++ b/src/Tests/Grand.Module.Api.Tests/Grand.Module.Api.Tests.csproj @@ -19,6 +19,8 @@ + + diff --git a/src/Tests/Grand.Module.Api.Tests/Infrastructure/LiteDbMapperStartupTests.cs b/src/Tests/Grand.Module.Api.Tests/Infrastructure/LiteDbMapperStartupTests.cs new file mode 100644 index 0000000000..1a3a8ec2f2 --- /dev/null +++ b/src/Tests/Grand.Module.Api.Tests/Infrastructure/LiteDbMapperStartupTests.cs @@ -0,0 +1,28 @@ +using Grand.Module.Api.DTOs.Catalog; +using Grand.Module.Api.Infrastructure; +using LiteDB; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections; +using System.Reflection; + +namespace Grand.Module.Api.Tests.Infrastructure; + +[TestClass] +public class LiteDbMapperStartupTests +{ + [TestMethod] + public void ConfigureServices_OnLiteDb_BuildsTheMappersOfTheApiModels() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["Database:UseLiteDb"] = "true" }) + .Build(); + + new LiteDbMapperStartup().ConfigureServices(new ServiceCollection(), configuration); + + var entities = typeof(BsonMapper).GetField("_entities", BindingFlags.NonPublic | BindingFlags.Instance)!; + var mapped = ((IDictionary)entities.GetValue(BsonMapper.Global)!).Keys.Cast().ToList(); + CollectionAssert.Contains(mapped, typeof(CategoryDto)); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs b/src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs new file mode 100644 index 0000000000..473203caea --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs @@ -0,0 +1,79 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Web.AdminShared.Extensions; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System.Collections.ObjectModel; + +namespace Grand.Web.Admin.Tests.Extensions; + +[TestClass] +public class DateTimeServiceExtensionsTests +{ + private const string ServerLabel = "Server time zone"; + + private readonly TimeZoneInfo _dateline = TimeZoneInfo.CreateCustomTimeZone("Dateline", TimeSpan.FromHours(-12), "Dateline", "Dateline"); + private readonly TimeZoneInfo _warsaw = TimeZoneInfo.CreateCustomTimeZone("Warsaw", TimeSpan.FromHours(1), "Warsaw", "Warsaw"); + private readonly TimeZoneInfo _unlisted = TimeZoneInfo.CreateCustomTimeZone("Other/Name", TimeSpan.FromHours(2), "Other", "Other"); + private IDateTimeService _dateTimeService; + + [TestInitialize] + public void Init() + { + var service = new Mock(); + service.Setup(x => x.GetSystemTimeZones()) + .Returns(new ReadOnlyCollection([_dateline, _warsaw])); + service.Setup(x => x.FindTimeZoneById(It.IsAny())).Throws(); + service.Setup(x => x.FindTimeZoneById("Other/Name")).Returns(_unlisted); + _dateTimeService = service.Object; + } + + private IList Options(string configured) => + _dateTimeService.GetStoreTimeZoneOptions(configured, ServerLabel); + + private static string Selected(IList options) => options.Single(x => x.Selected).Value; + + [TestMethod] + public void GetStoreTimeZoneOptions_StartWithTheServerZone_WithAnEmptyValue() + { + var first = Options("Warsaw")[0]; + + Assert.AreEqual("", first.Value); + StringAssert.StartsWith(first.Text, ServerLabel); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + public void GetStoreTimeZoneOptions_NothingConfigured_SelectsTheServerZone(string configured) + { + //so saving the form leaves the store following the server + Assert.AreEqual("", Selected(Options(configured))); + } + + [TestMethod] + public void GetStoreTimeZoneOptions_ConfiguredZoneListed_SelectsIt() + { + Assert.AreEqual("Warsaw", Selected(Options("Warsaw"))); + } + + [TestMethod] + public void GetStoreTimeZoneOptions_ConfiguredZoneDiffersInCase_SelectsTheListedId() + { + Assert.AreEqual("Warsaw", Selected(Options("warsaw"))); + } + + [TestMethod] + public void GetStoreTimeZoneOptions_ConfiguredZoneResolvesButIsNotListed_KeepsIt() + { + //a Windows id on Linux, or the reverse: dates are shown in it, so saving must not change it + Assert.AreEqual("Other/Name", Selected(Options("Other/Name"))); + } + + [TestMethod] + public void GetStoreTimeZoneOptions_ConfiguredZoneUnknownOnThisMachine_SelectsTheServerZone() + { + //DateTimeService.CurrentTimeZone falls back to the server here as well + Assert.AreEqual("", Selected(Options("Gone/Zone"))); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs index d3356f6851..9bc0191618 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs @@ -174,6 +174,17 @@ public async Task InsertCustomerModel_MapsStoreIdFromModel() _customerServiceMock.Verify(c => c.InsertCustomer(It.Is(x => x.StoreId == "store-1")), Times.Once); } + [TestMethod] + public async Task UpdateCustomerModel_GuestWithoutEmail_StaysWithoutOne() + { + var customer = await _customerViewModelService.UpdateCustomerModel( + new Customer { Id = "c1" }, new CustomerModel { Id = "c1", AdminComment = "called in" }); + + Assert.AreEqual("called in", customer.AdminComment); + Assert.IsTrue(string.IsNullOrEmpty(customer.Email)); + Assert.IsTrue(string.IsNullOrEmpty(customer.Username)); + } + [TestMethod] public async Task PrepareCustomerModel_StoreManager_PresetsCurrentStoreId() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs b/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs index 818d1bdf00..87efa0bbcb 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs @@ -21,7 +21,9 @@ public class CustomerValidatorTests private const string CurrentStoreId = "store-current"; private const string RequiredMessage = "Admin.Customers.Customers.Fields.Store.Required"; private const string MustBeCurrentStoreMessage = "Admin.Customers.Customers.Fields.Store.MustBeCurrentStore"; + private const string EmailRequiredMessage = "Admin.Customers.Customers.Fields.Email.Required"; + private Mock _customerServiceMock; private Mock _groupServiceMock; private CustomerValidator _validator; @@ -31,6 +33,7 @@ public void Setup() var translationServiceMock = new Mock(); translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns(k => k); + _customerServiceMock = new Mock(); _groupServiceMock = new Mock(); _groupServiceMock .Setup(g => g.GetAllCustomerGroups(It.IsAny(), It.IsAny(), It.IsAny(), @@ -50,7 +53,7 @@ public void Setup() translationServiceMock.Object, new Mock().Object, contextAccessorMock.Object, - new Mock().Object, + _customerServiceMock.Object, _groupServiceMock.Object, new CustomerSettings(), new CustomerConfig(), @@ -60,6 +63,79 @@ public void Setup() private static CustomerModel BuildModel(string storeId) => new() { Id = "customer-1", Email = "customer@example.com", StoreId = storeId }; + [TestMethod] + [DataRow(null)] + [DataRow("")] + public async Task Email_Empty_GuestWithoutEmail_IsAccepted(string email) + { + _customerServiceMock.Setup(c => c.GetCustomerById("customer-1")) + .ReturnsAsync(new Customer { Id = "customer-1", Email = null }); + + var result = await _validator.ValidateAsync(new CustomerModel { Id = "customer-1", Email = email, StoreId = CurrentStoreId }); + + Assert.IsFalse(result.Errors.Any(e => e.PropertyName == nameof(CustomerModel.Email))); + } + + [TestMethod] + public async Task Email_Empty_GuestBecomingRegistered_ReportsRequired() + { + _customerServiceMock.Setup(c => c.GetCustomerById("customer-1")) + .ReturnsAsync(new Customer { Id = "customer-1", Email = null }); + _groupServiceMock.Setup(g => g.GetCustomerGroupBySystemName(SystemCustomerGroupNames.Registered)) + .ReturnsAsync(new CustomerGroup { Id = "group-registered" }); + + var result = await _validator.ValidateAsync(new CustomerModel { + Id = "customer-1", StoreId = CurrentStoreId, CustomerGroups = ["group-registered"] + }); + + Assert.IsTrue(result.Errors.Any(e => e.ErrorMessage == EmailRequiredMessage)); + } + + [TestMethod] + public async Task Email_Invalid_GuestWithoutEmail_IsStillChecked() + { + _customerServiceMock.Setup(c => c.GetCustomerById("customer-1")) + .ReturnsAsync(new Customer { Id = "customer-1", Email = null }); + + var result = await _validator.ValidateAsync(new CustomerModel { Id = "customer-1", Email = "not-an-email", StoreId = CurrentStoreId }); + + Assert.IsTrue(result.Errors.Any(e => e.PropertyName == nameof(CustomerModel.Email))); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + public async Task Email_Cleared_RegisteredCustomer_ReportsRequired(string email) + { + _customerServiceMock.Setup(c => c.GetCustomerById("customer-1")) + .ReturnsAsync(new Customer { Id = "customer-1", Email = "customer@example.com" }); + + var result = await _validator.ValidateAsync(new CustomerModel { Id = "customer-1", Email = email, StoreId = CurrentStoreId }); + + Assert.IsTrue(result.Errors.Any(e => e.ErrorMessage == EmailRequiredMessage)); + } + + [TestMethod] + public async Task Email_Empty_NewCustomer_ReportsRequired() + { + var result = await _validator.ValidateAsync(new CustomerModel { StoreId = CurrentStoreId }); + + Assert.IsTrue(result.Errors.Any(e => e.ErrorMessage == EmailRequiredMessage)); + } + + [TestMethod] + public async Task Email_Changed_IsStillCheckedForDuplicates() + { + _customerServiceMock.Setup(c => c.GetCustomerById("customer-1")) + .ReturnsAsync(new Customer { Id = "customer-1", Email = "customer@example.com" }); + _customerServiceMock.Setup(c => c.GetCustomerByEmail("taken@example.com", It.IsAny())) + .ReturnsAsync(new Customer { Id = "customer-2", Email = "taken@example.com" }); + + var result = await _validator.ValidateAsync(new CustomerModel { Id = "customer-1", Email = "taken@example.com", StoreId = CurrentStoreId }); + + Assert.IsTrue(result.Errors.Any(e => e.ErrorMessage == "Account.EmailUsernameErrors.EmailAlreadyExists")); + } + [TestMethod] public async Task StoreId_Empty_FailsRequired() { diff --git a/src/Web/Grand.Web.Admin/App_Data/appsettings.json b/src/Web/Grand.Web.Admin/App_Data/appsettings.json index 1d4ce0a7ed..52adfd707b 100644 --- a/src/Web/Grand.Web.Admin/App_Data/appsettings.json +++ b/src/Web/Grand.Web.Admin/App_Data/appsettings.json @@ -45,8 +45,6 @@ "Database": { //This setting is required to use LiteDB database "UseLiteDb": false, - //LiteDB database as a singleton service in the application - "Singleton": true, //Init connection string - it's required only for the installation process! "LiteDbConnectionString": "Filename=App_Data/database.db", //Storefront keyword search (catalog search and autocomplete) narrows candidates through a text index, diff --git a/src/Web/Grand.Web.Admin/Controllers/SettingController.cs b/src/Web/Grand.Web.Admin/Controllers/SettingController.cs index 010a364798..ebaa89aa4b 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SettingController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SettingController.cs @@ -25,6 +25,7 @@ using Grand.Domain.Vendors; using Grand.Infrastructure.Caching; using Grand.SharedKernel.Extensions; +using Grand.Web.AdminShared.Extensions; using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Extensions.Mapping.Settings; using Grand.Web.AdminShared.Models.Settings; @@ -603,18 +604,11 @@ public async Task GeneralCommon([FromServices] IEnumerable(storeScope); - model.DateTimeSettings.DefaultStoreTimeZoneId = dateTimeSettings.DefaultStoreTimeZoneId; - var iswindows = OperatingSystem.IsWindows(); - foreach (var timeZone in dateTimeService.GetSystemTimeZones()) - { - var name = iswindows ? timeZone.DisplayName : $"{timeZone.StandardName} ({timeZone.Id})"; - model.DateTimeSettings.AvailableTimeZones.Add(new SelectListItem { - Text = name, - Value = timeZone.Id, - Selected = timeZone.Id.Equals(dateTimeSettings.DefaultStoreTimeZoneId, - StringComparison.OrdinalIgnoreCase) - }); - } + model.DateTimeSettings.AvailableTimeZones = dateTimeService.GetStoreTimeZoneOptions( + dateTimeSettings.DefaultStoreTimeZoneId, + translationService.GetResource("Admin.Settings.GeneralCommon.DefaultStoreTimeZone.Server")); + model.DateTimeSettings.DefaultStoreTimeZoneId = + model.DateTimeSettings.AvailableTimeZones.First(x => x.Selected).Value; //store information var storeInformationSettings = await settingService.LoadSetting(storeScope); diff --git a/src/Web/Grand.Web.AdminShared/Extensions/DateTimeServiceExtensions.cs b/src/Web/Grand.Web.AdminShared/Extensions/DateTimeServiceExtensions.cs new file mode 100644 index 0000000000..42cfb0dcbc --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Extensions/DateTimeServiceExtensions.cs @@ -0,0 +1,56 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Grand.Web.AdminShared.Extensions; + +public static class DateTimeServiceExtensions +{ + /// + /// The choices for a store's time zone, with the one dates are really shown in selected + /// (see DateTimeService.CurrentTimeZone). The empty value means the store follows the server, as it does + /// after installation, so saving the form never turns that into a fixed zone. A configured zone that + /// resolves but is not listed - a Windows id on Linux, or the reverse - keeps an option of its own. + /// + public static IList GetStoreTimeZoneOptions(this IDateTimeService dateTimeService, + string configuredTimeZoneId, string serverTimeZoneLabel) + { + var options = new List { + new() { Text = $"{serverTimeZoneLabel}: {DisplayName(TimeZoneInfo.Local)}", Value = "" } + }; + options.AddRange(dateTimeService.GetSystemTimeZones() + .Select(zone => new SelectListItem { Text = DisplayName(zone), Value = zone.Id })); + + var selected = options.FirstOrDefault(x => x.Value.Equals(configuredTimeZoneId, StringComparison.OrdinalIgnoreCase)) + ?? AddUnlistedZone(dateTimeService, configuredTimeZoneId, options) + ?? options[0]; + selected.Selected = true; + + return options; + } + + private static SelectListItem AddUnlistedZone(IDateTimeService dateTimeService, string timeZoneId, + List options) + { + if (string.IsNullOrEmpty(timeZoneId)) return null; + try + { + var option = new SelectListItem { + Text = DisplayName(dateTimeService.FindTimeZoneById(timeZoneId)), Value = timeZoneId + }; + options.Add(option); + return option; + } + catch (Exception exc) when (exc is TimeZoneNotFoundException or InvalidTimeZoneException) + { + //unknown on this machine - DateTimeService.CurrentTimeZone shows dates in the server's zone then + return null; + } + } + + private static string DisplayName(TimeZoneInfo zone) + { + return Grand.Infrastructure.OperatingSystem.IsWindows() + ? zone.DisplayName + : $"{zone.StandardName} ({zone.Id})"; + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index b99a19c1e9..6f8afaadc7 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -579,8 +579,8 @@ await _customerService.UpdateUserField(customer, SystemCustomerFieldNames.TwoFac customer.Email = model.Email; customer.Username = _customerSettings.UsernamesEnabled switch { - true when _customerSettings.AllowUsersToChangeUsernames => model.Username.Trim(), - false => model.Email.Trim(), + true when _customerSettings.AllowUsersToChangeUsernames => model.Username?.Trim(), + false => model.Email?.Trim(), _ => customer.Username }; diff --git a/src/Web/Grand.Web.AdminShared/Validators/Customers/CustomerValidator.cs b/src/Web/Grand.Web.AdminShared/Validators/Customers/CustomerValidator.cs index 5a7ce8144d..aa82ff2662 100644 --- a/src/Web/Grand.Web.AdminShared/Validators/Customers/CustomerValidator.cs +++ b/src/Web/Grand.Web.AdminShared/Validators/Customers/CustomerValidator.cs @@ -33,9 +33,11 @@ public CustomerValidator( CustomerCreateValidator(); CustomerEditValidator(); - //customer email - RuleFor(x => x.Email).NotEmpty().EmailAddress() - .WithMessage(translationService.GetResource("Admin.Customers.Customers.Fields.Email.Required")); + //customer email - a guest has none, and staying a guest must not take inventing one + RuleFor(x => x.Email) + .NotEmpty().WithMessage(translationService.GetResource("Admin.Customers.Customers.Fields.Email.Required")) + .EmailAddress().WithMessage(translationService.GetResource("Admin.Customers.Customers.Fields.Email.Required")) + .WhenAsync(async (x, _) => !string.IsNullOrEmpty(x.Email) || !await StaysGuestWithoutEmail(x)); //store - required only when the editor is not an administrator (admins may manage store-independent //back-office/system accounts that have no store) @@ -126,6 +128,17 @@ public CustomerValidator( context.AddFailure("The username cannot be empty"); }); + async Task StaysGuestWithoutEmail(CustomerModel model) + { + if (string.IsNullOrEmpty(model.Id)) return false; + + var customer = await customerService.GetCustomerById(model.Id); + if (customer == null || !string.IsNullOrEmpty(customer.Email)) return false; + + var registered = await groupService.GetCustomerGroupBySystemName(SystemCustomerGroupNames.Registered); + return registered == null || model.CustomerGroups == null || !model.CustomerGroups.Contains(registered.Id); + } + async Task ValidateCustomerGroups(IList customerGroups, CustomerModel customerModel) { ArgumentNullException.ThrowIfNull(customerGroups); @@ -311,7 +324,8 @@ void CustomerEditValidator() customer?.Id == contextAccessor.WorkContext.CurrentCustomer.Id) context.AddFailure("You can't edit own data from admin panel"); - if (customer != null && customer.Email != x.Email.ToLower()) + //an empty email is for the required rule above to judge: guests have none + if (customer != null && !string.IsNullOrWhiteSpace(x.Email) && customer.Email != x.Email.ToLower()) { if (!CommonHelper.IsValidEmail(x.Email)) context.AddFailure( diff --git a/src/Web/Grand.Web.Store/App_Data/appsettings.json b/src/Web/Grand.Web.Store/App_Data/appsettings.json index 0bcd3f9b71..d859ae916b 100644 --- a/src/Web/Grand.Web.Store/App_Data/appsettings.json +++ b/src/Web/Grand.Web.Store/App_Data/appsettings.json @@ -45,8 +45,6 @@ "Database": { //This setting is required to use LiteDB database "UseLiteDb": false, - //LiteDB database as a singleton service in the application - "Singleton": true, //Init connection string - it's required only for the installation process! "LiteDbConnectionString": "Filename=App_Data/database.db", //Storefront keyword search (catalog search and autocomplete) narrows candidates through a text index, diff --git a/src/Web/Grand.Web.Store/Controllers/SettingController.cs b/src/Web/Grand.Web.Store/Controllers/SettingController.cs index d042d9409b..822c12fa43 100644 --- a/src/Web/Grand.Web.Store/Controllers/SettingController.cs +++ b/src/Web/Grand.Web.Store/Controllers/SettingController.cs @@ -20,6 +20,7 @@ using Grand.Domain.Stores; using Grand.Infrastructure; using Grand.Infrastructure.Caching; +using Grand.Web.AdminShared.Extensions; using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Extensions.Mapping.Settings; using Grand.Web.AdminShared.Models.Settings; @@ -78,18 +79,11 @@ public async Task GeneralCommon([FromServices] IEnumerable(storeScope); - model.DateTimeSettings.DefaultStoreTimeZoneId = dateTimeSettings.DefaultStoreTimeZoneId; - var isWindows = Grand.Infrastructure.OperatingSystem.IsWindows(); - foreach (var timeZone in dateTimeService.GetSystemTimeZones()) - { - var name = isWindows ? timeZone.DisplayName : $"{timeZone.StandardName} ({timeZone.Id})"; - model.DateTimeSettings.AvailableTimeZones.Add(new SelectListItem { - Text = name, - Value = timeZone.Id, - Selected = timeZone.Id.Equals(dateTimeSettings.DefaultStoreTimeZoneId, - StringComparison.OrdinalIgnoreCase) - }); - } + model.DateTimeSettings.AvailableTimeZones = dateTimeService.GetStoreTimeZoneOptions( + dateTimeSettings.DefaultStoreTimeZoneId, + translationService.GetResource("Admin.Settings.GeneralCommon.DefaultStoreTimeZone.Server")); + model.DateTimeSettings.DefaultStoreTimeZoneId = + model.DateTimeSettings.AvailableTimeZones.First(x => x.Selected).Value; var storeInformationSettings = await settingService.LoadSetting(storeScope); model.StoreInformationSettings = storeInformationSettings.ToModel(); diff --git a/src/Web/Grand.Web.Vendor/App_Data/appsettings.json b/src/Web/Grand.Web.Vendor/App_Data/appsettings.json index 1d4ce0a7ed..52adfd707b 100644 --- a/src/Web/Grand.Web.Vendor/App_Data/appsettings.json +++ b/src/Web/Grand.Web.Vendor/App_Data/appsettings.json @@ -45,8 +45,6 @@ "Database": { //This setting is required to use LiteDB database "UseLiteDb": false, - //LiteDB database as a singleton service in the application - "Singleton": true, //Init connection string - it's required only for the installation process! "LiteDbConnectionString": "Filename=App_Data/database.db", //Storefront keyword search (catalog search and autocomplete) narrows candidates through a text index, diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml index 7dd15fd123..6feffdd128 100644 Binary files a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml and b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml differ diff --git a/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml b/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml index b549b75b9b..1a1df48f7a 100644 --- a/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml +++ b/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml @@ -765,4 +765,7 @@ Payment overdue + + Server time zone + diff --git a/src/Web/Grand.Web/App_Data/appsettings.json b/src/Web/Grand.Web/App_Data/appsettings.json index 8cc0c015e1..39578c2ac5 100644 --- a/src/Web/Grand.Web/App_Data/appsettings.json +++ b/src/Web/Grand.Web/App_Data/appsettings.json @@ -56,8 +56,6 @@ "Database": { //This setting is required to use LiteDB database "UseLiteDb": false, - //LiteDB database as a singleton service in the application - "Singleton": true, //Init connection string - it's required only for the installation process! "LiteDbConnectionString": "Filename=App_Data/database.db", //Storefront keyword search (catalog search and autocomplete) narrows candidates through a text index, diff --git a/src/Web/Grand.Web/vueapp/src/compat/validate.js b/src/Web/Grand.Web/vueapp/src/compat/validate.js index 810fffbb30..7f13d3ce7f 100644 --- a/src/Web/Grand.Web/vueapp/src/compat/validate.js +++ b/src/Web/Grand.Web/vueapp/src/compat/validate.js @@ -66,6 +66,9 @@ const messages = { const isEmpty = v => v === '' || v === null || v === undefined +//What getValue() reports for a field left as it was: an unticked checkbox is false, a group is []. +const isUnanswered = v => isEmpty(v) || v === false || (Array.isArray(v) && v.length === 0) + const validators = { required(value, params) { const allowFalse = params && typeof params === 'object' && 'allowFalse' in params @@ -168,6 +171,18 @@ export const ValidationProvider = { mounted() { this._onEvent = () => this.validate() + /* + * Leaving a field nobody has touched is not an attempt to fill it in, so it + * reports nothing; submitting still does. The message used to appear on + * mousedown, when an autofocused field lost focus, and move the layout - the + * button under the pointer slid away before mouseup and the click was lost. + * A field being typed in validates on `input`, so its layout is already settled. + */ + this._onFocusOut = () => { + if (this.state === null && isUnanswered(this.getValue())) return + this.validate() + } + /* * Controls that commit on `change` must not be validated on `input`. * A real click on a checkbox dispatches click, input and change as @@ -188,13 +203,13 @@ export const ValidationProvider = { if (!commitsOnChange) this.$el.addEventListener('input', this._onEvent) this.$el.addEventListener('change', this._onEvent) - this.$el.addEventListener('focusout', this._onEvent) + this.$el.addEventListener('focusout', this._onFocusOut) }, unmounted() { if (this.$el && this._onEvent) { this.$el.removeEventListener('input', this._onEvent) this.$el.removeEventListener('change', this._onEvent) - this.$el.removeEventListener('focusout', this._onEvent) + this.$el.removeEventListener('focusout', this._onFocusOut) } }, methods: { diff --git a/src/Web/Grand.Web/wwwroot/bundles/app.runtime.bundle.js b/src/Web/Grand.Web/wwwroot/bundles/app.runtime.bundle.js index 1d58022418..b472856af2 100644 --- a/src/Web/Grand.Web/wwwroot/bundles/app.runtime.bundle.js +++ b/src/Web/Grand.Web/wwwroot/bundles/app.runtime.bundle.js @@ -103,7 +103,7 @@ var BO=Object.create(null);function VO(e,t){if(!W(e)){if(e.nodeType)e=e.innerHTM :style="{ height: '3.5rem', cursor: 'pointer', opacity: i === index ? 1 : 0.5 }" @click="goTo(i)"> - `};function xk(e){e.component(`b-form-rating`,vk),e.component(`countdown`,yk),e.component(`vue-gallery-slideshow`,bk)}function Sk(e,t,n){if(!e)return null;let r=n||document,i=typeof CSS<`u`&&CSS.escape?CSS.escape(e):e;return r.querySelector(`[name="${i}"]`)?.getAttribute(`data-val-`+t)||null}function Ck(e,t,n){let r=(window.grandValidationMessages||{})[e],i=r&&r!==`validation.`+e?r:t;return n===void 0?i:String(i).replace(`{0}`,n)}var wk={required:(e,t,n,r)=>Sk(e,`required`,r)||n||Ck(`required`,`This field is required.`),email:(e,t,n,r)=>Sk(e,`email`,r)||Ck(`email`,`This field must be a valid email.`),confirmed:(e,t,n,r)=>Sk(e,`equalto`,r)||Ck(`confirmed`,`The confirmation does not match.`),min:(e,t,n,r)=>Sk(e,`min`,r)||Ck(`minlength`,`This field should be at least {0} characters.`,t),max:(e,t,n,r)=>Sk(e,`max`,r)||Ck(`maxlength`,`This field should be at most {0} characters.`,t),exact_length:(e,t,n)=>n||Ck(`exactlength`,`Select at least {0} option.`,t)},Tk=e=>e===``||e==null,Ek={required(e,t){let n=t&&typeof t==`object`&&`allowFalse`in t?t.allowFalse:void 0;return n===!1||n===`false`?e===!0:!Tk(e)},email(e){return Tk(e)?!0:/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(e)},min(e,t){if(Tk(e))return!0;let n=Array.isArray(t)?t[0]:t;return String(e).length>=parseInt(n)},max(e,t){if(Tk(e))return!0;let n=Array.isArray(t)?t[0]:t;return String(e).length<=parseInt(n)},confirmed(e,t,n){let r=Array.isArray(t)?t[0]:t;typeof r==`string`&&r.startsWith(`@`)&&(r=r.substring(1));let i=n.observer?n.observer.findProvider(r):null;return!i||e===i.getValue()},exact_length(e,t){if(Tk(e))return!1;let n=Array.isArray(t)?t[0]:t,r=parseInt(n,10),i=Number.isNaN(r)?1:r;return(e.length===void 0?String(e).length:e.length)>=i}};function Dk(e){return e?typeof e==`string`?e.split(`|`).filter(Boolean).map(e=>{let t=e.indexOf(`:`);return t===-1?{name:e,params:null}:{name:e.substring(0,t),params:e.substring(t+1).split(`,`)}}):Object.entries(e).filter(e=>e[1]!==!1).map(([e,t])=>({name:e,params:t===!0?null:t})):[]}var Ok={name:`ValidationProvider`,inject:{observer:{from:`veeObserver`,default:null}},props:{tag:{type:String,default:`div`},rules:{type:[String,Object],default:null},name:{type:String,default:null},vid:{type:String,default:null},mode:{type:String,default:null}},data(){return{errors:[],state:null}},computed:{classes(){return this.state===null?``:this.state?`is-valid`:`is-invalid`},fieldId(){return this.vid||this.name}},created(){this.observer&&this.observer.register(this)},beforeUnmount(){this.observer&&this.observer.unregister(this)},mounted(){this._onEvent=()=>this.validate();let e=this.control();e?.tagName!==`SELECT`&&e?.type!==`checkbox`&&e?.type!==`radio`&&this.$el.addEventListener(`input`,this._onEvent),this.$el.addEventListener(`change`,this._onEvent),this.$el.addEventListener(`focusout`,this._onEvent)},unmounted(){this.$el&&this._onEvent&&(this.$el.removeEventListener(`input`,this._onEvent),this.$el.removeEventListener(`change`,this._onEvent),this.$el.removeEventListener(`focusout`,this._onEvent))},methods:{control(){return this.$el?this.$el.querySelector(`input, select, textarea`):null},getValue(){let e=this.control();if(e){if(e.type===`checkbox`){if(!e.hasAttribute(`value`))return e.checked;let t=this.$el.querySelectorAll(`input[type=checkbox]`);return Array.from(t).filter(e=>e.checked).map(e=>e.value)}if(e.type===`radio`){let e=this.$el.querySelectorAll(`input[type=radio]`);for(let t of e)if(t.checked)return t.value}else return e.value}},fieldName(){let e=this.control();return this.name||(e?e.getAttribute(`name`):null)||this.vid},validate(){let e=this.getValue(),t=[],n=Dk(this.rules),r=n.map(e=>Array.isArray(e.params)?e.params[1]:null).find(Boolean)||null;for(let i of n){let n=Ek[i.name];if(n&&!n(e,i.params,this)){let e=wk[i.name],n=Array.isArray(i.params)?i.params[0]:i.params,a=i.params&&i.params[1]||r;t.push(e?e(this.fieldName(),n,a,this.$el):Ck(`invalid`,`Invalid value.`))}}this.errors=t,this.state=t.length===0;let i=this.control();return i&&(i.classList.toggle(`is-invalid`,t.length>0),i.classList.toggle(`is-valid`,t.length===0&&this.state!==null)),Promise.resolve(t.length===0)},reset(){this.errors=[],this.state=null;let e=this.control();e&&e.classList.remove(`is-invalid`,`is-valid`)}},render(){let e=this.$slots.default?this.$slots.default({errors:this.errors,classes:this.classes,valid:this.state}):[];return Nx(this.tag,null,e)}},kk={name:`ValidationObserver`,props:{tag:{type:String,default:`div`}},provide(){return{veeObserver:this}},data(){return{providers:[]}},computed:{errors(){let e={};return this.providers.forEach(t=>{e[t.fieldId||t.fieldName()]=t.errors}),e},invalid(){return this.providers.some(e=>e.state===!1)}},methods:{register(e){this.providers.push(e)},unregister(e){let t=this.providers.indexOf(e);t>-1&&this.providers.splice(t,1)},findProvider(e){return this.providers.find(t=>t.vid===e||t.name===e)||null},validate(){return Promise.all(this.providers.map(e=>e.validate())).then(e=>e.every(Boolean))},handleSubmit(e){return this.validate().then(t=>{t&&typeof e==`function`&&e()})},reset(){this.providers.forEach(e=>e.reset())}},render(){let e=this.$slots.default?this.$slots.default({handleSubmit:this.handleSubmit,validate:this.validate,reset:this.reset,errors:this.errors,invalid:this.invalid}):[];return Nx(this.tag,null,e)}};function Ak(e){e.component(`validation-provider`,Ok),e.component(`validation-observer`,kk)}var jk=null;function Mk(){return jk||(jk=document.createElement(`div`),jk.className=`toast-container position-fixed top-0 end-0 p-3`,jk.style.zIndex=`1090`,document.body.appendChild(jk)),jk}var Nk={toast(e,t={}){let n=t.variant||`info`,r=document.createElement(`div`);r.className=`toast show align-items-center border-`+n,r.setAttribute(`role`,`alert`);let i=document.createElement(`div`);i.className=`toast-header text-bg-`+n;let a=document.createElement(`strong`);a.className=`me-auto`,a.innerText=t.title||``;let o=document.createElement(`button`);o.type=`button`,o.className=`btn-close btn-close-white ms-2`,o.addEventListener(`click`,()=>r.remove()),i.appendChild(a),i.appendChild(o),r.appendChild(i);let s=document.createElement(`div`);if(s.className=`toast-body`,t.href){let n=document.createElement(`a`);n.href=t.href,n.innerText=e,s.appendChild(n)}else s.innerText=e;r.appendChild(s),Mk().appendChild(r);let c=t.autoHideDelay||5e3;t.noAutoHide||setTimeout(()=>r.remove(),c)}},Pk={};function Z(e,t){Pk[e]=t}function Fk(e){let t=e.textContent.trim();if(!t)return{};try{return JSON.parse(t)}catch(t){return console.error(`[grand] view "${e.dataset.grandVm}" has an unparseable payload`,t),null}}var Ik=`script[type="application/json"][data-grand-vm]`;function Lk(e,t=[]){return e.querySelectorAll(Ik).forEach(e=>t.push(e)),e.querySelectorAll(`template`).forEach(e=>Lk(e.content,t)),t}function Rk(e=document){Lk(e).forEach(e=>{if(e.dataset.grandVmReady)return;let t=e.dataset.grandVm,n=Pk[t];if(!n){console.error(`[grand] no view-model registered for "${t}"`);return}let r=Fk(e);r!==null&&(e.dataset.grandVmReady=`1`,n(r,e))})}function zk(e){let t=document.querySelector(`#ModalQuickView.show`);return t?t.querySelector(`#product-details-form`):e?document.querySelector(e):null}function Bk(e){document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,e):e()}Bk(function(){let e=document.querySelector(`input[name=__RequestVerificationToken]`);if(!e)return;let t=e.value;R.defaults.headers.common[`X-CSRF-TOKEN`]=t});function Vk(e,t,n){n===void 0&&(n=0),e.length>0&&e[n](function(){n++,n===e.length?t():Vk(e,t,n)})}function Hk(){var e=document.createEvent(`Event`);e.initEvent(`DOMContentLoaded`,!0,!0),document.dispatchEvent(e)}function Uk(e,t){var n=document.createElement(`script`);n.type=`text/javascript`,e.src?(n.onload=t,n.onerror=t,n.src=e.src):n.textContent=e.innerText,document.body.appendChild(n),e.parentNode.removeChild(e),e.src||t()}var Wk=[`application/javascript`,`application/ecmascript`,`application/x-ecmascript`,`application/x-javascript`,`text/ecmascript`,`text/javascript`,`text/javascript1.0`,`text/javascript1.1`,`text/javascript1.2`,`text/javascript1.3`,`text/javascript1.4`,`text/javascript1.5`,`text/jscript`,`text/livescript`,`text/x-ecmascript`,`text/x-javascript`];function Gk(e){var t=e.querySelectorAll(`script`),n=[],r;[].forEach.call(t,function(e){r=e.getAttribute(`type`),r&&Wk.indexOf(r)===-1||n.push(function(t){Uk(e,t)})}),Vk(n,Hk)}function Kk(e){e.stopPropagation()}(function(){function e(e,t){return t||=document,t.querySelectorAll(e)}function t(e,t){for(var n in Object.keys(e))t(e[n])}function n(){var t=e(`ul`,this)[0];t&&!t.classList.contains(`-visible`)&&t.classList.add(`-visible`)}function r(){var e=this.parentElement;e&&e.classList.contains(`-visible`)&&setTimeout(function(){e.classList.remove(`-visible`)},300)}window.addEventListener(`load`,function(){t(e(`.Menu li.-hasSubmenu`),function(e){e.showMenu=n,e.hideMenu=r}),t(e(`.Menu > li.-hasSubmenu`),function(e){e.addEventListener(`click`,n)}),t(e(`.Menu li .back`),function(e){e.addEventListener(`click`,r)}),t(e(`.Menu > li.-hasSubmenu li.-hasSubmenu`),function(e){e.addEventListener(`click`,n)})})})();var Q={availableDates:[],availableDatesFrom:[],availableDatesTo:[],currentMonth:-1,currentYear:-1,currentMonthFrom:-1,currentYearFrom:-1,currentMonthTo:-1,currentYearTo:-1,_parameter:``,noReservationsMessage:``,productId:``,ajaxUrl:``,ajaxUrl2:``,startDate:``,startDateMonth:``,startDateYear:``,init:function(e,t,n,r,i,a,o){this.noReservationsMessage=r,this.ajaxUrl=i,this.ajaxUrl2=o,this.productId=a,this.startDate=e,this.startDateMonth=n,this.startDateYear=t,document.getElementById(`reservationDatepicker`)!=null&&this.fillAvailableDates(t,n,Q._parameter,!1),document.getElementById(`reservationDatepickerFrom`)!=null&&this.fillAvailableDatesFrom(t,n),document.getElementById(`reservationDatepickerTo`)!=null&&this.fillAvailableDatesTo(t,n);var s=new Date(this.startDate),c=new af.default({field:document.getElementById(`reservationDatepicker`),onSelect:this.onDatePickerDateChange,disableDayFn:this.daysToMark,format:`MM/DD/YYYY`,toString:function(e){let t=(`0`+e.getDate()).slice(-2);return`${(`0`+(e.getMonth()+1)).slice(-2)}/${t}/${e.getFullYear()}`},parse:function(e){let t=e.split(`/`),n=t[0],r=parseInt(t[1],10),i=parseInt(t[2],10);return new Date(i,r,n)},onDraw:function(e){var t=e.calendars[0].year.toString(),n=(e.calendars[0].month+1).toString();document.getElementById(`hoursDiv`)!=null&&(document.getElementById(`hoursDiv`).innerHTML=``),Q.fillAvailableDates(t,n,Q._parameter,!1),Q.onDatePickerDateChange()},firstDay:1,defaultDate:s});new af.default({field:document.getElementById(`reservationDatepickerFrom`),onSelect:this.onDatePickerSelect,disableDayFn:this.daysToMarkFrom,format:`MM/DD/YYYY`,toString:function(e){let t=(`0`+e.getDate()).slice(-2);return`${(`0`+(e.getMonth()+1)).slice(-2)}/${t}/${e.getFullYear()}`},parse:function(e){let t=e.split(`/`),n=parseInt(t[0],10),r=parseInt(t[1],10),i=parseInt(t[2],10);return new Date(i,r,n)},onDraw:function(e){var t=e.calendars[0].year.toString(),n=(e.calendars[0].month+1).toString();Q.fillAvailableDatesFrom(t,n)},firstDay:1,defaultDate:s}),new af.default({field:document.getElementById(`reservationDatepickerTo`),onSelect:this.onDatePickerSelect,disableDayFn:this.daysToMarkTo,format:`MM/DD/YYYY`,toString:function(e){let t=(`0`+e.getDate()).slice(-2);return`${(`0`+(e.getMonth()+1)).slice(-2)}/${t}/${e.getFullYear()}`},parse:function(e){let t=e.split(`/`),n=parseInt(t[0],10),r=parseInt(t[1],10),i=parseInt(t[2],10);return new Date(i,r,n)},onDraw:function(e){var t=e.calendars[0].year.toString(),n=(e.calendars[0].month+1).toString();Q.fillAvailableDatesTo(t,n)},firstDay:1,defaultDate:s}),this.onDatePickerDateChange(),document.getElementById(`parameterDropdown`)!=null&&document.querySelector(`#parameterDropdown`).addEventListener(`change`,function(){Q.fillAvailableDates(Q.currentYear,Q.currentMonth,this.value),c.clear(),document.getElementById(`hoursDiv`)!=null&&(document.getElementById(`hoursDiv`).innerHTML=Q.noReservationsMessage)})},reload:function(e,t,n){this.startDate=e,this.startDateMonth=n,this.startDateYear=t,this.fillAvailableDates(t,n,Q._parameter,!1),new af.default({field:document.getElementById(`reservationDatepicker`),onSelect:this.onDatePickerDateChange,disableDayFn:this.daysToMark,format:`MM/DD/YYYY`,toString:function(e){let t=(`0`+e.getDate()).slice(-2);return`${e.getMonth()+1}/${t}/${e.getFullYear()}`},parse:function(e){let t=e.split(`/`),n=parseInt(t[0],10),r=parseInt(t[1],10)-1,i=parseInt(t[2],10);return new Date(i,r,n)},onDraw:function(e){var t=e.calendars[0].year.toString(),n=(e.calendars[0].month+1).toString();document.getElementById(`hoursDiv`)!=null&&(document.getElementById(`hoursDiv`).innerHTML=``),Q.fillAvailableDates(t,n,Q._parameter,!1)},firstDay:1,defaultDate:this.startDate}),this.onDatePickerDateChange()},daysToMark:function(e){for(let a=0;a`}}if(Q.availableDates.length==0){var f=document.createElement(`label`);f.innerHTML=Q.noReservationsMessage,document.querySelector(`#hoursDiv`).appendChild(f)}}},fillAvailableDates:function(e,t,n){var r={productId:Q.productId,month:t,year:e,parameter:n};R({url:Q.ajaxUrl,method:`post`,params:r,headers:{Accept:`application/json`,"Content-Type":`application/json`}}).then(r=>{Q.currentMonth=t,Q.currentYear=e,Q.availableDates=r.data,Q._parameter=n}).catch(function(e){alert(e)})},fillAvailableDatesFrom:function(e,t){var n={productId:Q.productId,month:t,year:e,parameter:null};R({url:Q.ajaxUrl,method:`post`,params:n,headers:{Accept:`application/json`,"Content-Type":`application/json`}}).then(n=>(Q.currentMonthFrom=t,Q.currentYearFrom=e,Q.availableDatesFrom=Q.availableDatesFrom.concat(n.data),!0)).catch(function(e){alert(e)})},fillAvailableDatesTo:function(e,t){var n={productId:Q.productId,month:t,year:e,parameter:null};R({url:Q.ajaxUrl,method:`post`,params:n,headers:{Accept:`application/json`,"Content-Type":`application/json`}}).then(n=>{Q.currentMonthTo=t,Q.currentYearTo=e,Q.availableDatesTo=Q.availableDatesTo.concat(n.data)}).catch(function(e){alert(e)})},onDatePickerSelect:function(){var e=zk(`#product-details-form-`+Q.productId);if(e){var t=new FormData(e);R({url:Q.ajaxUrl2,method:`post`,data:t}).then(e=>{var t=function(e,t){var n=document.querySelector(e);n&&(n.innerText=t)};e.data.sku&&t(`#sku-`+Q.productId,e.data.sku),e.data.mpn&&t(`#mpn-`+Q.productId,e.data.mpn),e.data.gtin&&t(`#gtin-`+Q.productId,e.data.gtin),e.data.price&&(document.querySelector(`.price-value-`+Q.productId+` .actual-price`)?t(`.price-value-`+Q.productId+` .actual-price`,e.data.price):t(`.price-value-`+Q.productId,e.data.price))})}}};window.runScripts=Gk,window.StopPropagation=Kk,window.Reservation=Q;var qk={loadWaiting:!1,sidebarcartselector:`.sidebar-cart`,quickview_product:function(e){R({url:e,method:`get`}).then(function(e){qk.success_process(e)}).catch(function(e){e.axiosFailure}).then(function(){qk.resetLoadWaiting()})},addproducttocart_catalog:function(e,t,n){if(t.toLowerCase()==`true`){var r=document.querySelector(`#addtocart_`+n+`_EnteredQuantity`).value;e.indexOf(`forceredirection`)==-1?e+=`?quantity=`+r:e+=`&quantity=`+r}this.loadWaiting==0&&(this.setLoadWaiting(!0),R({url:e,method:`post`}).then(function(e){qk.success_process(e)}).catch(function(e){e.axiosFailure}).then(function(){window.vmwishlist!==void 0&&window.vmwishlist.getModel(),window.vmorder!==void 0&&window.vmorder.getModel(),qk.resetLoadWaiting()}))},addproducttocart_details:function(e,t){if(this.loadWaiting==0){this.setLoadWaiting(!0);var n=zk(t);n?R({url:e,data:new FormData(n),method:`post`}).then(function(e){qk.success_process(e)}).catch(function(e){e.axiosFailure}).then(function(){window.vmwishlist!==void 0&&window.vmwishlist.getModel(),window.vmorder!==void 0&&window.vmorder.getModel(),qk.resetLoadWaiting()}):this.setLoadWaiting(!1)}},updateitem:function(e,t){var n,r=zk(t);if(r){var i=new FormData(r);n=window.vmwishlist===void 0?window.vmorder:window.vmwishlist,R({url:e,data:i,method:`post`}).then(function(e){e.data.success?window.bootstrap.Modal.getOrCreateInstance(document.getElementById(`ModalQuickView`)).hide():n.displayWarning(e.data.message,`danger`)}).catch(function(e){e.axiosFailure}).then(function(){n.getModel(),qk.resetLoadWaiting()})}},addbid:function(e,t){if(this.loadWaiting==0){this.setLoadWaiting(!0);var n=document.querySelector(t);R({url:e,data:new FormData(n),method:`post`}).then(function(e){qk.success_process(e)}).catch(function(e){e.axiosFailure}).then(function(){qk.resetLoadWaiting()})}},addproducttocomparelist:function(e,t,n){if(this.loadWaiting==0){this.setLoadWaiting(!0);var r=this.getCookie(`Grand.CompareProduct`);return r===``?r=e:r.includes(e)||(r=r+`|`+e),this.setCookie(`Grand.CompareProduct`,r),window.vm.updateCompareProductsQty(),window.vm.displayBarNotification(t,n,`success`,3500),this.resetLoadWaiting(),!1}},setLoadWaiting:function(e){this.loadWaiting=e},success_process:function(e){if(e.data.updatetopwishlistsectionhtml&&document.querySelector(`.wishlist-qty`)&&(document.querySelector(`.wishlist-qty`).innerHTML=e.data.updatetopwishlistsectionhtml),e.data.sidebarshoppingcartmodel){var t=e.data.sidebarshoppingcartmodel;window.vm.flycart=t,window.vm.flycartitems=t.Items,window.vm.flycartindicator=t.TotalProducts}if(e.data.updatetopcartsectionhtml&&(window.vm.flycartindicator=e.data.updatetopcartsectionhtml),e.data.product&&e.data.success==1&&(window.vm.PopupQuickViewVueModal=e.data.model,Object.assign(window.vm.PopupQuickViewVueModal,{RelatedProducts:[]}),window.bootstrap.Modal.getOrCreateInstance(document.getElementById(`ModalQuickView`)).show(),e.data.model.ProductType==20)){var n=new Date(e.data.model.StartDate).toLocaleDateString(`en-US`),r=new Date(e.data.model.StartDate).getFullYear(),i=new Date(e.data.model.StartDate).getUTCMonth()+1;Object.assign(window.vm.PopupQuickViewVueModal,{ReservationFullDate:n}),Object.assign(window.vm.PopupQuickViewVueModal,{ReservationYear:r}),Object.assign(window.vm.PopupQuickViewVueModal,{ReservationMonth:i})}if(e.data.message){if(e.data.success==1){if(window.vm.PopupAddToCartVueModal=e.data.model,window.bootstrap.Modal.getOrCreateInstance(document.getElementById(`ModalQuickView`)).hide(),window.bootstrap.Modal.getOrCreateInstance(document.getElementById(`ModalAddToCart`)).show(),e.data.refreshreservation==1){var a=document.querySelector(`#parameterDropdown`),o=a?a.value:``;window.Reservation.fillAvailableDates(window.Reservation.currentYear,window.Reservation.currentMonth,o,!0)}}else window.vm.displayBarNotification(e.data.message,``,`error`,3500);return!1}return e.data.redirect?(location.href=e.data.redirect,!0):!1},resetLoadWaiting:function(){qk.setLoadWaiting(!1)},axiosFailure:function(){alert(`Failed to add the product. Please refresh the page and try one more time.`)},setCookie:function(e,t,n){let r=new Date;r.setTime(r.getTime()+n*24*60*60*1e3);let i=`expires=`+r.toUTCString();document.cookie=e+`=`+t+`;`+i+`;path=/`},getCookie:function(e){let t=e+`=`,n=document.cookie.split(`;`);for(let e=0;et.$nextTick(e))}function Yk(e,t,n){document.addEventListener(e,e=>{let r=e.target.closest?.(t);r&&n(r,e)})}Z(`advancedSearch`,({elements:e})=>{let t=()=>{let t=document.getElementById(e.checkboxId),n=document.getElementById(e.blockId);t&&n&&(n.style.display=t.checked?`flex`:`none`)};Yk(`click`,`#`+e.checkboxId,t),Jk(t)});function Xk(){return document.querySelector(`input[name=__RequestVerificationToken]`)?.value??``}function Zk(e={}){let t=new FormData;return Object.entries(e).forEach(([e,n])=>t.append(e,n)),t.append(`__RequestVerificationToken`,Xk()),t}function Qk(e,t,n=`danger`){e&&Nk.toast(e,{title:t,variant:n,autoHideDelay:5e3,appendToast:!0})}function $(e,t,n){Qk(e?.response?.data?.message||n||e?.message||String(e),t)}function $k(e){navigator.permissions&&navigator.permissions.query({name:`microphone`}).then(t=>{e(t.state!==`denied`),t.onchange=()=>e(t.state!==`denied`)}).catch(()=>{})}function eA(e,t){let n=(e,n)=>(e||[]).forEach(e=>{let r=document.querySelector(`#${t}_attribute_label_${e}`),i=document.querySelector(`#${t}_attribute_input_${e}`);r&&(r.style.display=n),i&&(i.style.display=n)});n(e.enabledattributeids,`block`),n(e.disabledattributeids,`none`)}function tA(e,t,n=document){let r=n.querySelector(`#download-message, .download-message`);r&&(r.style.display=`block`,r.classList.toggle(`alert-info`,t),r.classList.toggle(`alert-danger`,!t),r.innerText=e)}function nA(e,t=document){let n=t.querySelector(`.download-file`);n&&(n.style.display=`inline-block`,n.setAttribute(`href`,e))}Z(`checkoutAttributes`,({routes:e})=>{let t=()=>{let t=document.querySelector(`#shopping-cart-form`);if(t)return R.post(e.change,new FormData(t)).then(({data:e})=>{let t=window.vmorder;t&&(t.totals=e.model,t.cart.CheckoutAttributeInfo=e.checkoutattributeinfo||``),eA(e,`checkout`)}).catch(e=>console.error(`[grand] checkout attribute change failed`,e))};window.checkoutAttributeChange=t,window.uploadFile=e=>{let n=new FormData;n.append(`file`,e.files[0]);let r=e.getAttribute(`attribute`);R.post(e.getAttribute(`data-url`),n).then(({data:n})=>{if(n.success){let t=window.vmorder?.cart.CheckoutAttributes.find(e=>e.Id===r);t&&(t.DefaultValue=n.downloadGuid),e.setAttribute(`qq-button-id`,n.downloadGuid);let i=document.querySelector(`.hidden-upload-input`);i&&(i.value=n.downloadGuid),nA(n.downloadUrl)}return tA(n.message,n.success),t()}).catch(e=>console.error(`[grand] checkout attribute upload failed`,e))},Jk(t)}),Z(`contactAttributes`,({routes:e})=>{let t=()=>{let t=document.querySelector(`#contactus-form`);if(t)return R.post(e.change,new FormData(t)).then(({data:e})=>eA(e,`contact`)).catch(e=>console.error(`[grand] contact attribute change failed`,e))};window.contactAttributeChange=t,window.uploadFile=e=>{let n=new FormData;n.append(`file`,e.files[0]);let r=e.closest(`.contact-attribute-upload`)||document;R.post(e.getAttribute(`data-url`),n).then(({data:e})=>{if(e.success){let t=r.querySelector(`.hidden-upload-input`);t&&(t.value=e.downloadGuid),nA(e.downloadUrl,r)}return tA(e.message,e.success,r),t()}).catch(e=>console.error(`[grand] contact attribute upload failed`,e))},Yk(`change`,`.contact-attributes [id^="contact_attribute_"]`,t),Yk(`click`,`.contact-attributes [id^="contact_attribute_"]`,t),Yk(`click`,`.contact-attributes [id^="color-squares-"] li`,e=>{document.querySelector(`.contact-attributes .selected-value`)?.classList.remove(`selected-value`),e.classList.add(`selected-value`)}),Jk(t)}),Z(`barNotifications`,({messages:e})=>{ik(t=>{e.forEach(({text:e,variant:n})=>t.displayBarNotification(e,``,n,3500))})}),Z(`checkoutSteps`,({routes:e})=>{Jk(()=>{let t=window.vmorder;t?.vCartUrl?(t.vCartUrl.init(e.cartSummary,e.cartTotal),t.vShipping.init(`#co-shipping-form`,e.saveShipping),t.vBilling.init(`#co-billing-form`,e.saveBilling),t.vShippingMethod.init(`#co-shipping-method-form`,e.saveShippingMethod),t.vPaymentMethod.init(`#co-payment-method-form`,e.savePaymentMethod),t.vPaymentInfo.init(`#co-payment-info-form`,e.savePaymentInfo),t.vConfirmOrder.init(e.confirmOrder,e.completed)):console.error(`[grand] checkout controllers missing - public.checkout.js did not load`)})}),Z(`jsResources`,({res:e,routes:t,validation:n})=>{window.grandRes=Object.assign(window.grandRes||{},e),window.grandRoutes=Object.assign(window.grandRoutes||{},t),window.grandValidationMessages=Object.assign(window.grandValidationMessages||{},n)}),Z(`confirmDelete`,({route:e,field:t,selector:n,res:r})=>{document.addEventListener(`click`,i=>{let a=i.target.closest(n);a&&window.confirm(r.areYouSure)&&R.post(e,Zk({[t]:a.dataset.deleteId})).then(e=>{e.data.success===!1?Qk(e.data.error||e.data.message,r.warning):window.location=e.data.redirect}).catch(e=>$(e,r.warning))})}),Z(`confirmPost`,({route:e,elements:t,res:n})=>{Yk(`click`,`#`+t.buttonId,r=>{window.confirm(n.areYouSure)&&R.post(e).then(()=>{r.style.display=`none`;let e=document.getElementById(t.checkId);e&&(e.checked=!0)}).catch(e=>$(e,n.warning))})}),Z(`estimateShipping`,({routes:e,elements:t,res:n})=>{Yk(`click`,`#`+t.buttonId,()=>{let t=document.getElementById(`shopping-cart-form`);t&&R.post(e.estimate,new FormData(t)).then(e=>{let t=document.querySelector(`.estimate-shipping-result`);t&&(t.innerHTML=e.data),window.checkoutAttributeChange?.()}).catch(()=>Qk(n.estimateFailed,n.warning))}),Yk(`change`,`#`+t.countrySelectId,r=>{let i=document.getElementById(t.stateSelectId),a=document.getElementById(t.progressId);a&&(a.style.display=`block`),R.get(e.getStates,{params:{countryId:r.value,addSelectStateItem:!1}}).then(e=>{i&&(i.innerHTML=``,e.data.forEach(e=>{let t=document.createElement(`option`);t.value=e.id,t.innerHTML=e.name,i.appendChild(t)})),a&&(a.style.display=`none`),window.checkoutAttributeChange?.()}).catch(e=>{a&&(a.style.display=`none`),$(e,n.warning)})})}),Z(`cookieBar`,({route:e,elements:t,res:n})=>{Yk(`click`,`#`+t.barId+` [data-accept]`,r=>{R.post(e,null,{params:{accept:r.dataset.accept}}).then(()=>{let e=document.getElementById(t.barId);e&&(e.style.display=`none`)}).catch(e=>$(e,n.warning))})}),Z(`geolocation`,({route:e,known:t})=>{navigator.geolocation&&navigator.geolocation.getCurrentPosition(n=>{let{latitude:r,longitude:i}=n.coords;if(r===t.latitude&&i===t.longitude)return;let a=new FormData;a.append(`latitude`,r),a.append(`longitude`,i),R.post(e,a).catch(e=>console.error(`[grand] position update failed`,e))})}),Z(`inDomComponents`,({components:e})=>{e.forEach(({name:e,template:t,props:n})=>{ek(e,{template:t,props:(n||[]).reduce((e,t)=>(e[t]=null,e),{})})})}),Z(`passwordPage`,({route:e,elements:t,submitOnEnter:n,res:r})=>{let i=e=>e?document.getElementById(e):null,a=()=>{let n=i(t.pageIdFieldId),a=i(t.passwordId);n&&a&&R.post(e,Zk({id:n.value,password:a.value})).then(e=>{let n=i(t.errorId);if(!e.data.Authenticated){n&&(n.innerText=e.data.Error);return}let r=i(t.pageId),a=r?.querySelector(t.titleSelector);if(a&&(a.innerHTML=e.data.Title,!a.innerText.trim())){let e=i(t.titleId);e&&(e.style.display=`none`)}let o=r?.querySelector(`.page-body`);o&&(o.innerHTML=e.data.Body);let s=i(t.promptId);s&&(s.style.display=`none`),r&&(r.style.display=`block`)}).catch(e=>$(e,r.warning))};Jk(()=>{let e=i(t.pageId);e&&(e.style.display=`none`)}),t.buttonId&&Yk(`click`,`#`+t.buttonId,a),n&&Yk(`keydown`,`#`+t.passwordId,(e,t)=>{t.key===`Enter`&&a()})});function rA(e){let t=e.dataset.bundleProduct,n=document.getElementById(`product-details-form`);if(n)return R.post(e.dataset.attributeRoute,new FormData(n),{params:{product:t}}).then(e=>{let n=document.querySelector(`.price-value-`+t);n&&e.data.price&&(n.innerText=e.data.price)}).catch(e=>console.error(`[grand] bundle attribute change failed`,e))}Yk(`change`,`[data-bundle-product] [id^="product_attribute_"]`,e=>rA(e.closest(`[data-bundle-product]`))),Yk(`click`,`[data-bundle-product] [id^="product_attribute_"]`,e=>rA(e.closest(`[data-bundle-product]`))),Z(`productAttributesBundle`,({componentName:e,templateId:t,containerId:n,attributes:r})=>{ek(e,{template:t,data:()=>({productAttributes:r}),mounted(){let e=document.getElementById(n);e&&rA(e)}})});var iA={main:e=>`
+
`};function xk(e){e.component(`b-form-rating`,vk),e.component(`countdown`,yk),e.component(`vue-gallery-slideshow`,bk)}function Sk(e,t,n){if(!e)return null;let r=n||document,i=typeof CSS<`u`&&CSS.escape?CSS.escape(e):e;return r.querySelector(`[name="${i}"]`)?.getAttribute(`data-val-`+t)||null}function Ck(e,t,n){let r=(window.grandValidationMessages||{})[e],i=r&&r!==`validation.`+e?r:t;return n===void 0?i:String(i).replace(`{0}`,n)}var wk={required:(e,t,n,r)=>Sk(e,`required`,r)||n||Ck(`required`,`This field is required.`),email:(e,t,n,r)=>Sk(e,`email`,r)||Ck(`email`,`This field must be a valid email.`),confirmed:(e,t,n,r)=>Sk(e,`equalto`,r)||Ck(`confirmed`,`The confirmation does not match.`),min:(e,t,n,r)=>Sk(e,`min`,r)||Ck(`minlength`,`This field should be at least {0} characters.`,t),max:(e,t,n,r)=>Sk(e,`max`,r)||Ck(`maxlength`,`This field should be at most {0} characters.`,t),exact_length:(e,t,n)=>n||Ck(`exactlength`,`Select at least {0} option.`,t)},Tk=e=>e===``||e==null,Ek=e=>Tk(e)||e===!1||Array.isArray(e)&&e.length===0,Dk={required(e,t){let n=t&&typeof t==`object`&&`allowFalse`in t?t.allowFalse:void 0;return n===!1||n===`false`?e===!0:!Tk(e)},email(e){return Tk(e)?!0:/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(e)},min(e,t){if(Tk(e))return!0;let n=Array.isArray(t)?t[0]:t;return String(e).length>=parseInt(n)},max(e,t){if(Tk(e))return!0;let n=Array.isArray(t)?t[0]:t;return String(e).length<=parseInt(n)},confirmed(e,t,n){let r=Array.isArray(t)?t[0]:t;typeof r==`string`&&r.startsWith(`@`)&&(r=r.substring(1));let i=n.observer?n.observer.findProvider(r):null;return!i||e===i.getValue()},exact_length(e,t){if(Tk(e))return!1;let n=Array.isArray(t)?t[0]:t,r=parseInt(n,10),i=Number.isNaN(r)?1:r;return(e.length===void 0?String(e).length:e.length)>=i}};function Ok(e){return e?typeof e==`string`?e.split(`|`).filter(Boolean).map(e=>{let t=e.indexOf(`:`);return t===-1?{name:e,params:null}:{name:e.substring(0,t),params:e.substring(t+1).split(`,`)}}):Object.entries(e).filter(e=>e[1]!==!1).map(([e,t])=>({name:e,params:t===!0?null:t})):[]}var kk={name:`ValidationProvider`,inject:{observer:{from:`veeObserver`,default:null}},props:{tag:{type:String,default:`div`},rules:{type:[String,Object],default:null},name:{type:String,default:null},vid:{type:String,default:null},mode:{type:String,default:null}},data(){return{errors:[],state:null}},computed:{classes(){return this.state===null?``:this.state?`is-valid`:`is-invalid`},fieldId(){return this.vid||this.name}},created(){this.observer&&this.observer.register(this)},beforeUnmount(){this.observer&&this.observer.unregister(this)},mounted(){this._onEvent=()=>this.validate(),this._onFocusOut=()=>{this.state===null&&Ek(this.getValue())||this.validate()};let e=this.control();e?.tagName!==`SELECT`&&e?.type!==`checkbox`&&e?.type!==`radio`&&this.$el.addEventListener(`input`,this._onEvent),this.$el.addEventListener(`change`,this._onEvent),this.$el.addEventListener(`focusout`,this._onFocusOut)},unmounted(){this.$el&&this._onEvent&&(this.$el.removeEventListener(`input`,this._onEvent),this.$el.removeEventListener(`change`,this._onEvent),this.$el.removeEventListener(`focusout`,this._onFocusOut))},methods:{control(){return this.$el?this.$el.querySelector(`input, select, textarea`):null},getValue(){let e=this.control();if(e){if(e.type===`checkbox`){if(!e.hasAttribute(`value`))return e.checked;let t=this.$el.querySelectorAll(`input[type=checkbox]`);return Array.from(t).filter(e=>e.checked).map(e=>e.value)}if(e.type===`radio`){let e=this.$el.querySelectorAll(`input[type=radio]`);for(let t of e)if(t.checked)return t.value}else return e.value}},fieldName(){let e=this.control();return this.name||(e?e.getAttribute(`name`):null)||this.vid},validate(){let e=this.getValue(),t=[],n=Ok(this.rules),r=n.map(e=>Array.isArray(e.params)?e.params[1]:null).find(Boolean)||null;for(let i of n){let n=Dk[i.name];if(n&&!n(e,i.params,this)){let e=wk[i.name],n=Array.isArray(i.params)?i.params[0]:i.params,a=i.params&&i.params[1]||r;t.push(e?e(this.fieldName(),n,a,this.$el):Ck(`invalid`,`Invalid value.`))}}this.errors=t,this.state=t.length===0;let i=this.control();return i&&(i.classList.toggle(`is-invalid`,t.length>0),i.classList.toggle(`is-valid`,t.length===0&&this.state!==null)),Promise.resolve(t.length===0)},reset(){this.errors=[],this.state=null;let e=this.control();e&&e.classList.remove(`is-invalid`,`is-valid`)}},render(){let e=this.$slots.default?this.$slots.default({errors:this.errors,classes:this.classes,valid:this.state}):[];return Nx(this.tag,null,e)}},Ak={name:`ValidationObserver`,props:{tag:{type:String,default:`div`}},provide(){return{veeObserver:this}},data(){return{providers:[]}},computed:{errors(){let e={};return this.providers.forEach(t=>{e[t.fieldId||t.fieldName()]=t.errors}),e},invalid(){return this.providers.some(e=>e.state===!1)}},methods:{register(e){this.providers.push(e)},unregister(e){let t=this.providers.indexOf(e);t>-1&&this.providers.splice(t,1)},findProvider(e){return this.providers.find(t=>t.vid===e||t.name===e)||null},validate(){return Promise.all(this.providers.map(e=>e.validate())).then(e=>e.every(Boolean))},handleSubmit(e){return this.validate().then(t=>{t&&typeof e==`function`&&e()})},reset(){this.providers.forEach(e=>e.reset())}},render(){let e=this.$slots.default?this.$slots.default({handleSubmit:this.handleSubmit,validate:this.validate,reset:this.reset,errors:this.errors,invalid:this.invalid}):[];return Nx(this.tag,null,e)}};function jk(e){e.component(`validation-provider`,kk),e.component(`validation-observer`,Ak)}var Mk=null;function Nk(){return Mk||(Mk=document.createElement(`div`),Mk.className=`toast-container position-fixed top-0 end-0 p-3`,Mk.style.zIndex=`1090`,document.body.appendChild(Mk)),Mk}var Pk={toast(e,t={}){let n=t.variant||`info`,r=document.createElement(`div`);r.className=`toast show align-items-center border-`+n,r.setAttribute(`role`,`alert`);let i=document.createElement(`div`);i.className=`toast-header text-bg-`+n;let a=document.createElement(`strong`);a.className=`me-auto`,a.innerText=t.title||``;let o=document.createElement(`button`);o.type=`button`,o.className=`btn-close btn-close-white ms-2`,o.addEventListener(`click`,()=>r.remove()),i.appendChild(a),i.appendChild(o),r.appendChild(i);let s=document.createElement(`div`);if(s.className=`toast-body`,t.href){let n=document.createElement(`a`);n.href=t.href,n.innerText=e,s.appendChild(n)}else s.innerText=e;r.appendChild(s),Nk().appendChild(r);let c=t.autoHideDelay||5e3;t.noAutoHide||setTimeout(()=>r.remove(),c)}},Fk={};function Z(e,t){Fk[e]=t}function Ik(e){let t=e.textContent.trim();if(!t)return{};try{return JSON.parse(t)}catch(t){return console.error(`[grand] view "${e.dataset.grandVm}" has an unparseable payload`,t),null}}var Lk=`script[type="application/json"][data-grand-vm]`;function Rk(e,t=[]){return e.querySelectorAll(Lk).forEach(e=>t.push(e)),e.querySelectorAll(`template`).forEach(e=>Rk(e.content,t)),t}function zk(e=document){Rk(e).forEach(e=>{if(e.dataset.grandVmReady)return;let t=e.dataset.grandVm,n=Fk[t];if(!n){console.error(`[grand] no view-model registered for "${t}"`);return}let r=Ik(e);r!==null&&(e.dataset.grandVmReady=`1`,n(r,e))})}function Bk(e){let t=document.querySelector(`#ModalQuickView.show`);return t?t.querySelector(`#product-details-form`):e?document.querySelector(e):null}function Vk(e){document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,e):e()}Vk(function(){let e=document.querySelector(`input[name=__RequestVerificationToken]`);if(!e)return;let t=e.value;R.defaults.headers.common[`X-CSRF-TOKEN`]=t});function Hk(e,t,n){n===void 0&&(n=0),e.length>0&&e[n](function(){n++,n===e.length?t():Hk(e,t,n)})}function Uk(){var e=document.createEvent(`Event`);e.initEvent(`DOMContentLoaded`,!0,!0),document.dispatchEvent(e)}function Wk(e,t){var n=document.createElement(`script`);n.type=`text/javascript`,e.src?(n.onload=t,n.onerror=t,n.src=e.src):n.textContent=e.innerText,document.body.appendChild(n),e.parentNode.removeChild(e),e.src||t()}var Gk=[`application/javascript`,`application/ecmascript`,`application/x-ecmascript`,`application/x-javascript`,`text/ecmascript`,`text/javascript`,`text/javascript1.0`,`text/javascript1.1`,`text/javascript1.2`,`text/javascript1.3`,`text/javascript1.4`,`text/javascript1.5`,`text/jscript`,`text/livescript`,`text/x-ecmascript`,`text/x-javascript`];function Kk(e){var t=e.querySelectorAll(`script`),n=[],r;[].forEach.call(t,function(e){r=e.getAttribute(`type`),r&&Gk.indexOf(r)===-1||n.push(function(t){Wk(e,t)})}),Hk(n,Uk)}function qk(e){e.stopPropagation()}(function(){function e(e,t){return t||=document,t.querySelectorAll(e)}function t(e,t){for(var n in Object.keys(e))t(e[n])}function n(){var t=e(`ul`,this)[0];t&&!t.classList.contains(`-visible`)&&t.classList.add(`-visible`)}function r(){var e=this.parentElement;e&&e.classList.contains(`-visible`)&&setTimeout(function(){e.classList.remove(`-visible`)},300)}window.addEventListener(`load`,function(){t(e(`.Menu li.-hasSubmenu`),function(e){e.showMenu=n,e.hideMenu=r}),t(e(`.Menu > li.-hasSubmenu`),function(e){e.addEventListener(`click`,n)}),t(e(`.Menu li .back`),function(e){e.addEventListener(`click`,r)}),t(e(`.Menu > li.-hasSubmenu li.-hasSubmenu`),function(e){e.addEventListener(`click`,n)})})})();var Q={availableDates:[],availableDatesFrom:[],availableDatesTo:[],currentMonth:-1,currentYear:-1,currentMonthFrom:-1,currentYearFrom:-1,currentMonthTo:-1,currentYearTo:-1,_parameter:``,noReservationsMessage:``,productId:``,ajaxUrl:``,ajaxUrl2:``,startDate:``,startDateMonth:``,startDateYear:``,init:function(e,t,n,r,i,a,o){this.noReservationsMessage=r,this.ajaxUrl=i,this.ajaxUrl2=o,this.productId=a,this.startDate=e,this.startDateMonth=n,this.startDateYear=t,document.getElementById(`reservationDatepicker`)!=null&&this.fillAvailableDates(t,n,Q._parameter,!1),document.getElementById(`reservationDatepickerFrom`)!=null&&this.fillAvailableDatesFrom(t,n),document.getElementById(`reservationDatepickerTo`)!=null&&this.fillAvailableDatesTo(t,n);var s=new Date(this.startDate),c=new af.default({field:document.getElementById(`reservationDatepicker`),onSelect:this.onDatePickerDateChange,disableDayFn:this.daysToMark,format:`MM/DD/YYYY`,toString:function(e){let t=(`0`+e.getDate()).slice(-2);return`${(`0`+(e.getMonth()+1)).slice(-2)}/${t}/${e.getFullYear()}`},parse:function(e){let t=e.split(`/`),n=t[0],r=parseInt(t[1],10),i=parseInt(t[2],10);return new Date(i,r,n)},onDraw:function(e){var t=e.calendars[0].year.toString(),n=(e.calendars[0].month+1).toString();document.getElementById(`hoursDiv`)!=null&&(document.getElementById(`hoursDiv`).innerHTML=``),Q.fillAvailableDates(t,n,Q._parameter,!1),Q.onDatePickerDateChange()},firstDay:1,defaultDate:s});new af.default({field:document.getElementById(`reservationDatepickerFrom`),onSelect:this.onDatePickerSelect,disableDayFn:this.daysToMarkFrom,format:`MM/DD/YYYY`,toString:function(e){let t=(`0`+e.getDate()).slice(-2);return`${(`0`+(e.getMonth()+1)).slice(-2)}/${t}/${e.getFullYear()}`},parse:function(e){let t=e.split(`/`),n=parseInt(t[0],10),r=parseInt(t[1],10),i=parseInt(t[2],10);return new Date(i,r,n)},onDraw:function(e){var t=e.calendars[0].year.toString(),n=(e.calendars[0].month+1).toString();Q.fillAvailableDatesFrom(t,n)},firstDay:1,defaultDate:s}),new af.default({field:document.getElementById(`reservationDatepickerTo`),onSelect:this.onDatePickerSelect,disableDayFn:this.daysToMarkTo,format:`MM/DD/YYYY`,toString:function(e){let t=(`0`+e.getDate()).slice(-2);return`${(`0`+(e.getMonth()+1)).slice(-2)}/${t}/${e.getFullYear()}`},parse:function(e){let t=e.split(`/`),n=parseInt(t[0],10),r=parseInt(t[1],10),i=parseInt(t[2],10);return new Date(i,r,n)},onDraw:function(e){var t=e.calendars[0].year.toString(),n=(e.calendars[0].month+1).toString();Q.fillAvailableDatesTo(t,n)},firstDay:1,defaultDate:s}),this.onDatePickerDateChange(),document.getElementById(`parameterDropdown`)!=null&&document.querySelector(`#parameterDropdown`).addEventListener(`change`,function(){Q.fillAvailableDates(Q.currentYear,Q.currentMonth,this.value),c.clear(),document.getElementById(`hoursDiv`)!=null&&(document.getElementById(`hoursDiv`).innerHTML=Q.noReservationsMessage)})},reload:function(e,t,n){this.startDate=e,this.startDateMonth=n,this.startDateYear=t,this.fillAvailableDates(t,n,Q._parameter,!1),new af.default({field:document.getElementById(`reservationDatepicker`),onSelect:this.onDatePickerDateChange,disableDayFn:this.daysToMark,format:`MM/DD/YYYY`,toString:function(e){let t=(`0`+e.getDate()).slice(-2);return`${e.getMonth()+1}/${t}/${e.getFullYear()}`},parse:function(e){let t=e.split(`/`),n=parseInt(t[0],10),r=parseInt(t[1],10)-1,i=parseInt(t[2],10);return new Date(i,r,n)},onDraw:function(e){var t=e.calendars[0].year.toString(),n=(e.calendars[0].month+1).toString();document.getElementById(`hoursDiv`)!=null&&(document.getElementById(`hoursDiv`).innerHTML=``),Q.fillAvailableDates(t,n,Q._parameter,!1)},firstDay:1,defaultDate:this.startDate}),this.onDatePickerDateChange()},daysToMark:function(e){for(let a=0;a`}}if(Q.availableDates.length==0){var f=document.createElement(`label`);f.innerHTML=Q.noReservationsMessage,document.querySelector(`#hoursDiv`).appendChild(f)}}},fillAvailableDates:function(e,t,n){var r={productId:Q.productId,month:t,year:e,parameter:n};R({url:Q.ajaxUrl,method:`post`,params:r,headers:{Accept:`application/json`,"Content-Type":`application/json`}}).then(r=>{Q.currentMonth=t,Q.currentYear=e,Q.availableDates=r.data,Q._parameter=n}).catch(function(e){alert(e)})},fillAvailableDatesFrom:function(e,t){var n={productId:Q.productId,month:t,year:e,parameter:null};R({url:Q.ajaxUrl,method:`post`,params:n,headers:{Accept:`application/json`,"Content-Type":`application/json`}}).then(n=>(Q.currentMonthFrom=t,Q.currentYearFrom=e,Q.availableDatesFrom=Q.availableDatesFrom.concat(n.data),!0)).catch(function(e){alert(e)})},fillAvailableDatesTo:function(e,t){var n={productId:Q.productId,month:t,year:e,parameter:null};R({url:Q.ajaxUrl,method:`post`,params:n,headers:{Accept:`application/json`,"Content-Type":`application/json`}}).then(n=>{Q.currentMonthTo=t,Q.currentYearTo=e,Q.availableDatesTo=Q.availableDatesTo.concat(n.data)}).catch(function(e){alert(e)})},onDatePickerSelect:function(){var e=Bk(`#product-details-form-`+Q.productId);if(e){var t=new FormData(e);R({url:Q.ajaxUrl2,method:`post`,data:t}).then(e=>{var t=function(e,t){var n=document.querySelector(e);n&&(n.innerText=t)};e.data.sku&&t(`#sku-`+Q.productId,e.data.sku),e.data.mpn&&t(`#mpn-`+Q.productId,e.data.mpn),e.data.gtin&&t(`#gtin-`+Q.productId,e.data.gtin),e.data.price&&(document.querySelector(`.price-value-`+Q.productId+` .actual-price`)?t(`.price-value-`+Q.productId+` .actual-price`,e.data.price):t(`.price-value-`+Q.productId,e.data.price))})}}};window.runScripts=Kk,window.StopPropagation=qk,window.Reservation=Q;var Jk={loadWaiting:!1,sidebarcartselector:`.sidebar-cart`,quickview_product:function(e){R({url:e,method:`get`}).then(function(e){Jk.success_process(e)}).catch(function(e){e.axiosFailure}).then(function(){Jk.resetLoadWaiting()})},addproducttocart_catalog:function(e,t,n){if(t.toLowerCase()==`true`){var r=document.querySelector(`#addtocart_`+n+`_EnteredQuantity`).value;e.indexOf(`forceredirection`)==-1?e+=`?quantity=`+r:e+=`&quantity=`+r}this.loadWaiting==0&&(this.setLoadWaiting(!0),R({url:e,method:`post`}).then(function(e){Jk.success_process(e)}).catch(function(e){e.axiosFailure}).then(function(){window.vmwishlist!==void 0&&window.vmwishlist.getModel(),window.vmorder!==void 0&&window.vmorder.getModel(),Jk.resetLoadWaiting()}))},addproducttocart_details:function(e,t){if(this.loadWaiting==0){this.setLoadWaiting(!0);var n=Bk(t);n?R({url:e,data:new FormData(n),method:`post`}).then(function(e){Jk.success_process(e)}).catch(function(e){e.axiosFailure}).then(function(){window.vmwishlist!==void 0&&window.vmwishlist.getModel(),window.vmorder!==void 0&&window.vmorder.getModel(),Jk.resetLoadWaiting()}):this.setLoadWaiting(!1)}},updateitem:function(e,t){var n,r=Bk(t);if(r){var i=new FormData(r);n=window.vmwishlist===void 0?window.vmorder:window.vmwishlist,R({url:e,data:i,method:`post`}).then(function(e){e.data.success?window.bootstrap.Modal.getOrCreateInstance(document.getElementById(`ModalQuickView`)).hide():n.displayWarning(e.data.message,`danger`)}).catch(function(e){e.axiosFailure}).then(function(){n.getModel(),Jk.resetLoadWaiting()})}},addbid:function(e,t){if(this.loadWaiting==0){this.setLoadWaiting(!0);var n=document.querySelector(t);R({url:e,data:new FormData(n),method:`post`}).then(function(e){Jk.success_process(e)}).catch(function(e){e.axiosFailure}).then(function(){Jk.resetLoadWaiting()})}},addproducttocomparelist:function(e,t,n){if(this.loadWaiting==0){this.setLoadWaiting(!0);var r=this.getCookie(`Grand.CompareProduct`);return r===``?r=e:r.includes(e)||(r=r+`|`+e),this.setCookie(`Grand.CompareProduct`,r),window.vm.updateCompareProductsQty(),window.vm.displayBarNotification(t,n,`success`,3500),this.resetLoadWaiting(),!1}},setLoadWaiting:function(e){this.loadWaiting=e},success_process:function(e){if(e.data.updatetopwishlistsectionhtml&&document.querySelector(`.wishlist-qty`)&&(document.querySelector(`.wishlist-qty`).innerHTML=e.data.updatetopwishlistsectionhtml),e.data.sidebarshoppingcartmodel){var t=e.data.sidebarshoppingcartmodel;window.vm.flycart=t,window.vm.flycartitems=t.Items,window.vm.flycartindicator=t.TotalProducts}if(e.data.updatetopcartsectionhtml&&(window.vm.flycartindicator=e.data.updatetopcartsectionhtml),e.data.product&&e.data.success==1&&(window.vm.PopupQuickViewVueModal=e.data.model,Object.assign(window.vm.PopupQuickViewVueModal,{RelatedProducts:[]}),window.bootstrap.Modal.getOrCreateInstance(document.getElementById(`ModalQuickView`)).show(),e.data.model.ProductType==20)){var n=new Date(e.data.model.StartDate).toLocaleDateString(`en-US`),r=new Date(e.data.model.StartDate).getFullYear(),i=new Date(e.data.model.StartDate).getUTCMonth()+1;Object.assign(window.vm.PopupQuickViewVueModal,{ReservationFullDate:n}),Object.assign(window.vm.PopupQuickViewVueModal,{ReservationYear:r}),Object.assign(window.vm.PopupQuickViewVueModal,{ReservationMonth:i})}if(e.data.message){if(e.data.success==1){if(window.vm.PopupAddToCartVueModal=e.data.model,window.bootstrap.Modal.getOrCreateInstance(document.getElementById(`ModalQuickView`)).hide(),window.bootstrap.Modal.getOrCreateInstance(document.getElementById(`ModalAddToCart`)).show(),e.data.refreshreservation==1){var a=document.querySelector(`#parameterDropdown`),o=a?a.value:``;window.Reservation.fillAvailableDates(window.Reservation.currentYear,window.Reservation.currentMonth,o,!0)}}else window.vm.displayBarNotification(e.data.message,``,`error`,3500);return!1}return e.data.redirect?(location.href=e.data.redirect,!0):!1},resetLoadWaiting:function(){Jk.setLoadWaiting(!1)},axiosFailure:function(){alert(`Failed to add the product. Please refresh the page and try one more time.`)},setCookie:function(e,t,n){let r=new Date;r.setTime(r.getTime()+n*24*60*60*1e3);let i=`expires=`+r.toUTCString();document.cookie=e+`=`+t+`;`+i+`;path=/`},getCookie:function(e){let t=e+`=`,n=document.cookie.split(`;`);for(let e=0;et.$nextTick(e))}function Xk(e,t,n){document.addEventListener(e,e=>{let r=e.target.closest?.(t);r&&n(r,e)})}Z(`advancedSearch`,({elements:e})=>{let t=()=>{let t=document.getElementById(e.checkboxId),n=document.getElementById(e.blockId);t&&n&&(n.style.display=t.checked?`flex`:`none`)};Xk(`click`,`#`+e.checkboxId,t),Yk(t)});function Zk(){return document.querySelector(`input[name=__RequestVerificationToken]`)?.value??``}function Qk(e={}){let t=new FormData;return Object.entries(e).forEach(([e,n])=>t.append(e,n)),t.append(`__RequestVerificationToken`,Zk()),t}function $k(e,t,n=`danger`){e&&Pk.toast(e,{title:t,variant:n,autoHideDelay:5e3,appendToast:!0})}function $(e,t,n){$k(e?.response?.data?.message||n||e?.message||String(e),t)}function eA(e){navigator.permissions&&navigator.permissions.query({name:`microphone`}).then(t=>{e(t.state!==`denied`),t.onchange=()=>e(t.state!==`denied`)}).catch(()=>{})}function tA(e,t){let n=(e,n)=>(e||[]).forEach(e=>{let r=document.querySelector(`#${t}_attribute_label_${e}`),i=document.querySelector(`#${t}_attribute_input_${e}`);r&&(r.style.display=n),i&&(i.style.display=n)});n(e.enabledattributeids,`block`),n(e.disabledattributeids,`none`)}function nA(e,t,n=document){let r=n.querySelector(`#download-message, .download-message`);r&&(r.style.display=`block`,r.classList.toggle(`alert-info`,t),r.classList.toggle(`alert-danger`,!t),r.innerText=e)}function rA(e,t=document){let n=t.querySelector(`.download-file`);n&&(n.style.display=`inline-block`,n.setAttribute(`href`,e))}Z(`checkoutAttributes`,({routes:e})=>{let t=()=>{let t=document.querySelector(`#shopping-cart-form`);if(t)return R.post(e.change,new FormData(t)).then(({data:e})=>{let t=window.vmorder;t&&(t.totals=e.model,t.cart.CheckoutAttributeInfo=e.checkoutattributeinfo||``),tA(e,`checkout`)}).catch(e=>console.error(`[grand] checkout attribute change failed`,e))};window.checkoutAttributeChange=t,window.uploadFile=e=>{let n=new FormData;n.append(`file`,e.files[0]);let r=e.getAttribute(`attribute`);R.post(e.getAttribute(`data-url`),n).then(({data:n})=>{if(n.success){let t=window.vmorder?.cart.CheckoutAttributes.find(e=>e.Id===r);t&&(t.DefaultValue=n.downloadGuid),e.setAttribute(`qq-button-id`,n.downloadGuid);let i=document.querySelector(`.hidden-upload-input`);i&&(i.value=n.downloadGuid),rA(n.downloadUrl)}return nA(n.message,n.success),t()}).catch(e=>console.error(`[grand] checkout attribute upload failed`,e))},Yk(t)}),Z(`contactAttributes`,({routes:e})=>{let t=()=>{let t=document.querySelector(`#contactus-form`);if(t)return R.post(e.change,new FormData(t)).then(({data:e})=>tA(e,`contact`)).catch(e=>console.error(`[grand] contact attribute change failed`,e))};window.contactAttributeChange=t,window.uploadFile=e=>{let n=new FormData;n.append(`file`,e.files[0]);let r=e.closest(`.contact-attribute-upload`)||document;R.post(e.getAttribute(`data-url`),n).then(({data:e})=>{if(e.success){let t=r.querySelector(`.hidden-upload-input`);t&&(t.value=e.downloadGuid),rA(e.downloadUrl,r)}return nA(e.message,e.success,r),t()}).catch(e=>console.error(`[grand] contact attribute upload failed`,e))},Xk(`change`,`.contact-attributes [id^="contact_attribute_"]`,t),Xk(`click`,`.contact-attributes [id^="contact_attribute_"]`,t),Xk(`click`,`.contact-attributes [id^="color-squares-"] li`,e=>{document.querySelector(`.contact-attributes .selected-value`)?.classList.remove(`selected-value`),e.classList.add(`selected-value`)}),Yk(t)}),Z(`barNotifications`,({messages:e})=>{ik(t=>{e.forEach(({text:e,variant:n})=>t.displayBarNotification(e,``,n,3500))})}),Z(`checkoutSteps`,({routes:e})=>{Yk(()=>{let t=window.vmorder;t?.vCartUrl?(t.vCartUrl.init(e.cartSummary,e.cartTotal),t.vShipping.init(`#co-shipping-form`,e.saveShipping),t.vBilling.init(`#co-billing-form`,e.saveBilling),t.vShippingMethod.init(`#co-shipping-method-form`,e.saveShippingMethod),t.vPaymentMethod.init(`#co-payment-method-form`,e.savePaymentMethod),t.vPaymentInfo.init(`#co-payment-info-form`,e.savePaymentInfo),t.vConfirmOrder.init(e.confirmOrder,e.completed)):console.error(`[grand] checkout controllers missing - public.checkout.js did not load`)})}),Z(`jsResources`,({res:e,routes:t,validation:n})=>{window.grandRes=Object.assign(window.grandRes||{},e),window.grandRoutes=Object.assign(window.grandRoutes||{},t),window.grandValidationMessages=Object.assign(window.grandValidationMessages||{},n)}),Z(`confirmDelete`,({route:e,field:t,selector:n,res:r})=>{document.addEventListener(`click`,i=>{let a=i.target.closest(n);a&&window.confirm(r.areYouSure)&&R.post(e,Qk({[t]:a.dataset.deleteId})).then(e=>{e.data.success===!1?$k(e.data.error||e.data.message,r.warning):window.location=e.data.redirect}).catch(e=>$(e,r.warning))})}),Z(`confirmPost`,({route:e,elements:t,res:n})=>{Xk(`click`,`#`+t.buttonId,r=>{window.confirm(n.areYouSure)&&R.post(e).then(()=>{r.style.display=`none`;let e=document.getElementById(t.checkId);e&&(e.checked=!0)}).catch(e=>$(e,n.warning))})}),Z(`estimateShipping`,({routes:e,elements:t,res:n})=>{Xk(`click`,`#`+t.buttonId,()=>{let t=document.getElementById(`shopping-cart-form`);t&&R.post(e.estimate,new FormData(t)).then(e=>{let t=document.querySelector(`.estimate-shipping-result`);t&&(t.innerHTML=e.data),window.checkoutAttributeChange?.()}).catch(()=>$k(n.estimateFailed,n.warning))}),Xk(`change`,`#`+t.countrySelectId,r=>{let i=document.getElementById(t.stateSelectId),a=document.getElementById(t.progressId);a&&(a.style.display=`block`),R.get(e.getStates,{params:{countryId:r.value,addSelectStateItem:!1}}).then(e=>{i&&(i.innerHTML=``,e.data.forEach(e=>{let t=document.createElement(`option`);t.value=e.id,t.innerHTML=e.name,i.appendChild(t)})),a&&(a.style.display=`none`),window.checkoutAttributeChange?.()}).catch(e=>{a&&(a.style.display=`none`),$(e,n.warning)})})}),Z(`cookieBar`,({route:e,elements:t,res:n})=>{Xk(`click`,`#`+t.barId+` [data-accept]`,r=>{R.post(e,null,{params:{accept:r.dataset.accept}}).then(()=>{let e=document.getElementById(t.barId);e&&(e.style.display=`none`)}).catch(e=>$(e,n.warning))})}),Z(`geolocation`,({route:e,known:t})=>{navigator.geolocation&&navigator.geolocation.getCurrentPosition(n=>{let{latitude:r,longitude:i}=n.coords;if(r===t.latitude&&i===t.longitude)return;let a=new FormData;a.append(`latitude`,r),a.append(`longitude`,i),R.post(e,a).catch(e=>console.error(`[grand] position update failed`,e))})}),Z(`inDomComponents`,({components:e})=>{e.forEach(({name:e,template:t,props:n})=>{ek(e,{template:t,props:(n||[]).reduce((e,t)=>(e[t]=null,e),{})})})}),Z(`passwordPage`,({route:e,elements:t,submitOnEnter:n,res:r})=>{let i=e=>e?document.getElementById(e):null,a=()=>{let n=i(t.pageIdFieldId),a=i(t.passwordId);n&&a&&R.post(e,Qk({id:n.value,password:a.value})).then(e=>{let n=i(t.errorId);if(!e.data.Authenticated){n&&(n.innerText=e.data.Error);return}let r=i(t.pageId),a=r?.querySelector(t.titleSelector);if(a&&(a.innerHTML=e.data.Title,!a.innerText.trim())){let e=i(t.titleId);e&&(e.style.display=`none`)}let o=r?.querySelector(`.page-body`);o&&(o.innerHTML=e.data.Body);let s=i(t.promptId);s&&(s.style.display=`none`),r&&(r.style.display=`block`)}).catch(e=>$(e,r.warning))};Yk(()=>{let e=i(t.pageId);e&&(e.style.display=`none`)}),t.buttonId&&Xk(`click`,`#`+t.buttonId,a),n&&Xk(`keydown`,`#`+t.passwordId,(e,t)=>{t.key===`Enter`&&a()})});function iA(e){let t=e.dataset.bundleProduct,n=document.getElementById(`product-details-form`);if(n)return R.post(e.dataset.attributeRoute,new FormData(n),{params:{product:t}}).then(e=>{let n=document.querySelector(`.price-value-`+t);n&&e.data.price&&(n.innerText=e.data.price)}).catch(e=>console.error(`[grand] bundle attribute change failed`,e))}Xk(`change`,`[data-bundle-product] [id^="product_attribute_"]`,e=>iA(e.closest(`[data-bundle-product]`))),Xk(`click`,`[data-bundle-product] [id^="product_attribute_"]`,e=>iA(e.closest(`[data-bundle-product]`))),Z(`productAttributesBundle`,({componentName:e,templateId:t,containerId:n,attributes:r})=>{ek(e,{template:t,data:()=>({productAttributes:r}),mounted(){let e=document.getElementById(n);e&&iA(e)}})});var aA={main:e=>`
@@ -113,4 +113,4 @@ var BO=Object.create(null);function VO(e,t){if(!W(e)){if(e.nodeType)e=e.innerHTM
-
`};Z(`productGallery`,({componentName:e,variant:t,mainImageId:n,images:r})=>{ek(e,{template:iA[t](n),data:()=>({images:r,index:null}),components:{VueGallerySlideshow:bk}})}),Z(`galleryThumbnails`,({mainImageId:e})=>{Yk(`click`,`[data-gallery-thumb]`,t=>{let n=document.getElementById(e);n&&n.setAttribute(`src`,t.getAttribute(`data-src`))})}),Z(`imageZoom`,()=>{Yk(`click`,`[data-zoom-image]`,e=>{let t=e.parentElement?.querySelector(`img`);t&&window.mediumZoom&&window.mediumZoom(t,{background:`#232323`,margin:30}).open()})}),Z(`pushNotifications`,({firebase:e,route:t})=>{Jk(()=>{window.PushNotifications&&(window.PushNotifications.init(e.publicApiKey,e.senderId,e.projectId,e.authDomain,e.storageBucket,e.databaseUrl,t,e.appId),window.PushNotifications.process())})});function aA(e,t){let n=parseInt(e.value,10);e.value=Math.max(1,(isNaN(n)?0:n)+t)}Z(`quantityStepper`,()=>{Yk(`click`,`[data-quantity-step]`,e=>{let t=document.getElementById(e.dataset.quantityTarget);t&&aA(t,Number(e.dataset.quantityStep))})}),Z(`quickViewModal`,({elements:e})=>{Yk(`shown.bs.modal`,`#`+e.modalId,()=>{let e=window.vm;e&&(e.initReservationQV?.(),e.QuickViewShown?.())})}),Z(`reservationInfo`,({productId:e,start:t,routes:n,res:r})=>{Jk(()=>{window.Reservation?.init(t.date,t.year,t.month,r.noAvailableReservations,n.datesForMonth,e,n.attributeChange)})}),Z(`searchModal`,({elements:e})=>{Yk(`shown.bs.modal`,`#`+e.modalId,()=>{let e=tk()?.$refs?.searchBoxInput;e&&e.focus()})});var oA=991;Z(`twoColumnsSidebar`,({elements:e})=>{Jk(()=>{let t=document.querySelector(e.sidebarSelector),n=document.getElementById(e.desktopHostId);if(!t||!n)return;let r=document.getElementById(e.toggleId),i=Array.prototype.some.call(t.children,t=>t.id!==e.categoryNavigationId);r&&!i&&(r.classList.remove(`d-block`),r.classList.add(`d-none`));let a=()=>{let r=(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)<=oA?document.querySelector(e.mobileHostSelector):n;r&&t.parentElement!==r&&r.appendChild(t)};a(),window.addEventListener(`resize`,a)})}),Z(`enableWhenChecked`,({elements:e})=>{Yk(`change`,`#`+e.checkboxId,t=>{let n=document.getElementById(e.targetId);n&&n.classList.toggle(`disabled`,!t.checked)})}),Z(`checkAll`,({elements:e})=>{Yk(`click`,`#`+e.checkboxId,t=>{document.querySelectorAll(e.rowSelector).forEach(e=>{e.checked=t.checked})})});var sA=`username-available-status alert alert-success d-flex justify-content-center mt-3 mb-0`,cA=`username-not-available-status alert alert-danger d-flex justify-content-center mt-3 mb-0`,lA=`username-not-available-status text-danger d-flex order-2 mt-3`;Z(`usernameAvailability`,({route:e,elements:t,res:n})=>{Yk(`click`,`#`+t.buttonId,r=>{let i=document.getElementById(t.usernameId),a=document.getElementById(t.resultId),o=document.getElementById(t.progressId);if(!i||!a)return;if(a.innerText=``,!i.value){a.setAttribute(`class`,lA),a.innerText=n.enterUsername;return}o&&(o.style.display=`block`),r.setAttribute(`disabled`,`disabled`);let s=()=>{r.removeAttribute(`disabled`),o&&(o.style.display=`none`)};R.post(e,Zk({Username:i.value})).then(e=>{s(),a.setAttribute(`class`,e.data.Available?sA:cA),a.innerText=e.data.Text}).catch(e=>{s(),$(e,n.warning)})})});function uA(e,t){if(!t)return;let n=document.querySelector(e);n&&(n.innerText=t)}function dA(e,t){e&&e.forEach(e=>{let n=document.querySelector(`#product_attribute_label_`+e),r=document.querySelector(`#product_attribute_input_`+e);n&&(n.style.display=t),r&&(r.style.display=t)})}function fA(e,t){let n=document.getElementById(`stock-availability-schema-`+e);if(!t)n?.remove();else if(n)n.setAttribute(`href`,t);else{let n=document.querySelector(`[data-offer-for="${e}"]`);if(n){let r=document.createElement(`link`);r.id=`stock-availability-schema-`+e,r.setAttribute(`itemprop`,`availability`),r.setAttribute(`href`,t),n.prepend(r)}}let r=document.getElementById(`product-structured-data-`+e);if(r)try{let e=JSON.parse(r.textContent);if(!e.offers)return;t?e.offers.availability=t:delete e.offers.availability,r.textContent=JSON.stringify(e)}catch(e){console.error(`[grand] product structured data could not be updated`,e)}}function pA(e,t,n){if(!t)return;let r=()=>{let n=document.getElementById(`main-product-img-`+e);n&&n.setAttribute(`src`,t)};if(n!==`swiper`||document.querySelector(`.product-grouped`)){r();return}let i=window.vm?.$refs?.swiperTop?.$swiper;i?i.slides[i.activeIndex]?.querySelector(`img`)?.dataset.srcs!==t&&i.slides.forEach((e,n)=>{e.querySelector(`img`)?.dataset.srcs===t&&i.slideTo(n,1e3,!1)}):r()}Z(`productAttributes`,({productId:e,attributes:t,route:n,picture:r,res:i})=>{let a=hk({data:()=>({ProductAttributes:t}),methods:{attrchange(){let t=document.getElementById(`product-details-form-`+e);if(t)return R.post(n,new FormData(t),{params:{product:e}}).then(({data:t})=>{let n=document.querySelector(`.price-value-${e} .actual-price`)||document.querySelector(`.price-value-${e}`);t.price&&n&&(n.innerText=t.price),uA(`#sku-`+e,t.sku),uA(`#mpn-`+e,t.mpn),uA(`#gtin-`+e,t.gtin),uA(`#stock-availability-value-`+e,t.stockAvailability),`availability`in t&&fA(e,t.availability);let i=document.querySelector(`#out-of-stock-subscribe-`+e);i&&typeof t.outOfStockSubscription==`boolean`&&(i.style.display=t.outOfStockSubscription?`block`:`none`),i&&t.buttonTextOutOfStockSubscription&&(i.value=t.buttonTextOutOfStockSubscription),dA(t.enabledattributemappingids,`table-cell`),dA(t.disabledattributemappingids,`none`),setTimeout(()=>pA(e,t.pictureDefaultSizeUrl,r),100)}).catch(e=>$(e,i.warning))}}}),o=`standardProductAttributes_`+e;window[o]=QO(o,a),ik(e=>e.$nextTick(()=>a.attrchange()))}),Z(`warehouseSelector`,({productId:e,route:t,elements:n})=>{Yk(`change`,`#`+n.selectId,()=>{let r=document.getElementById(n.selectId);if(!r)return;let i=new FormData;i.append(`warehouseId`,r.value),i.append(`productId`,e),R.post(t,i).then(t=>{let n=document.getElementById(`stock-availability-value-`+e);n&&t.data.stockAvailability&&(n.innerText=t.data.stockAvailability),`availability`in t.data&&fA(e,t.data.availability),window[`standardProductAttributes_`+e]?.attrchange()}).catch(e=>console.error(`[grand] warehouse change failed`,e))})}),Z(`state`,({name:e,data:t})=>{window[e]=QO(e,hk({data:()=>({...t})}))}),Z(`applyVendor`,({model:e,res:t})=>{window.applyvendor=QO(`applyvendor`,hk({data:()=>({Name:e.Name,Email:e.Email,AcceptPrivacyPolicy:!1})}));let n=document.getElementById(`apply-vendor`),r=document.getElementById(`accept-terms-of-service`);n&&r&&n.addEventListener(`click`,e=>{r.checked||(e.preventDefault(),Qk(t.acceptTermsRequired,t.warning))})});var mA=[`AskQuestionEmail`,`AskQuestionFullName`,`AskQuestionPhone`,`AskQuestionMessage`];Z(`askQuestion`,({data:e,captchaTarget:t,res:n})=>{window.askquestion=QO(`askquestion`,hk({data:()=>({...e}),methods:{async sendContactUsForm(e){let t=tk().$refs;if(!await t.contact.validate())return;let r={};mA.forEach(e=>{r[e]=t[e].value}),r.Id=t.AskQuestionProductId.value;let i=document.querySelector(`[id^='g-recaptcha-response']`);return i&&(r[`g-recaptcha-response-value`]=i.value),R.post(e,Zk(r)).then(e=>{let t=document.querySelector(`.product-standard`);if(!t)return;let n=t.querySelector(`.product-contact-error`),r=t.querySelector(`.product-contact-send`);e.data.success?(t.querySelector(`#contact-us-product`).style.display=`none`,n.style.display=`none`,r.innerHTML=e.data.message,r.style.display=`block`):(n.innerHTML=e.data.message,n.style.display=`block`)}).catch(e=>$(e,n.warning))},getCaptcha(){if(!t)return;let e=document.getElementById(`captcha-box`),n=document.getElementById(t);e&&n&&n.prepend(e)}}}))});var hA=4;function gA(e){let t=Math.max(1,e.PageNumber-hA),n=e.PageNumber+hA((e[n[t]]=e[n[t]]||[]).push(n),e),{}):e||{}}function yA(e,t){if(!t)return;let n=t.PagingFilteringContext;if(!n)return;e.pager=gA(n);let r=n.SpecificationFilter;r&&(r.NotFilteredItems=vA(r.NotFilteredItems,_A),r.AlreadyFilteredItems=vA(r.AlreadyFilteredItems,_A))}Z(`catalog`,({model:e,res:t})=>{window.catalog=QO(`catalog`,hk({data:()=>({Model:[],pager:[],clientRendered:!1}),created(){this.Model=e,yA(this,e)},watch:{Model(e){yA(this,e)}},methods:{loadProducts(e){this.getResponse(e);let t=new URL(e,window.location.origin);t.searchParams.delete(`timestamp`),window.history.replaceState({path:t.href},``,t.href)},setRating(e){let t=new URL(window.location.href);e===``?t.searchParams.delete(`rating`):t.searchParams.set(`rating`,e),t.searchParams.delete(`pagenumber`),this.loadProducts(t.href)},getResponse(e){let n=new URL(e,window.location.origin);return n.searchParams.set(`timestamp`,new Date().getTime()),R.get(n.href,{headers:{Accept:`application/json`}}).then(e=>{this.Model=e.data,yA(this,e.data),this.clientRendered=!0}).catch(e=>{console.error(`[grand] catalog request failed`,e),Qk(t.loadFailed,t.warning)})},goToPage(e){let t=new URL(window.location.href);t.searchParams.set(`pagenumber`,e),this.loadProducts(t.href),this.scrollToSection()},scrollToSection(){let e=document.getElementById(`catalog-products`);e&&window.scrollTo({top:e.offsetTop,left:0,behavior:`smooth`})}}}))}),Z(`catalogModern`,({model:e,res:t})=>{window.catalog=QO(`catalog`,hk({data:()=>({Model:[],pager:[],Count:[]}),created(){this.Model=e,this.refresh(e)},watch:{Model(e){this.refresh(e)}},methods:{refresh(e){e&&(yA(this,e),this.setCount(0))},loadProducts(e){let t=new URL(e,window.location.origin);t.searchParams.delete(`timestamp`),t.searchParams.delete(`pagenumber`),typeof AjaxFilter<`u`?t.searchParams.forEach((e,t)=>{let n=document.querySelector(`#ajaxfilter-form .`+t);n&&(n.value=e),AjaxFilter.setFilter(t)}):this.getResponse(e),window.history.replaceState({path:t.href},``,t.href)},getResponse(e){let n=new URL(e,window.location.origin);return n.searchParams.set(`timestamp`,new Date().getTime()),R.get(n.href,{headers:{Accept:`application/json`,"X-Response-View":`Json`}}).then(e=>{let t=e.data,n=t.PagingFilteringContext.PageNumber;if(n>1&&n!==this.Model.PagingFilteringContext.PageNumber){let e=this.Model.Products;this.Model=t,this.Model.Products=e.concat(t.Products)}else this.Model=t;this.refresh(this.Model),window.scrollpagination&&(window.scrollpagination.stop=!1)}).catch(e=>{console.error(`[grand] catalog request failed`,e),Qk(t.loadFailed,t.warning)})},goToPage(e){let t=new URL(window.location.href);t.searchParams.set(`pagenumber`,e),this.loadProducts(t.href)},setCount(e){let t=e>0?e:1,n=Math.ceil(this.Model.Products.length/t);this.Count={...this.Count,p_numbers:Array.from(Array(n).keys())}}}}))}),Z(`comments`,({name:e,formId:t,comments:n,fields:r})=>{let i={};r.forEach(e=>{i[e]=``}),window[e]=QO(e,hk({data:()=>({Model:n,...i}),methods:{submitComment(){let e=document.getElementById(t);return R.post(e.getAttribute(`action`),new FormData(e),{headers:{Accept:`application/json`}}).then(e=>{let{success:t,message:n,model:i}=e.data;window.vm.displayBarNotification(n,``,t?`success`:`error`,t?3e3:3500),t&&(r.forEach(e=>{this[e]=``}),i&&this.addComment(i))}).catch(e=>console.error(`[grand] comment submit failed`,e))},addComment(e){this.Model.push(e)}}}))}),Z(`compareProducts`,({model:e,specificationAttributes:t,loadRoute:n})=>{window.specificationAttributes=QO(`specificationAttributes`,hk({data:()=>({Model:t})})),ik(t=>{n?t.getCompareList(n):t.compareproducts=e})});function bA(e){let t=document.getElementById(`captcha-box`),n=document.getElementById(e);t&&n&&n.prepend(t)}Z(`contactForm`,({name:e,data:t,formId:n,submitMethod:r,modalId:i,captcha:a,res:o})=>{let s=hk({data:()=>({...t,Message:{Result:null,SuccessfullySent:!1}}),methods:{[r](e){let t=document.getElementById(n);return R.post(e,new FormData(t),{headers:{Accept:`application/json`}}).then(e=>{let{Result:n,SuccessfullySent:r}=e.data;this.Message.Result=n,this.Message.SuccessfullySent=r,r&&(t.style.display=`none`,i&&Ha.getOrCreateInstance(document.getElementById(i)).hide()),Qk(n,``,r?`info`:`danger`)}).catch(e=>$(e,o.warning))},getCaptcha(){a?.tabTarget&&bA(a.tabTarget)}}});if(window[e]=QO(e,s),i&&a?.popupTarget){let e=document.getElementById(i);e&&(e.addEventListener(`shown.bs.modal`,()=>{e.querySelector(`.captcha-box`)||bA(a.popupTarget)}),e.addEventListener(`hide.bs.modal`,()=>{e.querySelector(`.captcha-box`)&&bA(a.restoreTarget)}))}});var xA=new Map,SA=!1;function CA(e,t){e.classList.remove(t?`is-invalid`:`is-valid`),e.classList.add(t?`is-valid`:`is-invalid`)}function wA(e,t){if(!t.value)return;let n=document.getElementById(e.elements.stateSelectId);return R.get(e.routes.getStates,{params:{countryId:t.value,addSelectStateItem:`true`}}).then(t=>{let r=window[e.name];r.AvailableStates=t.data,r.StateProvinceId=t.data[0].id,n&&CA(n,!1)}).catch(t=>$(t,e.res.warning))}function TA(e){let t=document.getElementById(e.checkboxId),n=document.querySelector(e.targetSelector);t&&n&&(n.style.display=t.checked?`block`:`none`)}function EA(){SA||(SA=!0,document.addEventListener(`change`,e=>{let t=e.target.id;if(t)for(let n of xA.values()){if(t===n.elements.countrySelectId)return void wA(n,e.target);if(t===n.elements.stateSelectId)return void CA(e.target,e.target.selectedIndex>0);if(n.newsletterToggle&&t===n.newsletterToggle.checkboxId){TA(n.newsletterToggle);return}}}))}Z(`countryStateForm`,e=>{window[e.name]=QO(e.name,hk({data:()=>({...e.data})})),xA.set(e.name,e),EA(),ik(t=>t.$nextTick(()=>{document.querySelectorAll(`[data-checked='true']`).forEach(e=>{e.checked=!0}),e.newsletterToggle&&TA(e.newsletterToggle)}))}),Z(`globals`,({name:e,data:t})=>{window[e]=t}),Z(`merchandiseReturn`,({pickupDate:e,showPickupDate:t,itemIds:n})=>{let r={};n.forEach(e=>{r[`quantity_`+e]=`0`}),window.merchandisereturns=QO(`merchandisereturns`,hk({data:()=>({...t?{PickupDate:e??``}:{},newAddress:!1,...r,checkboxes:[]}),created(){this.isNewAddress()},methods:{allowed(e,t){let n=window.vm.$refs[t];n&&(e.checked?n.removeAttribute(`disabled`):n.setAttribute(`disabled`,`disabled`))},isNewAddress(){let e=document.getElementById(`pickup-address-select`);this.newAddress=!e||e.value===``}}}))}),Z(`outOfStockSubscription`,({productId:e,routes:t,elements:n,res:r})=>{ik(i=>i.$nextTick(()=>{let i=document.getElementById(n.buttonId);i&&(R.get(t.buttonText).then(e=>{i.value=e.data,i.style.display=`inline-block`}).catch(e=>$(e,r.warning)),i.addEventListener(`click`,()=>{let a=document.getElementById(`product-details-form-`+e);R.post(t.popup,a?new FormData(a):null).then(e=>{document.getElementById(n.contentId).innerHTML=e.data.resource,i.value=e.data.buttontext,Ha.getOrCreateInstance(document.getElementById(n.modalId)).show()}).catch(e=>$(e,r.warning))}))}))}),Z(`reviews`,e=>{let{name:t,modalId:n,methods:r,addReviewKey:i,overviewKey:a,overviewVm:o,helpfulness:s,res:c}=e,l=hk({data:()=>({Model:e.model,rating:null,captcha:null}),methods:{[r.setHelpfulness](e,t,n,r,i){return R.post(e,null,{params:{[s.reviewIdParam]:n,[s.ownerIdParam]:r,washelpful:t}}).then(e=>{document.getElementById(`helpfulness-vote-yes-`+n).innerHTML=e.data.TotalYes,document.getElementById(`helpfulness-vote-no-`+n).innerHTML=e.data.TotalNo,Qk(e.data.Result,i,`info`)}).catch(e=>$(e,c.warning))},[r.addReview](){let e=document.getElementById(n),t=e?.closest(`.tab-pane`);if(t&&!t.classList.contains(`active`)){let e=document.querySelector(`[data-bs-target="#${t.id}"], #${t.id}-tab`);e&&Ks.getOrCreateInstance(e).show()}Ha.getOrCreateInstance(e).show()},modalReviewShown(){let e=document.getElementById(n),t=this.Model[i];t.DisplayCaptcha&&!e.querySelector(`.captcha-box`)&&!t.SuccessfullyAdded&&document.getElementById(`captcha-popup`).prepend(document.getElementById(`captcha-box`)),this.Model={...this.Model,ReviewTitle:``,ReviewText:``}},modalReviewClose(){let e=document.getElementById(n);this.Model[i].DisplayCaptcha&&e.querySelector(`.captcha-box`)&&document.getElementById(`captcha-container`).prepend(document.getElementById(`captcha-box`))},[r.submitReview](){let e=document.getElementById(`addReviewForm`),t=e.getAttribute(`data-title`);return R.post(e.getAttribute(`action`),new FormData(e)).then(e=>{this.Model=e.data;let r=e.data[i].SuccessfullyAdded;r&&Ha.getOrCreateInstance(document.getElementById(n)).hide(),Qk(e.data[i].Result,t,r?`info`:`danger`);let s=e.data[a];s&&window[o]&&(window[o].Model=s)}).catch(e=>$(e,c.warning))}}});window[t]=QO(t,l),document.addEventListener(`shown.bs.modal`,e=>{e.target.id===n&&l.modalReviewShown()}),document.addEventListener(`hide.bs.modal`,e=>{e.target.id===n&&l.modalReviewClose()})}),Z(`scrollPagination`,()=>{let e=hk({data:()=>({stop:!1}),methods:{next(){let e=window.catalog,t=e.Model.PagingFilteringContext.PageNumber,n=e.Count?.p_numbers;if(n&&n.length&&!(n[n.length-1]<=t)){if(typeof AjaxFilter>`u`){let n=new URL(window.location.href);n.searchParams.set(`pagenumber`,t+1),e.loadProducts(n.href)}else document.getElementById(`PageNumber`).value=t,AjaxFilter.setFilter(`pagenumber`)}},pageScroll(){let e=document.getElementById(`catalog-products`);if(!e)return;let t=e.scrollHeight*.3,{PageNumber:n,TotalPages:r}=window.catalog.Model.PagingFilteringContext;window.scrollY>=t&&ne.pageScroll())});var DA=600;function OA(e,t){return e.filter(e=>e.SearchType===t)}Z(`searchBox`,({lang:e,routes:t,res:n})=>{let r=null;window.searchbox=QO(`searchbox`,hk({data:()=>({recording:!1,recognition:null,searchitems:null,focus:!1,text:``,lang:e,placeholder:n.placeholder,micMessage:n.micHold,searchcategories:null,searchbrands:null,searchblog:null,searchproducts:null,allowed:!0}),created(){this.allowCheck()},methods:{formSubmit(){tk().$refs.searchForm.submit()},startRecording(){this.recognition.start()},stopRecording(){this.recognition.stop()},allowCheck(){$k(e=>{this.allowed=e})},autocompleteVue(){let e=tk().$refs.searchBoxInput,n=e.getAttribute(`minlength`);clearTimeout(r),!(e.value.length{let e=document.getElementById(`SearchCategoryId`);R.get(t.autocomplete,{params:{term:this.text,categoryId:e?e.value:``}}).then(e=>{let t=e.data;t&&(this.searchitems=t,this.searchcategories=OA(t,`Category`),this.searchbrands=OA(t,`Brand`),this.searchblog=OA(t,`Blog`),this.searchproducts=OA(t,`Product`))}).catch(e=>console.error(`[grand] autocomplete failed`,e))},DA))},handleSpeech(e,t,r){let i=e.results[e.resultIndex][0].transcript,a=i.toLowerCase().trim();a===n.voiceStop?this.stopRecording():!r.value&&this.recording?this.text=i:a===n.voiceGo?(this.stopRecording(),t.submit()):a===n.voiceReset?this.text=``:this.recording&&(this.text=i)}},watch:{text(){this.autocompleteVue(),this.text===``&&(this.searchitems=null)},recording(){let e=window.SpeechRecognition||window.webkitSpeechRecognition;if(!e){this.allowed=!1;return}if(!this.recording){this.stopRecording(),this.placeholder=n.placeholder,this.micMessage=n.micHold;return}let t=tk().$refs;this.recognition=new e,this.recognition.continuous=!0,this.recognition.lang=this.lang,t.searchBoxInput.focus(),this.startRecording(),this.placeholder=n.micPlaceholder,this.micMessage=n.micPlaceholder,this.recognition.addEventListener(`result`,e=>this.handleSpeech(e,t.searchForm,t.searchBoxInput))}}}))}),Z(`shoppingCart`,({model:e,routes:t,res:n})=>{let r=e=>Qk(e,n.warning);window.vmorder=QO(`vmorder`,hk({data:()=>({cart:e,totals:null,checkoutAsGuest:!1,MinOrderTotalWarning:null,TermsOfServiceOnOrderConfirmPage:null,ConfirmWarnings:null,terms:!1,acceptTerms:!1}),created(){this.updateTotals(),document.body.classList.add(`cart-view`)},watch:{terms(){this.terms===!0&&(this.acceptTerms=!1)}},methods:{updateCart(){return R.get(t.summary).then(e=>{this.cart=e.data}).catch(e=>$(e,n.warning))},updateTotals(){return R.get(t.total).then(e=>{this.totals=e.data}).catch(e=>$(e,n.warning))},termsCheck(e){this.cart.MinOrderSubtotalWarning??(this.terms?(this.checkout(e),this.acceptTerms=!1):this.acceptTerms=!0)},checkout(e){if(e){window.location=t.checkout;return}if(!this.cart.ShowCheckoutAsGuestButton&&this.cart.IsGuest){window.location=t.loginReturningToCart;return}let n=document.getElementById(`shopping-cart-form`);n.setAttribute(`action`,t.startCheckout),n.submit()},ApplyGiftVoucher(e){return R.post(e,Zk({giftvouchercouponcode:document.getElementById(`giftvouchercouponcode`).value})).then(e=>(this.cart=e.data.model,this.updateTotals())).catch(e=>$(e,n.warning))},removeGiftVoucher(e){return R.get(e).then(e=>(this.cart=e.data.model,this.updateTotals())).catch(e=>$(e,n.warning))},getModel(){return R.get(t.shoppingCart,{params:{timestamp:new Date().getTime()}}).then(e=>(this.cart=e.data,window.vm.flycartindicator=this.cart.Items.reduce((e,t)=>e+t.Quantity,0),this.updateTotals())).catch(e=>$(e,n.warning))},updateQuantity(e,i){return R.post(t.updateQuantity,Zk({shoppingcartId:i,ShoppingCartType:1,quantity:document.getElementById(e+i).value})).then(e=>(e.data.success?(this.cart=e.data.model,window.vm.flycartindicator=e.data.totalproducts):r(e.data.warnings),this.updateTotals())).catch(e=>$(e,n.warning))},updateCartType(e,r){return R.post(t.changeTypeCartItem,Zk({id:e,status:r})).then(e=>(this.cart=e.data.model,window.vm.updateSidebarShoppingCart(t.sidebarShoppingCart),this.updateTotals())).catch(e=>$(e,n.warning))},updateOnCart(e){return R.get(`${t.getItemCart}/${e}`).then(t=>{t.data.success?(window.vm.PopupQuickViewVueModal=t.data.model,window.vm.UpdatedShoppingCartItemId=e,Ha.getOrCreateInstance(document.getElementById(`ModalQuickView`)).show()):r(t.data.message)}).catch(e=>$(e,n.warning))},cartClick(e){let t=e.target.closest(`.deleteshoppingcartitem`);t&&t.dataset.deleteUrl&&this.deleteitem(t.dataset.deleteUrl)},deleteitem(e){return R.post(e).then(e=>{let t=window.vm;if(t.flycartindicator=e.data.totalproducts,document.querySelector(`.sidebar-cart`)){let n=e.data.sidebarshoppingcartmodel;t.flycart=n,t.flycartitems=n.Items,t.flycartindicator=n.TotalProducts}return this.cart=e.data.model,this.updateTotals()}).catch(e=>$(e,n.warning))},ApplyDiscountCode(e){return R.post(e,Zk({discountcouponcode:document.getElementById(`discountcouponcode`).value})).then(e=>(this.cart=e.data.model,this.updateTotals())).catch(e=>$(e,n.warning))},RemoveDiscountId(e){return R.get(e).then(e=>(this.cart=e.data.model,this.updateTotals())).catch(e=>$(e,n.warning))},displayWarning(e){r(e)}}}))}),Z(`vendorReviewOverview`,({model:e})=>{window.vendorreviewsoverview=QO(`vendorreviewsoverview`,hk({data:()=>({Model:e}),computed:{rating(){return this.Model&&this.Model.TotalReviews?this.Model.RatingSum/this.Model.TotalReviews:0}}}))});var kA=/[`~!#$%^&*()_|+\-=?;:'",.<>{}[\]\\/]/gi;function AA(e){let t=new SpeechSynthesisUtterance;t.text=e,window.speechSynthesis.speak(t)}Z(`voiceNavigation`,({lang:e,commands:t,res:n})=>{window.voicenavigator=QO(`voicenavigator`,hk({data:()=>({recording:!1,recognition:null,lang:e,commands:t,transcript:``,variant:`info`,micMessage:n.micHold,voiceMessage:n.tryAgain,allowed:!0}),created(){this.allowCheck()},methods:{startRecording(){this.recognition.start()},stopRecording(){this.recognition.stop()},allowCheck(){$k(e=>{this.allowed=e})},handleSpeech(e){if(!this.recording||!e.results[0].isFinal)return;let t=e.results[e.resultIndex][0].transcript;this.transcript=t;let n=t.replace(kA,``).toLowerCase(),r=this.commands.find(e=>e.triggers.some(e=>n.includes(e)));r?(this.stopRecording(),this.variant=`success`,setTimeout(()=>{window.location.href=r.url},300)):setTimeout(()=>AA(this.voiceMessage),300)}},watch:{recording(){let e=window.SpeechRecognition||window.webkitSpeechRecognition;e?this.recording?(this.recognition=new e,this.recognition.continuous=!0,this.recognition.lang=this.lang,this.recognition.interimResults=!1,this.recognition.maxAlternatives=1,this.startRecording(),this.micMessage=n.micPlaceholder,this.recognition.addEventListener(`result`,e=>this.handleSpeech(e))):(this.stopRecording(),this.transcript=``,this.variant=`info`,this.micMessage=n.micHold):this.allowed=!1}}}))}),Z(`wishlist`,({model:e,routes:t,res:n})=>{window.vmwishlist=QO(`vmwishlist`,hk({data:()=>({Model:e,PopupUpdateVueModal:null}),methods:{deleteFromWishlist(e){return R.get(`${t.deleteItem}/${e}`).then(e=>{if(e.data.success)return this.getModel();this.displayWarning(e.data.message,`danger`)}).catch(e=>$(e,n.warning))},addToCartFromWishlist(e){return R.post(t.addToCart+e).then(e=>{if(!e.data.success)this.displayWarning(e.data.message,`danger`);else return window.vm.updateSidebarShoppingCart(t.sidebarShoppingCart),this.displayWarning(n.addedToCart,`info`,n.success),this.getModel()}).catch(e=>$(e,n.warning))},getItemCart(e){return R.get(`${t.getItemCart}/${e}`).then(t=>{t.data.success?(window.vm.PopupQuickViewVueModal=t.data.model,window.vm.UpdatedShoppingCartItemId=e,Ha.getOrCreateInstance(document.getElementById(`ModalQuickView`)).show()):this.displayWarning(t.data.message,`danger`)}).catch(e=>$(e,n.warning))},updateQuantity(e,r){return R.post(t.updateQuantity,Zk({shoppingcartId:r,ShoppingCartType:2,quantity:document.getElementById(e+r).value})).then(e=>{if(e.data.success)return this.getModel();this.displayWarning(e.data.warnings,`danger`)}).catch(e=>$(e,n.warning))},getModel(){return R.get(t.wishlist,{params:{timestamp:new Date().getTime()},headers:{Accept:`application/json`,"X-Response-View":`Json`}}).then(e=>{this.Model=e.data;let t=document.querySelector(`.wishlist-qty`);t&&(t.innerHTML=this.Model.Items.reduce((e,t)=>e+t.Quantity,0))}).catch(e=>$(e,n.warning))},displayWarning(e,t,r){Qk(e,r??n.warning,t)}}}))});function jA(e){Object.defineProperty(e.config.globalProperties,"vm",{get:()=>tk(),configurable:!0}),Object.assign(e.config.globalProperties,{window,document,location,localStorage,bootstrap:It,hideTooltip:NA})}$O(e=>{xk(e),Ak(e),jA(e),e.config.globalProperties.$bvToast=Nk,e.config.errorHandler=(e,t,n)=>console.error(e,n)});function MA(){document.body.__tooltipDelegate||(document.body.__tooltipDelegate=new qo(document.body,{selector:`[data-bs-toggle="tooltip"]`}))}document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,MA):MA();function NA(e){e&&qo.getInstance(e)?.hide()}window.hideTooltip=NA,rk(()=>Rk()),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>Rk()):Rk(),window.grandInitViews=Rk,window.grandMountIslands=dk,window.bootstrap=It,window.Vue=_k,window.axios=R,window.Pikaday=af.default,window.VueGallerySlideshow=bk,window.$bvToast=Nk,window.vee_getMessage=Sk})(); \ No newline at end of file + `};Z(`productGallery`,({componentName:e,variant:t,mainImageId:n,images:r})=>{ek(e,{template:aA[t](n),data:()=>({images:r,index:null}),components:{VueGallerySlideshow:bk}})}),Z(`galleryThumbnails`,({mainImageId:e})=>{Xk(`click`,`[data-gallery-thumb]`,t=>{let n=document.getElementById(e);n&&n.setAttribute(`src`,t.getAttribute(`data-src`))})}),Z(`imageZoom`,()=>{Xk(`click`,`[data-zoom-image]`,e=>{let t=e.parentElement?.querySelector(`img`);t&&window.mediumZoom&&window.mediumZoom(t,{background:`#232323`,margin:30}).open()})}),Z(`pushNotifications`,({firebase:e,route:t})=>{Yk(()=>{window.PushNotifications&&(window.PushNotifications.init(e.publicApiKey,e.senderId,e.projectId,e.authDomain,e.storageBucket,e.databaseUrl,t,e.appId),window.PushNotifications.process())})});function oA(e,t){let n=parseInt(e.value,10);e.value=Math.max(1,(isNaN(n)?0:n)+t)}Z(`quantityStepper`,()=>{Xk(`click`,`[data-quantity-step]`,e=>{let t=document.getElementById(e.dataset.quantityTarget);t&&oA(t,Number(e.dataset.quantityStep))})}),Z(`quickViewModal`,({elements:e})=>{Xk(`shown.bs.modal`,`#`+e.modalId,()=>{let e=window.vm;e&&(e.initReservationQV?.(),e.QuickViewShown?.())})}),Z(`reservationInfo`,({productId:e,start:t,routes:n,res:r})=>{Yk(()=>{window.Reservation?.init(t.date,t.year,t.month,r.noAvailableReservations,n.datesForMonth,e,n.attributeChange)})}),Z(`searchModal`,({elements:e})=>{Xk(`shown.bs.modal`,`#`+e.modalId,()=>{let e=tk()?.$refs?.searchBoxInput;e&&e.focus()})});var sA=991;Z(`twoColumnsSidebar`,({elements:e})=>{Yk(()=>{let t=document.querySelector(e.sidebarSelector),n=document.getElementById(e.desktopHostId);if(!t||!n)return;let r=document.getElementById(e.toggleId),i=Array.prototype.some.call(t.children,t=>t.id!==e.categoryNavigationId);r&&!i&&(r.classList.remove(`d-block`),r.classList.add(`d-none`));let a=()=>{let r=(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)<=sA?document.querySelector(e.mobileHostSelector):n;r&&t.parentElement!==r&&r.appendChild(t)};a(),window.addEventListener(`resize`,a)})}),Z(`enableWhenChecked`,({elements:e})=>{Xk(`change`,`#`+e.checkboxId,t=>{let n=document.getElementById(e.targetId);n&&n.classList.toggle(`disabled`,!t.checked)})}),Z(`checkAll`,({elements:e})=>{Xk(`click`,`#`+e.checkboxId,t=>{document.querySelectorAll(e.rowSelector).forEach(e=>{e.checked=t.checked})})});var cA=`username-available-status alert alert-success d-flex justify-content-center mt-3 mb-0`,lA=`username-not-available-status alert alert-danger d-flex justify-content-center mt-3 mb-0`,uA=`username-not-available-status text-danger d-flex order-2 mt-3`;Z(`usernameAvailability`,({route:e,elements:t,res:n})=>{Xk(`click`,`#`+t.buttonId,r=>{let i=document.getElementById(t.usernameId),a=document.getElementById(t.resultId),o=document.getElementById(t.progressId);if(!i||!a)return;if(a.innerText=``,!i.value){a.setAttribute(`class`,uA),a.innerText=n.enterUsername;return}o&&(o.style.display=`block`),r.setAttribute(`disabled`,`disabled`);let s=()=>{r.removeAttribute(`disabled`),o&&(o.style.display=`none`)};R.post(e,Qk({Username:i.value})).then(e=>{s(),a.setAttribute(`class`,e.data.Available?cA:lA),a.innerText=e.data.Text}).catch(e=>{s(),$(e,n.warning)})})});function dA(e,t){if(!t)return;let n=document.querySelector(e);n&&(n.innerText=t)}function fA(e,t){e&&e.forEach(e=>{let n=document.querySelector(`#product_attribute_label_`+e),r=document.querySelector(`#product_attribute_input_`+e);n&&(n.style.display=t),r&&(r.style.display=t)})}function pA(e,t){let n=document.getElementById(`stock-availability-schema-`+e);if(!t)n?.remove();else if(n)n.setAttribute(`href`,t);else{let n=document.querySelector(`[data-offer-for="${e}"]`);if(n){let r=document.createElement(`link`);r.id=`stock-availability-schema-`+e,r.setAttribute(`itemprop`,`availability`),r.setAttribute(`href`,t),n.prepend(r)}}let r=document.getElementById(`product-structured-data-`+e);if(r)try{let e=JSON.parse(r.textContent);if(!e.offers)return;t?e.offers.availability=t:delete e.offers.availability,r.textContent=JSON.stringify(e)}catch(e){console.error(`[grand] product structured data could not be updated`,e)}}function mA(e,t,n){if(!t)return;let r=()=>{let n=document.getElementById(`main-product-img-`+e);n&&n.setAttribute(`src`,t)};if(n!==`swiper`||document.querySelector(`.product-grouped`)){r();return}let i=window.vm?.$refs?.swiperTop?.$swiper;i?i.slides[i.activeIndex]?.querySelector(`img`)?.dataset.srcs!==t&&i.slides.forEach((e,n)=>{e.querySelector(`img`)?.dataset.srcs===t&&i.slideTo(n,1e3,!1)}):r()}Z(`productAttributes`,({productId:e,attributes:t,route:n,picture:r,res:i})=>{let a=hk({data:()=>({ProductAttributes:t}),methods:{attrchange(){let t=document.getElementById(`product-details-form-`+e);if(t)return R.post(n,new FormData(t),{params:{product:e}}).then(({data:t})=>{let n=document.querySelector(`.price-value-${e} .actual-price`)||document.querySelector(`.price-value-${e}`);t.price&&n&&(n.innerText=t.price),dA(`#sku-`+e,t.sku),dA(`#mpn-`+e,t.mpn),dA(`#gtin-`+e,t.gtin),dA(`#stock-availability-value-`+e,t.stockAvailability),`availability`in t&&pA(e,t.availability);let i=document.querySelector(`#out-of-stock-subscribe-`+e);i&&typeof t.outOfStockSubscription==`boolean`&&(i.style.display=t.outOfStockSubscription?`block`:`none`),i&&t.buttonTextOutOfStockSubscription&&(i.value=t.buttonTextOutOfStockSubscription),fA(t.enabledattributemappingids,`table-cell`),fA(t.disabledattributemappingids,`none`),setTimeout(()=>mA(e,t.pictureDefaultSizeUrl,r),100)}).catch(e=>$(e,i.warning))}}}),o=`standardProductAttributes_`+e;window[o]=QO(o,a),ik(e=>e.$nextTick(()=>a.attrchange()))}),Z(`warehouseSelector`,({productId:e,route:t,elements:n})=>{Xk(`change`,`#`+n.selectId,()=>{let r=document.getElementById(n.selectId);if(!r)return;let i=new FormData;i.append(`warehouseId`,r.value),i.append(`productId`,e),R.post(t,i).then(t=>{let n=document.getElementById(`stock-availability-value-`+e);n&&t.data.stockAvailability&&(n.innerText=t.data.stockAvailability),`availability`in t.data&&pA(e,t.data.availability),window[`standardProductAttributes_`+e]?.attrchange()}).catch(e=>console.error(`[grand] warehouse change failed`,e))})}),Z(`state`,({name:e,data:t})=>{window[e]=QO(e,hk({data:()=>({...t})}))}),Z(`applyVendor`,({model:e,res:t})=>{window.applyvendor=QO(`applyvendor`,hk({data:()=>({Name:e.Name,Email:e.Email,AcceptPrivacyPolicy:!1})}));let n=document.getElementById(`apply-vendor`),r=document.getElementById(`accept-terms-of-service`);n&&r&&n.addEventListener(`click`,e=>{r.checked||(e.preventDefault(),$k(t.acceptTermsRequired,t.warning))})});var hA=[`AskQuestionEmail`,`AskQuestionFullName`,`AskQuestionPhone`,`AskQuestionMessage`];Z(`askQuestion`,({data:e,captchaTarget:t,res:n})=>{window.askquestion=QO(`askquestion`,hk({data:()=>({...e}),methods:{async sendContactUsForm(e){let t=tk().$refs;if(!await t.contact.validate())return;let r={};hA.forEach(e=>{r[e]=t[e].value}),r.Id=t.AskQuestionProductId.value;let i=document.querySelector(`[id^='g-recaptcha-response']`);return i&&(r[`g-recaptcha-response-value`]=i.value),R.post(e,Qk(r)).then(e=>{let t=document.querySelector(`.product-standard`);if(!t)return;let n=t.querySelector(`.product-contact-error`),r=t.querySelector(`.product-contact-send`);e.data.success?(t.querySelector(`#contact-us-product`).style.display=`none`,n.style.display=`none`,r.innerHTML=e.data.message,r.style.display=`block`):(n.innerHTML=e.data.message,n.style.display=`block`)}).catch(e=>$(e,n.warning))},getCaptcha(){if(!t)return;let e=document.getElementById(`captcha-box`),n=document.getElementById(t);e&&n&&n.prepend(e)}}}))});var gA=4;function _A(e){let t=Math.max(1,e.PageNumber-gA),n=e.PageNumber+gA((e[n[t]]=e[n[t]]||[]).push(n),e),{}):e||{}}function bA(e,t){if(!t)return;let n=t.PagingFilteringContext;if(!n)return;e.pager=_A(n);let r=n.SpecificationFilter;r&&(r.NotFilteredItems=yA(r.NotFilteredItems,vA),r.AlreadyFilteredItems=yA(r.AlreadyFilteredItems,vA))}Z(`catalog`,({model:e,res:t})=>{window.catalog=QO(`catalog`,hk({data:()=>({Model:[],pager:[],clientRendered:!1}),created(){this.Model=e,bA(this,e)},watch:{Model(e){bA(this,e)}},methods:{loadProducts(e){this.getResponse(e);let t=new URL(e,window.location.origin);t.searchParams.delete(`timestamp`),window.history.replaceState({path:t.href},``,t.href)},setRating(e){let t=new URL(window.location.href);e===``?t.searchParams.delete(`rating`):t.searchParams.set(`rating`,e),t.searchParams.delete(`pagenumber`),this.loadProducts(t.href)},getResponse(e){let n=new URL(e,window.location.origin);return n.searchParams.set(`timestamp`,new Date().getTime()),R.get(n.href,{headers:{Accept:`application/json`}}).then(e=>{this.Model=e.data,bA(this,e.data),this.clientRendered=!0}).catch(e=>{console.error(`[grand] catalog request failed`,e),$k(t.loadFailed,t.warning)})},goToPage(e){let t=new URL(window.location.href);t.searchParams.set(`pagenumber`,e),this.loadProducts(t.href),this.scrollToSection()},scrollToSection(){let e=document.getElementById(`catalog-products`);e&&window.scrollTo({top:e.offsetTop,left:0,behavior:`smooth`})}}}))}),Z(`catalogModern`,({model:e,res:t})=>{window.catalog=QO(`catalog`,hk({data:()=>({Model:[],pager:[],Count:[]}),created(){this.Model=e,this.refresh(e)},watch:{Model(e){this.refresh(e)}},methods:{refresh(e){e&&(bA(this,e),this.setCount(0))},loadProducts(e){let t=new URL(e,window.location.origin);t.searchParams.delete(`timestamp`),t.searchParams.delete(`pagenumber`),typeof AjaxFilter<`u`?t.searchParams.forEach((e,t)=>{let n=document.querySelector(`#ajaxfilter-form .`+t);n&&(n.value=e),AjaxFilter.setFilter(t)}):this.getResponse(e),window.history.replaceState({path:t.href},``,t.href)},getResponse(e){let n=new URL(e,window.location.origin);return n.searchParams.set(`timestamp`,new Date().getTime()),R.get(n.href,{headers:{Accept:`application/json`,"X-Response-View":`Json`}}).then(e=>{let t=e.data,n=t.PagingFilteringContext.PageNumber;if(n>1&&n!==this.Model.PagingFilteringContext.PageNumber){let e=this.Model.Products;this.Model=t,this.Model.Products=e.concat(t.Products)}else this.Model=t;this.refresh(this.Model),window.scrollpagination&&(window.scrollpagination.stop=!1)}).catch(e=>{console.error(`[grand] catalog request failed`,e),$k(t.loadFailed,t.warning)})},goToPage(e){let t=new URL(window.location.href);t.searchParams.set(`pagenumber`,e),this.loadProducts(t.href)},setCount(e){let t=e>0?e:1,n=Math.ceil(this.Model.Products.length/t);this.Count={...this.Count,p_numbers:Array.from(Array(n).keys())}}}}))}),Z(`comments`,({name:e,formId:t,comments:n,fields:r})=>{let i={};r.forEach(e=>{i[e]=``}),window[e]=QO(e,hk({data:()=>({Model:n,...i}),methods:{submitComment(){let e=document.getElementById(t);return R.post(e.getAttribute(`action`),new FormData(e),{headers:{Accept:`application/json`}}).then(e=>{let{success:t,message:n,model:i}=e.data;window.vm.displayBarNotification(n,``,t?`success`:`error`,t?3e3:3500),t&&(r.forEach(e=>{this[e]=``}),i&&this.addComment(i))}).catch(e=>console.error(`[grand] comment submit failed`,e))},addComment(e){this.Model.push(e)}}}))}),Z(`compareProducts`,({model:e,specificationAttributes:t,loadRoute:n})=>{window.specificationAttributes=QO(`specificationAttributes`,hk({data:()=>({Model:t})})),ik(t=>{n?t.getCompareList(n):t.compareproducts=e})});function xA(e){let t=document.getElementById(`captcha-box`),n=document.getElementById(e);t&&n&&n.prepend(t)}Z(`contactForm`,({name:e,data:t,formId:n,submitMethod:r,modalId:i,captcha:a,res:o})=>{let s=hk({data:()=>({...t,Message:{Result:null,SuccessfullySent:!1}}),methods:{[r](e){let t=document.getElementById(n);return R.post(e,new FormData(t),{headers:{Accept:`application/json`}}).then(e=>{let{Result:n,SuccessfullySent:r}=e.data;this.Message.Result=n,this.Message.SuccessfullySent=r,r&&(t.style.display=`none`,i&&Ha.getOrCreateInstance(document.getElementById(i)).hide()),$k(n,``,r?`info`:`danger`)}).catch(e=>$(e,o.warning))},getCaptcha(){a?.tabTarget&&xA(a.tabTarget)}}});if(window[e]=QO(e,s),i&&a?.popupTarget){let e=document.getElementById(i);e&&(e.addEventListener(`shown.bs.modal`,()=>{e.querySelector(`.captcha-box`)||xA(a.popupTarget)}),e.addEventListener(`hide.bs.modal`,()=>{e.querySelector(`.captcha-box`)&&xA(a.restoreTarget)}))}});var SA=new Map,CA=!1;function wA(e,t){e.classList.remove(t?`is-invalid`:`is-valid`),e.classList.add(t?`is-valid`:`is-invalid`)}function TA(e,t){if(!t.value)return;let n=document.getElementById(e.elements.stateSelectId);return R.get(e.routes.getStates,{params:{countryId:t.value,addSelectStateItem:`true`}}).then(t=>{let r=window[e.name];r.AvailableStates=t.data,r.StateProvinceId=t.data[0].id,n&&wA(n,!1)}).catch(t=>$(t,e.res.warning))}function EA(e){let t=document.getElementById(e.checkboxId),n=document.querySelector(e.targetSelector);t&&n&&(n.style.display=t.checked?`block`:`none`)}function DA(){CA||(CA=!0,document.addEventListener(`change`,e=>{let t=e.target.id;if(t)for(let n of SA.values()){if(t===n.elements.countrySelectId)return void TA(n,e.target);if(t===n.elements.stateSelectId)return void wA(e.target,e.target.selectedIndex>0);if(n.newsletterToggle&&t===n.newsletterToggle.checkboxId){EA(n.newsletterToggle);return}}}))}Z(`countryStateForm`,e=>{window[e.name]=QO(e.name,hk({data:()=>({...e.data})})),SA.set(e.name,e),DA(),ik(t=>t.$nextTick(()=>{document.querySelectorAll(`[data-checked='true']`).forEach(e=>{e.checked=!0}),e.newsletterToggle&&EA(e.newsletterToggle)}))}),Z(`globals`,({name:e,data:t})=>{window[e]=t}),Z(`merchandiseReturn`,({pickupDate:e,showPickupDate:t,itemIds:n})=>{let r={};n.forEach(e=>{r[`quantity_`+e]=`0`}),window.merchandisereturns=QO(`merchandisereturns`,hk({data:()=>({...t?{PickupDate:e??``}:{},newAddress:!1,...r,checkboxes:[]}),created(){this.isNewAddress()},methods:{allowed(e,t){let n=window.vm.$refs[t];n&&(e.checked?n.removeAttribute(`disabled`):n.setAttribute(`disabled`,`disabled`))},isNewAddress(){let e=document.getElementById(`pickup-address-select`);this.newAddress=!e||e.value===``}}}))}),Z(`outOfStockSubscription`,({productId:e,routes:t,elements:n,res:r})=>{ik(i=>i.$nextTick(()=>{let i=document.getElementById(n.buttonId);i&&(R.get(t.buttonText).then(e=>{i.value=e.data,i.style.display=`inline-block`}).catch(e=>$(e,r.warning)),i.addEventListener(`click`,()=>{let a=document.getElementById(`product-details-form-`+e);R.post(t.popup,a?new FormData(a):null).then(e=>{document.getElementById(n.contentId).innerHTML=e.data.resource,i.value=e.data.buttontext,Ha.getOrCreateInstance(document.getElementById(n.modalId)).show()}).catch(e=>$(e,r.warning))}))}))}),Z(`reviews`,e=>{let{name:t,modalId:n,methods:r,addReviewKey:i,overviewKey:a,overviewVm:o,helpfulness:s,res:c}=e,l=hk({data:()=>({Model:e.model,rating:null,captcha:null}),methods:{[r.setHelpfulness](e,t,n,r,i){return R.post(e,null,{params:{[s.reviewIdParam]:n,[s.ownerIdParam]:r,washelpful:t}}).then(e=>{document.getElementById(`helpfulness-vote-yes-`+n).innerHTML=e.data.TotalYes,document.getElementById(`helpfulness-vote-no-`+n).innerHTML=e.data.TotalNo,$k(e.data.Result,i,`info`)}).catch(e=>$(e,c.warning))},[r.addReview](){let e=document.getElementById(n),t=e?.closest(`.tab-pane`);if(t&&!t.classList.contains(`active`)){let e=document.querySelector(`[data-bs-target="#${t.id}"], #${t.id}-tab`);e&&Ks.getOrCreateInstance(e).show()}Ha.getOrCreateInstance(e).show()},modalReviewShown(){let e=document.getElementById(n),t=this.Model[i];t.DisplayCaptcha&&!e.querySelector(`.captcha-box`)&&!t.SuccessfullyAdded&&document.getElementById(`captcha-popup`).prepend(document.getElementById(`captcha-box`)),this.Model={...this.Model,ReviewTitle:``,ReviewText:``}},modalReviewClose(){let e=document.getElementById(n);this.Model[i].DisplayCaptcha&&e.querySelector(`.captcha-box`)&&document.getElementById(`captcha-container`).prepend(document.getElementById(`captcha-box`))},[r.submitReview](){let e=document.getElementById(`addReviewForm`),t=e.getAttribute(`data-title`);return R.post(e.getAttribute(`action`),new FormData(e)).then(e=>{this.Model=e.data;let r=e.data[i].SuccessfullyAdded;r&&Ha.getOrCreateInstance(document.getElementById(n)).hide(),$k(e.data[i].Result,t,r?`info`:`danger`);let s=e.data[a];s&&window[o]&&(window[o].Model=s)}).catch(e=>$(e,c.warning))}}});window[t]=QO(t,l),document.addEventListener(`shown.bs.modal`,e=>{e.target.id===n&&l.modalReviewShown()}),document.addEventListener(`hide.bs.modal`,e=>{e.target.id===n&&l.modalReviewClose()})}),Z(`scrollPagination`,()=>{let e=hk({data:()=>({stop:!1}),methods:{next(){let e=window.catalog,t=e.Model.PagingFilteringContext.PageNumber,n=e.Count?.p_numbers;if(n&&n.length&&!(n[n.length-1]<=t)){if(typeof AjaxFilter>`u`){let n=new URL(window.location.href);n.searchParams.set(`pagenumber`,t+1),e.loadProducts(n.href)}else document.getElementById(`PageNumber`).value=t,AjaxFilter.setFilter(`pagenumber`)}},pageScroll(){let e=document.getElementById(`catalog-products`);if(!e)return;let t=e.scrollHeight*.3,{PageNumber:n,TotalPages:r}=window.catalog.Model.PagingFilteringContext;window.scrollY>=t&&ne.pageScroll())});var OA=600;function kA(e,t){return e.filter(e=>e.SearchType===t)}Z(`searchBox`,({lang:e,routes:t,res:n})=>{let r=null;window.searchbox=QO(`searchbox`,hk({data:()=>({recording:!1,recognition:null,searchitems:null,focus:!1,text:``,lang:e,placeholder:n.placeholder,micMessage:n.micHold,searchcategories:null,searchbrands:null,searchblog:null,searchproducts:null,allowed:!0}),created(){this.allowCheck()},methods:{formSubmit(){tk().$refs.searchForm.submit()},startRecording(){this.recognition.start()},stopRecording(){this.recognition.stop()},allowCheck(){eA(e=>{this.allowed=e})},autocompleteVue(){let e=tk().$refs.searchBoxInput,n=e.getAttribute(`minlength`);clearTimeout(r),!(e.value.length{let e=document.getElementById(`SearchCategoryId`);R.get(t.autocomplete,{params:{term:this.text,categoryId:e?e.value:``}}).then(e=>{let t=e.data;t&&(this.searchitems=t,this.searchcategories=kA(t,`Category`),this.searchbrands=kA(t,`Brand`),this.searchblog=kA(t,`Blog`),this.searchproducts=kA(t,`Product`))}).catch(e=>console.error(`[grand] autocomplete failed`,e))},OA))},handleSpeech(e,t,r){let i=e.results[e.resultIndex][0].transcript,a=i.toLowerCase().trim();a===n.voiceStop?this.stopRecording():!r.value&&this.recording?this.text=i:a===n.voiceGo?(this.stopRecording(),t.submit()):a===n.voiceReset?this.text=``:this.recording&&(this.text=i)}},watch:{text(){this.autocompleteVue(),this.text===``&&(this.searchitems=null)},recording(){let e=window.SpeechRecognition||window.webkitSpeechRecognition;if(!e){this.allowed=!1;return}if(!this.recording){this.stopRecording(),this.placeholder=n.placeholder,this.micMessage=n.micHold;return}let t=tk().$refs;this.recognition=new e,this.recognition.continuous=!0,this.recognition.lang=this.lang,t.searchBoxInput.focus(),this.startRecording(),this.placeholder=n.micPlaceholder,this.micMessage=n.micPlaceholder,this.recognition.addEventListener(`result`,e=>this.handleSpeech(e,t.searchForm,t.searchBoxInput))}}}))}),Z(`shoppingCart`,({model:e,routes:t,res:n})=>{let r=e=>$k(e,n.warning);window.vmorder=QO(`vmorder`,hk({data:()=>({cart:e,totals:null,checkoutAsGuest:!1,MinOrderTotalWarning:null,TermsOfServiceOnOrderConfirmPage:null,ConfirmWarnings:null,terms:!1,acceptTerms:!1}),created(){this.updateTotals(),document.body.classList.add(`cart-view`)},watch:{terms(){this.terms===!0&&(this.acceptTerms=!1)}},methods:{updateCart(){return R.get(t.summary).then(e=>{this.cart=e.data}).catch(e=>$(e,n.warning))},updateTotals(){return R.get(t.total).then(e=>{this.totals=e.data}).catch(e=>$(e,n.warning))},termsCheck(e){this.cart.MinOrderSubtotalWarning??(this.terms?(this.checkout(e),this.acceptTerms=!1):this.acceptTerms=!0)},checkout(e){if(e){window.location=t.checkout;return}if(!this.cart.ShowCheckoutAsGuestButton&&this.cart.IsGuest){window.location=t.loginReturningToCart;return}let n=document.getElementById(`shopping-cart-form`);n.setAttribute(`action`,t.startCheckout),n.submit()},ApplyGiftVoucher(e){return R.post(e,Qk({giftvouchercouponcode:document.getElementById(`giftvouchercouponcode`).value})).then(e=>(this.cart=e.data.model,this.updateTotals())).catch(e=>$(e,n.warning))},removeGiftVoucher(e){return R.get(e).then(e=>(this.cart=e.data.model,this.updateTotals())).catch(e=>$(e,n.warning))},getModel(){return R.get(t.shoppingCart,{params:{timestamp:new Date().getTime()}}).then(e=>(this.cart=e.data,window.vm.flycartindicator=this.cart.Items.reduce((e,t)=>e+t.Quantity,0),this.updateTotals())).catch(e=>$(e,n.warning))},updateQuantity(e,i){return R.post(t.updateQuantity,Qk({shoppingcartId:i,ShoppingCartType:1,quantity:document.getElementById(e+i).value})).then(e=>(e.data.success?(this.cart=e.data.model,window.vm.flycartindicator=e.data.totalproducts):r(e.data.warnings),this.updateTotals())).catch(e=>$(e,n.warning))},updateCartType(e,r){return R.post(t.changeTypeCartItem,Qk({id:e,status:r})).then(e=>(this.cart=e.data.model,window.vm.updateSidebarShoppingCart(t.sidebarShoppingCart),this.updateTotals())).catch(e=>$(e,n.warning))},updateOnCart(e){return R.get(`${t.getItemCart}/${e}`).then(t=>{t.data.success?(window.vm.PopupQuickViewVueModal=t.data.model,window.vm.UpdatedShoppingCartItemId=e,Ha.getOrCreateInstance(document.getElementById(`ModalQuickView`)).show()):r(t.data.message)}).catch(e=>$(e,n.warning))},cartClick(e){let t=e.target.closest(`.deleteshoppingcartitem`);t&&t.dataset.deleteUrl&&this.deleteitem(t.dataset.deleteUrl)},deleteitem(e){return R.post(e).then(e=>{let t=window.vm;if(t.flycartindicator=e.data.totalproducts,document.querySelector(`.sidebar-cart`)){let n=e.data.sidebarshoppingcartmodel;t.flycart=n,t.flycartitems=n.Items,t.flycartindicator=n.TotalProducts}return this.cart=e.data.model,this.updateTotals()}).catch(e=>$(e,n.warning))},ApplyDiscountCode(e){return R.post(e,Qk({discountcouponcode:document.getElementById(`discountcouponcode`).value})).then(e=>(this.cart=e.data.model,this.updateTotals())).catch(e=>$(e,n.warning))},RemoveDiscountId(e){return R.get(e).then(e=>(this.cart=e.data.model,this.updateTotals())).catch(e=>$(e,n.warning))},displayWarning(e){r(e)}}}))}),Z(`vendorReviewOverview`,({model:e})=>{window.vendorreviewsoverview=QO(`vendorreviewsoverview`,hk({data:()=>({Model:e}),computed:{rating(){return this.Model&&this.Model.TotalReviews?this.Model.RatingSum/this.Model.TotalReviews:0}}}))});var AA=/[`~!#$%^&*()_|+\-=?;:'",.<>{}[\]\\/]/gi;function jA(e){let t=new SpeechSynthesisUtterance;t.text=e,window.speechSynthesis.speak(t)}Z(`voiceNavigation`,({lang:e,commands:t,res:n})=>{window.voicenavigator=QO(`voicenavigator`,hk({data:()=>({recording:!1,recognition:null,lang:e,commands:t,transcript:``,variant:`info`,micMessage:n.micHold,voiceMessage:n.tryAgain,allowed:!0}),created(){this.allowCheck()},methods:{startRecording(){this.recognition.start()},stopRecording(){this.recognition.stop()},allowCheck(){eA(e=>{this.allowed=e})},handleSpeech(e){if(!this.recording||!e.results[0].isFinal)return;let t=e.results[e.resultIndex][0].transcript;this.transcript=t;let n=t.replace(AA,``).toLowerCase(),r=this.commands.find(e=>e.triggers.some(e=>n.includes(e)));r?(this.stopRecording(),this.variant=`success`,setTimeout(()=>{window.location.href=r.url},300)):setTimeout(()=>jA(this.voiceMessage),300)}},watch:{recording(){let e=window.SpeechRecognition||window.webkitSpeechRecognition;e?this.recording?(this.recognition=new e,this.recognition.continuous=!0,this.recognition.lang=this.lang,this.recognition.interimResults=!1,this.recognition.maxAlternatives=1,this.startRecording(),this.micMessage=n.micPlaceholder,this.recognition.addEventListener(`result`,e=>this.handleSpeech(e))):(this.stopRecording(),this.transcript=``,this.variant=`info`,this.micMessage=n.micHold):this.allowed=!1}}}))}),Z(`wishlist`,({model:e,routes:t,res:n})=>{window.vmwishlist=QO(`vmwishlist`,hk({data:()=>({Model:e,PopupUpdateVueModal:null}),methods:{deleteFromWishlist(e){return R.get(`${t.deleteItem}/${e}`).then(e=>{if(e.data.success)return this.getModel();this.displayWarning(e.data.message,`danger`)}).catch(e=>$(e,n.warning))},addToCartFromWishlist(e){return R.post(t.addToCart+e).then(e=>{if(!e.data.success)this.displayWarning(e.data.message,`danger`);else return window.vm.updateSidebarShoppingCart(t.sidebarShoppingCart),this.displayWarning(n.addedToCart,`info`,n.success),this.getModel()}).catch(e=>$(e,n.warning))},getItemCart(e){return R.get(`${t.getItemCart}/${e}`).then(t=>{t.data.success?(window.vm.PopupQuickViewVueModal=t.data.model,window.vm.UpdatedShoppingCartItemId=e,Ha.getOrCreateInstance(document.getElementById(`ModalQuickView`)).show()):this.displayWarning(t.data.message,`danger`)}).catch(e=>$(e,n.warning))},updateQuantity(e,r){return R.post(t.updateQuantity,Qk({shoppingcartId:r,ShoppingCartType:2,quantity:document.getElementById(e+r).value})).then(e=>{if(e.data.success)return this.getModel();this.displayWarning(e.data.warnings,`danger`)}).catch(e=>$(e,n.warning))},getModel(){return R.get(t.wishlist,{params:{timestamp:new Date().getTime()},headers:{Accept:`application/json`,"X-Response-View":`Json`}}).then(e=>{this.Model=e.data;let t=document.querySelector(`.wishlist-qty`);t&&(t.innerHTML=this.Model.Items.reduce((e,t)=>e+t.Quantity,0))}).catch(e=>$(e,n.warning))},displayWarning(e,t,r){$k(e,r??n.warning,t)}}}))});function MA(e){Object.defineProperty(e.config.globalProperties,"vm",{get:()=>tk(),configurable:!0}),Object.assign(e.config.globalProperties,{window,document,location,localStorage,bootstrap:It,hideTooltip:PA})}$O(e=>{xk(e),jk(e),MA(e),e.config.globalProperties.$bvToast=Pk,e.config.errorHandler=(e,t,n)=>console.error(e,n)});function NA(){document.body.__tooltipDelegate||(document.body.__tooltipDelegate=new qo(document.body,{selector:`[data-bs-toggle="tooltip"]`}))}document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,NA):NA();function PA(e){e&&qo.getInstance(e)?.hide()}window.hideTooltip=PA,rk(()=>zk()),document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>zk()):zk(),window.grandInitViews=zk,window.grandMountIslands=dk,window.bootstrap=It,window.Vue=_k,window.axios=R,window.Pikaday=af.default,window.VueGallerySlideshow=bk,window.$bvToast=Pk,window.vee_getMessage=Sk})(); \ No newline at end of file