From ee2a3fcdee9c0a8a11cacbf94be55ebd6676b593 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 11:00:04 +0100 Subject: [PATCH 01/11] Remove the LiteDB Singleton option With Singleton false every request opened its own LiteDatabase on the same file. LiteDB locks the file, so every request failed with an IOException and the application could not serve a single page. There is no working setup behind the option, so it goes. A value left in an existing configuration is ignored. --- .../Grand.Infrastructure/Configuration/DatabaseConfig.cs | 5 ----- .../Grand.Infrastructure/Startup/StartupApplication.cs | 9 +++------ src/Web/Grand.Web.Admin/App_Data/appsettings.json | 2 -- src/Web/Grand.Web.Store/App_Data/appsettings.json | 2 -- src/Web/Grand.Web.Vendor/App_Data/appsettings.json | 2 -- src/Web/Grand.Web/App_Data/appsettings.json | 2 -- 6 files changed, 3 insertions(+), 19 deletions(-) diff --git a/src/Core/Grand.Infrastructure/Configuration/DatabaseConfig.cs b/src/Core/Grand.Infrastructure/Configuration/DatabaseConfig.cs index 7338e85e2..273ab7a4a 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 6a070a392..f03178c69 100644 --- a/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs +++ b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs @@ -63,12 +63,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 }); } } diff --git a/src/Web/Grand.Web.Admin/App_Data/appsettings.json b/src/Web/Grand.Web.Admin/App_Data/appsettings.json index 1d4ce0a7e..52adfd707 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.Store/App_Data/appsettings.json b/src/Web/Grand.Web.Store/App_Data/appsettings.json index 0bcd3f9b7..d859ae916 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.Vendor/App_Data/appsettings.json b/src/Web/Grand.Web.Vendor/App_Data/appsettings.json index 1d4ce0a7e..52adfd707 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/appsettings.json b/src/Web/Grand.Web/App_Data/appsettings.json index 8cc0c015e..39578c2ac 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, From c71774ca8ccd0fae9ea31e9fa642492750b10a7c Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 11:00:05 +0100 Subject: [PATCH 02/11] Build LiteDB entity mappers before the application starts BsonMapper publishes a mapper before filling its members and reads published mappers without a lock. Threads meeting a type for the first time together either threw "Collection was modified" or "Member Id not found", or silently read an object with fields missing. Requests right after every start got a 500, and so did the scheduled tasks and the admin dashboard. The mappers are now built during the single-threaded part of startup, for every entity with its nested types, base types and interfaces. Base types matter because LINQ resolves x.Id on the type declaring it, ParentEntity. --- .../Grand.Data/LiteDb/LiteDBMapperWarmup.cs | 55 +++++ .../Startup/StartupApplication.cs | 4 + .../LiteDb/LiteDBMapperWarmupTests.cs | 199 ++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs create mode 100644 src/Tests/Grand.Data.Tests/LiteDb/LiteDBMapperWarmupTests.cs diff --git a/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs b/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs new file mode 100644 index 000000000..d4c1bf47d --- /dev/null +++ b/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs @@ -0,0 +1,55 @@ +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(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/Startup/StartupApplication.cs b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs index f03178c69..b4af7545f 100644 --- a/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs +++ b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs @@ -2,8 +2,10 @@ 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.TypeSearch; using LiteDB; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -84,6 +86,8 @@ private void RegisterDataLayer(IServiceCollection serviceCollection, IConfigurat } else { + //the mappers are not safe to build concurrently - startup is still single-threaded here + LiteDBMapperWarmup.Warm(BsonMapper.Global, new TypeSearcher().ClassesOfType()); //database context serviceCollection.AddScoped(); //store files context - grid fs 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 000000000..e0fb5b3d8 --- /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; +} From 0e8882033c454669091f13f7aa374261fc6289e0 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 11:00:05 +0100 Subject: [PATCH 03/11] Preselect the time zone dates are really shown in The installer leaves the store time zone empty, which means the server's. The settings form has no empty option, so its select showed the first zone in the list, and saving the page for any other reason stored UTC-12 and moved every date in the shop by hours. The form now preselects the configured zone, or the server's when none is set or the machine does not know it. --- .../DateTimeServiceExtensionsTests.cs | 50 +++++++++++++++++++ .../Controllers/SettingController.cs | 6 ++- .../Extensions/DateTimeServiceExtensions.cs | 19 +++++++ .../Controllers/SettingController.cs | 6 ++- 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs create mode 100644 src/Web/Grand.Web.AdminShared/Extensions/DateTimeServiceExtensions.cs 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 000000000..8cd33d06c --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs @@ -0,0 +1,50 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Web.AdminShared.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System.Collections.ObjectModel; + +namespace Grand.Web.Admin.Tests.Extensions; + +[TestClass] +public class DateTimeServiceExtensionsTests +{ + 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 IDateTimeService _dateTimeService; + + [TestInitialize] + public void Init() + { + var service = new Mock(); + service.Setup(x => x.GetSystemTimeZones()) + .Returns(new ReadOnlyCollection([_dateline, _warsaw, TimeZoneInfo.Local])); + _dateTimeService = service.Object; + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + public void GetEffectiveTimeZoneId_NothingConfigured_ReturnsTheServerZone(string configured) + { + Assert.AreEqual(TimeZoneInfo.Local.Id, _dateTimeService.GetEffectiveTimeZoneId(configured)); + } + + [TestMethod] + public void GetEffectiveTimeZoneId_ConfiguredZoneExists_ReturnsIt() + { + Assert.AreEqual("Warsaw", _dateTimeService.GetEffectiveTimeZoneId("Warsaw")); + } + + [TestMethod] + public void GetEffectiveTimeZoneId_ConfiguredZoneDiffersInCase_ReturnsTheListedId() + { + Assert.AreEqual("Warsaw", _dateTimeService.GetEffectiveTimeZoneId("warsaw")); + } + + [TestMethod] + public void GetEffectiveTimeZoneId_ConfiguredZoneUnknownOnThisMachine_ReturnsTheServerZone() + { + Assert.AreEqual(TimeZoneInfo.Local.Id, _dateTimeService.GetEffectiveTimeZoneId("Gone/Zone")); + } +} diff --git a/src/Web/Grand.Web.Admin/Controllers/SettingController.cs b/src/Web/Grand.Web.Admin/Controllers/SettingController.cs index 010a36479..482129484 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,7 +604,8 @@ public async Task GeneralCommon([FromServices] IEnumerable(storeScope); - model.DateTimeSettings.DefaultStoreTimeZoneId = dateTimeSettings.DefaultStoreTimeZoneId; + model.DateTimeSettings.DefaultStoreTimeZoneId = + dateTimeService.GetEffectiveTimeZoneId(dateTimeSettings.DefaultStoreTimeZoneId); var iswindows = OperatingSystem.IsWindows(); foreach (var timeZone in dateTimeService.GetSystemTimeZones()) { @@ -611,7 +613,7 @@ public async Task GeneralCommon([FromServices] IEnumerable + /// The time zone dates are really shown in: the configured one, or the server's when none is set or the + /// machine does not know it (see DateTimeService.CurrentTimeZone). A form has to preselect this one - + /// a select with nothing matching shows its first option, and saving would then store that zone. + /// + public static string GetEffectiveTimeZoneId(this IDateTimeService dateTimeService, string configuredTimeZoneId) + { + var configured = dateTimeService.GetSystemTimeZones() + .FirstOrDefault(x => x.Id.Equals(configuredTimeZoneId, StringComparison.OrdinalIgnoreCase)); + + return configured?.Id ?? TimeZoneInfo.Local.Id; + } +} diff --git a/src/Web/Grand.Web.Store/Controllers/SettingController.cs b/src/Web/Grand.Web.Store/Controllers/SettingController.cs index d042d9409..ced683a14 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,7 +79,8 @@ public async Task GeneralCommon([FromServices] IEnumerable(storeScope); - model.DateTimeSettings.DefaultStoreTimeZoneId = dateTimeSettings.DefaultStoreTimeZoneId; + model.DateTimeSettings.DefaultStoreTimeZoneId = + dateTimeService.GetEffectiveTimeZoneId(dateTimeSettings.DefaultStoreTimeZoneId); var isWindows = Grand.Infrastructure.OperatingSystem.IsWindows(); foreach (var timeZone in dateTimeService.GetSystemTimeZones()) { @@ -86,7 +88,7 @@ public async Task GeneralCommon([FromServices] IEnumerable Date: Sat, 31 Oct 2026 11:00:05 +0100 Subject: [PATCH 04/11] Stop reporting untouched empty fields when they lose focus On the login page the email field has focus from the start. Pressing the mouse on Checkout as Guest blurred it, the required message appeared and moved the layout, and the button slid from under the pointer before mouseup, so the first click did nothing. Register and Forgot password were hit the same way. Leaving a field nobody has touched is no attempt to fill it in, so it now reports nothing. Submitting and typing still validate. --- src/Web/Grand.Web/vueapp/src/compat/validate.js | 16 ++++++++++++++-- .../wwwroot/bundles/app.runtime.bundle.js | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Web/Grand.Web/vueapp/src/compat/validate.js b/src/Web/Grand.Web/vueapp/src/compat/validate.js index 810fffbb3..313bc3251 100644 --- a/src/Web/Grand.Web/vueapp/src/compat/validate.js +++ b/src/Web/Grand.Web/vueapp/src/compat/validate.js @@ -168,6 +168,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 && isEmpty(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 +200,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 1d5802241..f11b31903 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={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(),this._onFocusOut=()=>{this.state===null&&Tk(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=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=>`
From 2359aa6269ed43adf22c95014d5d57d8e6dfd04a Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 11:00:06 +0100 Subject: [PATCH 05/11] Skip gift vouchers without an order item when filtering by one A voucher created by hand has no PurchasedWithOrderItem. LiteDB filters in memory, so one such voucher made the query throw and deleting any order returned a 500. --- .../Orders/GetGiftVoucherQueryHandler.cs | 3 +- .../Orders/GetGiftVoucherQueryHandlerTests.cs | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetGiftVoucherQueryHandlerTests.cs 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 68da368e9..1842e318e 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/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 000000000..ce1978056 --- /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); + } +} From 02faa7e98047c02211ffc94cc6d1bd357499dcfd Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 11:00:06 +0100 Subject: [PATCH 06/11] Report a missing customer email instead of failing with a 500 The edit validator lowercased the posted email without checking it, so saving a guest, who has none, or clearing a registered customer's email threw. The existing required rule now reports it, as it already does on create. --- .../Validators/CustomerValidatorTests.cs | 43 ++++++++++++++++++- .../Validators/Customers/CustomerValidator.cs | 3 +- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs b/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs index 818d1bdf0..2a767ee5d 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs @@ -22,6 +22,7 @@ public class CustomerValidatorTests private const string RequiredMessage = "Admin.Customers.Customers.Fields.Store.Required"; private const string MustBeCurrentStoreMessage = "Admin.Customers.Customers.Fields.Store.MustBeCurrentStore"; + private Mock _customerServiceMock; private Mock _groupServiceMock; private CustomerValidator _validator; @@ -31,6 +32,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 +52,7 @@ public void Setup() translationServiceMock.Object, new Mock().Object, contextAccessorMock.Object, - new Mock().Object, + _customerServiceMock.Object, _groupServiceMock.Object, new CustomerSettings(), new CustomerConfig(), @@ -60,6 +62,45 @@ 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_ReportsRequired(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.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.PropertyName == nameof(CustomerModel.Email))); + } + + [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.AdminShared/Validators/Customers/CustomerValidator.cs b/src/Web/Grand.Web.AdminShared/Validators/Customers/CustomerValidator.cs index 5a7ce8144..d9d801874 100644 --- a/src/Web/Grand.Web.AdminShared/Validators/Customers/CustomerValidator.cs +++ b/src/Web/Grand.Web.AdminShared/Validators/Customers/CustomerValidator.cs @@ -311,7 +311,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 reported by the required rule above: guests have none + if (customer != null && !string.IsNullOrWhiteSpace(x.Email) && customer.Email != x.Email.ToLower()) { if (!CommonHelper.IsValidEmail(x.Email)) context.AddFailure( From 8459bae4fde6f65a460e3abb017158c0c19a7cc1 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 18:13:36 +0100 Subject: [PATCH 07/11] Build LiteDB mappers for API models and uninstalled plugins too Starting from installed BaseEntity classes left two ways to build a mapper during live traffic, on the dictionary every other request reads without a lock. API queries read their models through TableCollection, and no entity leads to those models. Installing a plugin writes its entities on the running site, before the restart that would have warmed them. --- .../Grand.Data/LiteDb/LiteDBMapperWarmup.cs | 5 ++ .../Startup/StartupApplication.cs | 8 ++- .../Infrastructure/LiteDbMapperStartup.cs | 31 ++++++++++++ .../Startup/StartupApplicationTests.cs | 50 +++++++++++++++++++ .../Grand.Module.Api.Tests.csproj | 2 + .../LiteDbMapperStartupTests.cs | 28 +++++++++++ 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/Modules/Grand.Module.Api/Infrastructure/LiteDbMapperStartup.cs create mode 100644 src/Tests/Grand.Infrastructure.Tests/Startup/StartupApplicationTests.cs create mode 100644 src/Tests/Grand.Module.Api.Tests/Infrastructure/LiteDbMapperStartupTests.cs diff --git a/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs b/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs index d4c1bf47d..5ad147add 100644 --- a/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs +++ b/src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs @@ -16,6 +16,11 @@ public static class LiteDBMapperWarmup //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(); diff --git a/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs index b4af7545f..277f1e0cc 100644 --- a/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs +++ b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs @@ -5,6 +5,7 @@ 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; @@ -87,7 +88,12 @@ private void RegisterDataLayer(IServiceCollection serviceCollection, IConfigurat else { //the mappers are not safe to build concurrently - startup is still single-threaded here - LiteDBMapperWarmup.Warm(BsonMapper.Global, new TypeSearcher().ClassesOfType()); + 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 000000000..ad59a4053 --- /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.Infrastructure.Tests/Startup/StartupApplicationTests.cs b/src/Tests/Grand.Infrastructure.Tests/Startup/StartupApplicationTests.cs new file mode 100644 index 000000000..9cae2e28b --- /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 ea169c0d9..0cc055eb3 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 000000000..1a3a8ec2f --- /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)); + } +} From 1b3960cb4a3588a94ccef3f3b5e90b4b5c12a695 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 18:13:38 +0100 Subject: [PATCH 08/11] Offer the server time zone as a choice in store settings Preselecting the server's zone still turned "not configured" into a fixed zone on any save, so the store stopped following the host. It also depended on TimeZoneInfo.Local.Id being one of the listed options. An empty option keeps the value empty. A configured zone that resolves but is not listed, such as a Windows id on Linux, gets its own option so that saving does not change it. --- .../DateTimeServiceExtensionsTests.cs | 47 ++++++++++++---- .../Controllers/SettingController.cs | 16 ++---- .../Extensions/DateTimeServiceExtensions.cs | 51 +++++++++++++++--- .../Controllers/SettingController.cs | 16 ++---- .../App_Data/Resources/DefaultLanguage.xml | Bin 1641900 -> 1642178 bytes .../App_Data/Resources/Upgrade/en_250.xml | 3 ++ 6 files changed, 93 insertions(+), 40 deletions(-) diff --git a/src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs b/src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs index 8cd33d06c..473203cae 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Extensions/DateTimeServiceExtensionsTests.cs @@ -1,5 +1,6 @@ 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; @@ -9,8 +10,11 @@ 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] @@ -18,33 +22,58 @@ public void Init() { var service = new Mock(); service.Setup(x => x.GetSystemTimeZones()) - .Returns(new ReadOnlyCollection([_dateline, _warsaw, TimeZoneInfo.Local])); + .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 GetEffectiveTimeZoneId_NothingConfigured_ReturnsTheServerZone(string configured) + 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(TimeZoneInfo.Local.Id, _dateTimeService.GetEffectiveTimeZoneId(configured)); + Assert.AreEqual("Warsaw", Selected(Options("Warsaw"))); } [TestMethod] - public void GetEffectiveTimeZoneId_ConfiguredZoneExists_ReturnsIt() + public void GetStoreTimeZoneOptions_ConfiguredZoneDiffersInCase_SelectsTheListedId() { - Assert.AreEqual("Warsaw", _dateTimeService.GetEffectiveTimeZoneId("Warsaw")); + Assert.AreEqual("Warsaw", Selected(Options("warsaw"))); } [TestMethod] - public void GetEffectiveTimeZoneId_ConfiguredZoneDiffersInCase_ReturnsTheListedId() + public void GetStoreTimeZoneOptions_ConfiguredZoneResolvesButIsNotListed_KeepsIt() { - Assert.AreEqual("Warsaw", _dateTimeService.GetEffectiveTimeZoneId("warsaw")); + //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 GetEffectiveTimeZoneId_ConfiguredZoneUnknownOnThisMachine_ReturnsTheServerZone() + public void GetStoreTimeZoneOptions_ConfiguredZoneUnknownOnThisMachine_SelectsTheServerZone() { - Assert.AreEqual(TimeZoneInfo.Local.Id, _dateTimeService.GetEffectiveTimeZoneId("Gone/Zone")); + //DateTimeService.CurrentTimeZone falls back to the server here as well + Assert.AreEqual("", Selected(Options("Gone/Zone"))); } } diff --git a/src/Web/Grand.Web.Admin/Controllers/SettingController.cs b/src/Web/Grand.Web.Admin/Controllers/SettingController.cs index 482129484..ebaa89aa4 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SettingController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SettingController.cs @@ -604,19 +604,11 @@ public async Task GeneralCommon([FromServices] IEnumerable(storeScope); + model.DateTimeSettings.AvailableTimeZones = dateTimeService.GetStoreTimeZoneOptions( + dateTimeSettings.DefaultStoreTimeZoneId, + translationService.GetResource("Admin.Settings.GeneralCommon.DefaultStoreTimeZone.Server")); model.DateTimeSettings.DefaultStoreTimeZoneId = - dateTimeService.GetEffectiveTimeZoneId(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(model.DateTimeSettings.DefaultStoreTimeZoneId, - StringComparison.OrdinalIgnoreCase) - }); - } + 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 index 57a70f317..42cfb0dcb 100644 --- a/src/Web/Grand.Web.AdminShared/Extensions/DateTimeServiceExtensions.cs +++ b/src/Web/Grand.Web.AdminShared/Extensions/DateTimeServiceExtensions.cs @@ -1,19 +1,56 @@ using Grand.Business.Core.Interfaces.Common.Directory; +using Microsoft.AspNetCore.Mvc.Rendering; namespace Grand.Web.AdminShared.Extensions; public static class DateTimeServiceExtensions { /// - /// The time zone dates are really shown in: the configured one, or the server's when none is set or the - /// machine does not know it (see DateTimeService.CurrentTimeZone). A form has to preselect this one - - /// a select with nothing matching shows its first option, and saving would then store that zone. + /// 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 string GetEffectiveTimeZoneId(this IDateTimeService dateTimeService, string configuredTimeZoneId) + public static IList GetStoreTimeZoneOptions(this IDateTimeService dateTimeService, + string configuredTimeZoneId, string serverTimeZoneLabel) { - var configured = dateTimeService.GetSystemTimeZones() - .FirstOrDefault(x => x.Id.Equals(configuredTimeZoneId, StringComparison.OrdinalIgnoreCase)); + 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 })); - return configured?.Id ?? TimeZoneInfo.Local.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.Store/Controllers/SettingController.cs b/src/Web/Grand.Web.Store/Controllers/SettingController.cs index ced683a14..822c12fa4 100644 --- a/src/Web/Grand.Web.Store/Controllers/SettingController.cs +++ b/src/Web/Grand.Web.Store/Controllers/SettingController.cs @@ -79,19 +79,11 @@ public async Task GeneralCommon([FromServices] IEnumerable(storeScope); + model.DateTimeSettings.AvailableTimeZones = dateTimeService.GetStoreTimeZoneOptions( + dateTimeSettings.DefaultStoreTimeZoneId, + translationService.GetResource("Admin.Settings.GeneralCommon.DefaultStoreTimeZone.Server")); model.DateTimeSettings.DefaultStoreTimeZoneId = - dateTimeService.GetEffectiveTimeZoneId(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(model.DateTimeSettings.DefaultStoreTimeZoneId, - StringComparison.OrdinalIgnoreCase) - }); - } + 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/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml index 7dd15fd1230ba07a5ae2be0ef1d97da8ca54720e..6feffdd12886526eb7942fe231addf78556b18ca 100644 GIT binary patch delta 132 zcmZ28JLS;al!g|@7N!>F7M2#)7Pc1l7LFFq7OocV7M>Q~Eqroorr%n@$0MG}kjs$D zP{okXkO!po7>a>p5kuMZgL+)H)8pC&xTeQ+2oz19u$<4TU2Y8@5c31E01yiTu@Ddo V1F;AYivqD25Q}e@TO-kz2mmJ;E1CcR delta 83 zcmX>!H)YN2l!g|@7N!>F7M2#)7Pc1l7LFFq7OocV7M>Q~Eqroo+D+E-0Wm)i3jnbo Z5DNjZFc6CXu_zFW0kQaYleH3ki2&WT9r6GG 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 b549b75b9..1a1df48f7 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 + From 79e30b6894c4d73431aa4db378a49c4c5a6cdbf0 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 18:13:39 +0100 Subject: [PATCH 09/11] Let a guest without an email be saved in the admin Replacing the 500 with a required error still left a guest uneditable unless an address was invented for them. The email is now required unless the customer has none stored and is not being moved to the Registered group. The required message is also localized now: WithMessage covered only EmailAddress, so an empty field showed the FluentValidation default. --- .../Services/CustomerViewModelServiceTests.cs | 11 ++++++ .../Validators/CustomerValidatorTests.cs | 39 ++++++++++++++++++- .../Services/CustomerViewModelService.cs | 4 +- .../Validators/Customers/CustomerValidator.cs | 21 ++++++++-- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs index d3356f685..9bc019161 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 2a767ee5d..87efa0bbc 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Validators/CustomerValidatorTests.cs @@ -21,6 +21,7 @@ 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; @@ -65,13 +66,39 @@ private static CustomerModel BuildModel(string storeId) => [TestMethod] [DataRow(null)] [DataRow("")] - public async Task Email_Empty_GuestWithoutEmail_ReportsRequired(string email) + 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))); } @@ -85,7 +112,15 @@ public async Task Email_Cleared_RegisteredCustomer_ReportsRequired(string email) var result = await _validator.ValidateAsync(new CustomerModel { Id = "customer-1", Email = email, StoreId = CurrentStoreId }); - Assert.IsTrue(result.Errors.Any(e => e.PropertyName == nameof(CustomerModel.Email))); + 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] diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index b99a19c1e..6f8afaadc 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 d9d801874..aa82ff266 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,7 @@ void CustomerEditValidator() customer?.Id == contextAccessor.WorkContext.CurrentCustomer.Id) context.AddFailure("You can't edit own data from admin panel"); - //an empty email is reported by the required rule above: guests have none + //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)) From 4d510e3b46ecd2fb6f8253b2e5dd376d29a9ebc5 Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 18:13:40 +0100 Subject: [PATCH 10/11] Skip billing addresses without a last name when filtering orders Table on LiteDB is LINQ over objects, so one address with no last name made the admin order list return a 500 when filtered by it. --- .../Handlers/Orders/GetOrderQueryHandler.cs | 3 +- .../Orders/GetOrderQueryHandlerTests.cs | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) 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 960891442..be9fa67c0 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/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetOrderQueryHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Queries/Handlers/Orders/GetOrderQueryHandlerTests.cs index acbdf3a11..0a4e90756 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); + } } From de317bf6c0a1cd457c1c1624e589df5c8b8f9beb Mon Sep 17 00:00:00 2001 From: support Date: Sat, 31 Oct 2026 18:13:41 +0100 Subject: [PATCH 11/11] Stop reporting an unticked checkbox when it loses focus An unticked box reports false and a group reports an empty list, neither of which counted as empty. Their message could still appear on mousedown, move the layout and lose the click, as it did for text fields. Submitting still reports them. --- src/Web/Grand.Web/vueapp/src/compat/validate.js | 5 ++++- src/Web/Grand.Web/wwwroot/bundles/app.runtime.bundle.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Web/Grand.Web/vueapp/src/compat/validate.js b/src/Web/Grand.Web/vueapp/src/compat/validate.js index 313bc3251..7f13d3ce7 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 @@ -176,7 +179,7 @@ export const ValidationProvider = { * A field being typed in validates on `input`, so its layout is already settled. */ this._onFocusOut = () => { - if (this.state === null && isEmpty(this.getValue())) return + if (this.state === null && isUnanswered(this.getValue())) return this.validate() } 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 f11b31903..b472856af 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(),this._onFocusOut=()=>{this.state===null&&Tk(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=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