PageSourceSearch

https://www.alisoncraighome.com/js/pages/item/top-section.js

js alisoncraighome.com collected 2026-09-27 02:38:34 UTC 29,588 bytes, 682 lines download raw bytes

1/*===================================
2=     Item Page Top Section App      =
3===================================*/
4
5document.addEventListener('alpine:init', () => {
6    /* Item Page Top Section App */
7    Alpine.data('itemPageTopSection', () => ({
8        init() {
9            this.initialized = true;
10
11            // Find Pricing Description embeddable section and check if it has any content
12            const pricingDescParentDiv = document.getElementsByClassName("item-page-15-pricing-desc")[0];
13            if (pricingDescParentDiv) {
14                this.hasPricingDescription = pricingDescParentDiv.children.length > 0;
15            }
16
17            // Event listener to hide Learn More description when modal is hidden
18            $('#customAddonsModal').on('hidden.bs.modal', () => {
19                this.showLearnMoreDesc = false;
20                this.toggleMobileItemCard();
21            });
22
23            // Toggle visibility of slide-up image page card
24            $('#standardAddonsModal').on('hidden.bs.modal', () => {
25                this.toggleMobileItemCard();
26            });
27
28            // Retrieve add-on data from URL query string if available
29            const addonDataString = this.getQueryStringValue("addonData");
30
31            if (addonDataString) {
32                this.addonDataFromUrl = JSON.parse(decodeURIComponent(decodeURIComponent(addonDataString)));
33            }
34
35            if (window.topSectionModel.stripePublicKey) {
36                const stripe = Stripe(window.topSectionModel.stripePublicKey, window.topSectionModel.stripeConnectAccountId ? { stripeAccount: window.topSectionModel.stripeConnectAccountId } : null);
37                const elements = stripe.elements();
38                const options = {
39                    amount: window.topSectionModel.itemPriceCents,
40                    currency: 'USD',
41                    countryCode: 'US',
42                };
43                const PaymentMessageElement =
44                    elements.create('paymentMethodMessaging', options);
45                PaymentMessageElement.mount('#payment-method-messaging-element');
46            }
47        },
48
49        hasPricingDescription: false,
50        isSticky: window.topSectionModel.enableTopSectionStickiness && (window.addOnsModel?.addonGroups ?? []).length == 0,
51        gpid: window.topSectionModel.gpid,  // Base product Gpid
52        parentQuantity: (window.topSectionModel.cartQuantity > 0 && !window.topSectionModel.cartQuantityRequired) ? window.topSectionModel.cartQuantity : 1,  // Quantity of base product
53        itemIsInCart: window.topSectionModel.itemIsInCart,  // Flag to indicate whether base product is in cart
54        addonGroups: window.addOnsModel?.addonGroups ?? [],  // Add-on groups associated with base product
55        currentAddon: {},  // Stores current add-on
56        showLearnMoreDesc: false,  // Flag to toggle visibility of Learn More description
57        selectedAddonItems: [],  // Stores selected add-ons
58        initialized: false,  // Flag to trigger skeleton loaders
59        adding: false,  // Flag to indicate whether products are in process of being added to cart
60        addonDataFromUrl: "",  // Stores add-on data from URL query string
61        inCartAddonGpids: window.addOnsModel?.inCartAddonGpids ?? [],  // GPIDs of add-ons already in cart
62
63        // Computed property to check whether current add-on is selected
64        get currentAddonIsSelected() {
65            return this.selectedAddonItems.find((a) => a.Gpid == this.currentAddon.Gpid);
66        },
67
68        // Computed property to get total count of add-ons to add to cart
69        get addToCartItemCount() {
70            return this.selectedAddonItems.filter(addon => addon.Price > 0).length + 1;
71        },
72
73        // Computed property to calculate total price including base product price
74        get addToCartPrice() {
75            const baseItemPrice = window.addOnsModel?.parentItemPrice ?? 0;
76            let totalPrice = 0;
77
78            // Calculate total price of selected add-on items
79            this.selectedAddonItems.forEach(item => {
80                for (i = 0; i < item.Qty; i++) {
81                    var priceOfAddon = item.Price;
82                    totalPrice += priceOfAddon;
83                }
84            });
85
86            return this.formatPrice(parseFloat(totalPrice) + parseFloat(baseItemPrice));
87        },
88
89        // Toggles visibility of slide-up image page card by adding or removing d-none class
90        toggleMobileItemCard() {
91            if ($('.__item-page-card').hasClass('d-none')) {
92                $('.__item-page-card').removeClass('d-none');
93            } else {
94                $('.__item-page-card').addClass('d-none');
95            }
96        },
97
98        // Formats price as currency
99        formatPrice(price) {
100            const formatter = new Intl.NumberFormat('en-US', {
101                style: 'currency',
102                currency: 'USD',
103            });
104
105            return formatter.format(price);
106        },
107
108        // Handles selection of add-on for checkbox display
109        handleSelectOption(addon) {
110            // Check whether selectedAddons has another add-on from this group
111            let existingAddon = this.selectedAddonItems.find((selectedAddonItem) => selectedAddonItem.groupId == addon.groupId && selectedAddonItem.Gpid != addon.Gpid);
112
113            if (existingAddon) {
114                // Remove existing add-on
115                let existingAddonInput = document.getElementById('check' + existingAddon.Gpid);
116                existingAddonInput.click();
117            }
118
119            this.$nextTick(() => {
120                // Select new add-on
121                let selectedAddonInput = document.getElementById('check' + addon.Gpid);
122                selectedAddonInput.click();
123            });
124        },
125
126        // Handles selection of add-on for all displays except checkbox
127        handleSelectAddonStandardPopup(addon) {
128            if (this.currentAddonIsSelected) {
129                // Find first Remove button for existing add-on
130                const existingAddonButton = document.querySelector(`.remove-button[data-gpid="${addon.Gpid}"]`);
131
132                if (existingAddonButton) {
133                    existingAddonButton.click();  // Programmatically click first Remove button
134                }
135            }
136            else
137            {
138                const selectedAddonButton = document.querySelector(`.add-button[data-gpid="${addon.Gpid}"]`);
139
140                if (selectedAddonButton) {
141                    selectedAddonButton.click();  // Programmatically click first Add button
142                }
143            }
144        },
145
146        // Handles click event for adding base product and add-ons to cart
147        async addToCartClick() {
148            this.adding = true;
149
150            // Ensure quantity of base product is at least 1
151            if (this.parentQuantity <= 0) {
152                this.parentQuantity = 1;
153            }
154
155            if (!this.gpid)
156                return;
157
158            var addToCartItems = [];
159
160            // only pass priced addons to cart
161            this.selectedAddonItems.filter(addon => addon.Price > 0).forEach((sAddon) =>
162                addToCartItems.push({ addonGpid: sAddon.Gpid, qty: sAddon.Qty }));
163
164            try {
165                const payload = {
166                    parentGpid: this.gpid,
167                    addons: addToCartItems,
168                    quantity: this.parentQuantity
169                };
170
171                const response = await fetch(Utils.baseUrl + "shoppingcart/ShoppingCart/AddMultipleItemsToShoppingCart",
172                    {
173                        method: "POST",
174                        headers: {
175                            "Content-Type": "application/json; charset=utf-8"
176                        },
177                        body: JSON.stringify(payload)
178                    }
179                );
180
181                if (!response.ok) {
182                    throw new Error(`HTTP ${response.status}`);
183                }
184
185                // Hide spinner
186                this.adding = false;
187                this.itemIsInCart = true;
188
189                // Update cart product count
190                const addedCount = this.selectedAddonItems.length + 1;
191
192                const cartCountEl = document.querySelector('.js-cart-count');
193                if (cartCountEl) {
194                    const currentCount = parseInt(cartCountEl.textContent.replace(/\D/g, ""), 10) || 0;
195
196                    const cartItemCount = currentCount + addedCount;
197                    cartCountEl.textContent = cartItemCount;
198                }
199
200                // Dispatch cart update event
201                document.dispatchEvent(
202                    new CustomEvent('update-cart', {
203                        detail: { added: addedCount },
204                        bubbles: true
205                    })
206                );
207
208                var data = response.json();
209
210                // Track GA4
211                fdEvents.publish("add_to_cart", data.gaEvent);
212
213                // Track Facebook Pixel
214                window.fbPixel.done(() => {
215                    fbq(
216                        'track',
217                        'AddToCart',
218                        { content_type: 'product', content_ids: [this.gpid.toString()] },
219                        { eventID: data.eventId }
220                    );
221                });
222
223                // Track Klaviyo
224                await trackKlaviyoCartAdd(this.gpid );
225                for (const i of addToCartItems) {
226                    await trackKlaviyoCartAdd(i.Gpid);
227                }
228            } catch (error) {
229                this.adding = false;
230                this.itemIsInCart = false;
231
232                console.error(`Error adding items to cart: ${JSON.stringify(this.selectedAddonItems)}`, error);
233
234                const modalBody = document.getElementById('added-to-cart-modal-body');
235                if (modalBody)
236                    modalBody.innerHTML = "Looks like we encountered a problem, try re-adding your products to the cart.";
237
238                const modalEl = document.getElementById('added-to-cart-modal');
239                if (modalEl && window.bootstrap?.Modal) {
240                    const modal = new bootstrap.Modal(modalEl);
241                    modal.show();
242                }
243            }
244        },
245
246        // Calculates total price of add-on based on its quantity
247        priceBasedOnQty(selectedAddonOption) {
248            let totalPrice = 0;
249
250            // Iterates over quantity and sums up price
251            for (i = 0; i < selectedAddonOption.Qty; i++) {
252                var priceOfAddon = selectedAddonOption.Price;
253                totalPrice += priceOfAddon;
254            }
255
256            return totalPrice;
257        },
258        checkIfRemovedFromCart(gpid) {
259            if (this.gpid == gpid) { // this item was removed from the cart
260                this.itemIsInCart = false;
261            }
262
263            // Check if removed item was selected addon
264            var addonIndex = this.selectedAddonItems.findIndex(item => item.Gpid == gpid);
265            if (addonIndex !== -1) {
266                this.selectedAddonItems.splice(addonIndex, 1);
267
268                // Dispatch event so add-on group component can reset its selected option
269                window.dispatchEvent(new CustomEvent('addon-removed-from-cart', { detail: { gpid: gpid } }));
270
271                this.updateUrl();
272            }
273        },
274
275        // Updates URL with current add-on data
276        updateUrl() {
277            const url = new URL(window.location.href);
278
279            url.searchParams.set("addonData", encodeURIComponent(JSON.stringify(this.generateAddonData())));
280            window.history.replaceState({ path: url.href }, "", url.href);
281        },
282
283        // Generates data object representing selected add-ons
284        generateAddonData() {
285            var data = [];
286            this.selectedAddonItems.forEach((selectedAddonItem) =>
287                data.push({ addonGpid: selectedAddonItem.Gpid, groupId: selectedAddonItem.groupId }));
288
289            return data;
290        },
291
292        // Retrieves value of query string parameter by key
293        getQueryStringValue(key) {
294            const query = window.location.search.substring(1);
295            const vars = query.split("&");
296
297            for (let i = 0; i < vars.length; i++) {
298                const pair = vars[i].split("=");
299                if (pair[0] == key) {
300                    return pair[1];
301                }
302            }
303        }
304    }));
305
306    /* Add-On Group Component */
307    Alpine.data('addonGroupComponent', (_addonGroup) => ({
308        init() {
309            // Check whether there is add-on data from URL
310            if (this.addonDataFromUrl) {
311                // Find add-on data that matches current group ID
312                const addonData = this.addonDataFromUrl.find(group => group.groupId == this.groupId);
313
314                if (addonData) {
315                    // Find add-on within group that matches Gpid from the URL data
316                    const addonFromUrl = _addonGroup.AddonOptions.find(addonOption => addonOption.Gpid == addonData.addonGpid);
317                    addonFromUrl.groupId = this.groupId;
318
319                    this.handleSelectAddon(addonFromUrl, true);
320                }
321            }
322
323            // If no add-on was selected from URL but item is in cart, check if any add-on
324            // from group is already in cart and auto-select it
325            if (!this.addonDataFromUrl && this.itemIsInCart) {
326                const inCartAddon = _addonGroup.AddonOptions.find(addonOption =>
327                    this.inCartAddonGpids.includes(addonOption.Gpid));
328
329                if (inCartAddon) {
330                    inCartAddon.groupId = this.groupId;
331                    this.handleSelectAddon(inCartAddon, true);
332                }
333            }
334
335            // If selectedOption is not already set and group is of type PrimaryAddon,
336            // check whether primary add-on is preselected
337            if (this.presentationType == 2) {
338                this.primaryAddon = _addonGroup.AddonOptions.find(addonOption => addonOption.IsPrimaryAddon);
339                this.primaryAddon.groupId = this.groupId;
340
341                // Pre-select primary add-on
342                if (this.primaryAddon && this.primaryAddon.IsPreselected && this.selectedOption.Gpid == 0) {
343                    this.handleSelectAddon(this.primaryAddon, true);
344                }
345            } else {
346                var preselectedAddon = _addonGroup.AddonOptions.find(addonOption => addonOption.IsPreselected);
347                if (preselectedAddon && this.selectedOption.Gpid == 0) {
348                    preselectedAddon.groupId = this.groupId;
349                    this.handleSelectAddon(preselectedAddon, true);
350                }
351            }
352        },
353
354        groupId: _addonGroup.GroupId,
355        groupName: _addonGroup.GroupName,
356        groupNameLeadInText: _addonGroup.CustomLeadInText,  // Lead-in text for group name
357        presentationType: _addonGroup.PresentationType,  // Stores presentation type (int) of add-on group
358        primaryAddon: {},
359        showSelectModal: false,  // Flag to toggle visibility of add-on selection modal
360
361        // Object to store details of currently selected add-on option
362        selectedOption: {
363            Gpid: 0,  // ID of selected add-on option
364            SDesc: '',  // Short description of selected add-on option
365            LDesc: '',  // Long description of selected add-on option
366            LDescHtml: '',
367            ImageUrl: '',
368            Url: '',
369            WebpImageUrl: '',  // WebP image URL of selected add-on option
370            price: 0,
371            PriceBeforeDiscountOrSale: 0,
372            PricingHtml: '',  // HTML content for pricing information
373            Qty: 0,
374            IsStandardPopupEnabled: false,
375            Width: "",
376            Depth: "",
377            Height: ""
378        },
379
380        // Generates list of add-on options with unique IDs and group IDs
381        addonOptions() {
382            var result = [];
383
384            // Iterate over each add-on option and assign unique ID and group ID
385            _addonGroup.AddonOptions.forEach((option) => {
386                const randomId = crypto.randomUUID();  // Generate random unique ID
387                const withId = { ...option, id: randomId, groupId: _addonGroup.GroupId };
388
389                return result.push(withId);  // Add modified option to result list
390            });
391
392            if (this.presentationType == 2 || this.presentationType == 3) {
393                result = result.sort((a, b) => {
394                    if (a.Gpid == this.selectedOption.Gpid) { return -1; }
395                    if (b.Gpid == this.selectedOption.Gpid) { return 1; }
396
397                    return 0;
398                });
399            }
400
401            return result;
402        },
403
404        // Checks position of specific add-on group within filtered list of dropdown groups
405        checkDropDownIndex(last) {
406            // Filter addonGroups to include only those of Dropdown PresentationType
407            // and find index of group with current groupId in filtered list
408            const dropdownGroups = this.addonGroups.filter(addonGroup =>
408 addonGroup.PresentationType == 3);
409            const index = dropdownGroups.findIndex(dropdownGroup => dropdownGroup.GroupId == this.groupId);
410
411            // Check whether group is either first or last item in filtered list
412            if (last) {
413                return index == dropdownGroups.length - 1;
414            } else {
415                return index == 0;
416            }
417        },
418
419        // Opens custom add-ons modal
420        openCustomAddonsModal(addon) {
421            // Set current addon
422            this.currentAddon = addon;
423
424            // Toggle visibility of slide-up image page card
425            this.toggleMobileItemCard();
426            $('#customAddonsModal').modal('show');
427        },
428
429        // Opens standard add-ons modal
430        openStandardAddonsModal(addon) {
431            this.currentAddon = addon;
432            console.log(addon);
433
434            // Toggle visibility of slide-up image page card
435            this.toggleMobileItemCard();
436            $('#standardAddonsModal').modal('show');
437        },
438
439        // Determines whether given checkbox should be disabled based on its
440        // Gpid (only one option should be selectable)
441        isDisabled(gpid) {
442            if (this.selectedOption.Gpid > 0) {
443                return this.selectedOption.Gpid != gpid ? true : false;
444            } else {
445                return false;
446            }
447        },
448
449        // Calls function to select or deselect add-on option based on whether checkbox is checked
450        checkCheckboxValue(event, addon, addonGroup) {
451            // Call function for when the checkbox is checked
452            if (event.target.checked) {
453                this.handleSelectAddon(addon);
454            } else {
455                // Call function for when the checkbox is unchecked
456                this.handleNoneOptionSelected(addonGroup);
457            }
458        },
459
460        // Handles deselection of add-on option
461        handleNoneOptionSelected(addonGroup) {
462            this.showSelectModal = false;  // Hide selection modal
463            var index = this.selectedAddonItems.findIndex(item => item.groupId === addonGroup.GroupId);  // Find index of add-on in selected items list
464
465            // If add-on is currently selected, remove it from selected items
466            if (index !== -1) {
467                var removedAddon = this.selectedAddonItems[index];
468                this.selectedAddonItems.splice(index, 1);  // Remove add-on from selected items
469
470                // If item is in cart, remove add-on from cart too
471                if (this.itemIsInCart && removedAddon) {
472                    this.removeAddonFromCart(removedAddon.Gpid);
473                }
474
475                // Reset details of selectedOption to default values
476                this.selectedOption.Gpid = 0;
477                this.selectedOption.SDesc = '';
478                this.selectedOption.LDesc = '';
479                this.selectedOption.LDescHtml = '';
480                this.selectedOption.ImageUrl = '';
481                this.selectedOption.Url = '';
482                this.selectedOption.WebpImageUrl = '';
483                this.selectedOption.Price = 0;
484                this.selectedOption.PriceBeforeDiscountOrSale = 0;
485                this.selectedOption.PricingHtml = '';
486                this.selectedOption.Qty = 0;
487                this.selectedOption.IsStandardPopupEnabled = false;
488                this.selectedOption.Width = '';
489                this.selectedOption.Depth = '';
490                this.selectedOption.Height = '';
491
492                // Update URL parameters
493                this.$nextTick(() => {
494                    this.updateUrl();
495                });
496            }
497        },
498
499        // Handles selection of add-on option
500        handleSelectAddon(addon, skipUpdateUrl) {
501            // Reset quantity selector and update parent quantity
502            const value = (window.topSectionModel.cartQuantity > 0 && !window.topSectionModel.cartQuantityRequired) ?
503                window.topSectionModel.cartQuantity :
504                1
505
506            $("#quantitySelect").val(value);
507            this.parentQuantity = value;
508            this.showSelectModal = false;  // Hide selection modal
509
510            // Remove existing add-on from same group before adding new one
511            var removedAddon = null;
512            this.selectedAddonItems.forEach((currentAddon) => {
513                if (currentAddon.groupId === addon.groupId) {
514                    removedAddon = currentAddon;
515                    // Find and remove existing add-on from selected items
516                    this.selectedAddonItems
517                        .splice(this.selectedAddonItems
518                            .findIndex((selectedAddon) => selectedAddon.id === currentAddon.id), 1);
519                }
520            });
521
522            // Add selected add-on to list of selected add-on items and set as new primary add-on
523            this.selectedAddonItems.push(addon);
524            this.primaryAddon = addon;
525
526            // Update details of selectedOption with information of selected add-on
527            this.selectedOption.Gpid = addon.Gpid;
528            this.selectedOption.SDesc = addon.SDesc;
529            this.selectedOption.LDesc = addon.LDesc;
530            this.selectedOption.LDescHtml = addon.LDescHtml;
531            this.selectedOption.ImageUrl = addon.ImageUrl;
532            this.selectedOption.Url = addon.Url;
533            this.selectedOption.WebpImageUrl = addon.WebpImageUrl;
534            this.selectedOption.Price = this.priceBasedOnQty(addon);
535            this.selectedOption.PriceBeforeDiscountOrSale = addon.PriceBeforeDiscountOrSale;
536            this.selectedOption.PricingHtml = addon.PricingHtml;
537            this.selectedOption.Qty = addon.Qty;
538            this.selectedOption.IsStandardPopupEnabled = addon.IsStandardPopupEnabled;
539            this.selectedOption.Width = addon.Width;
540            this.selectedOption.Depth = addon.Depth;
541            this.selectedOption.Height = addon.Height
542
543            if (!skipUpdateUrl) {
544                // Update URL parameters
545                this.$nextTick(() => {
546                    this.updateUrl();
547                });
548
549                // If item is already in cart, auto-add add-on to cart
550                if (this.itemIsInCart) {
551                    // Remove previously selected add-on from same group if it was in cart
552                    if (removedAddon) {
553                        this.removeAddonFromCart(removedAddon.Gpid);
554                    }
555                    this.addAddonToCart(addon);
556                }
557            }
558        },
559
560        // Handles click event for dropdown option
561        handleDropdownClick() {
562            // If add-on is currently selected, call handleNoneOptionSelected to remove add-on
563            if (this.selectedOption.Gpid != 0) {
564                this.handleNoneOptionSelected(_addonGroup);
565            }
566        },
567
568        // Resets selectedOption when add-on is removed from cart
569        resetSelectedOption(gpid) {
570            if (this.selectedOption.Gpid == gpid) {
571                this.selectedOption.Gpid = 0;
572                this.selectedOption.SDesc = '';
573                this.selectedOption.LDesc = '';
574                this.selectedOption.LDescHtml = '';
575                this.selectedOption.ImageUrl = '';
576                this.selectedOption.Url = '';
577                this.selectedOption.WebpImageUrl = '';
578                this.selectedOption.Price = 0;
579                this.selectedOption.PriceBeforeDiscountOrSale = 0;
580                this.selectedOption.PricingHtml = '';
581                this.selectedOption.Qty = 0;
582                this.selectedOption.IsStandardPopupEnabled = false;
583                this.selectedOption.Width = '';
584                this.selectedOption.Depth = '';
585                this.selectedOption.Height = '';
586            }
587        },
588
589        // Adds single add-on to cart when parent item is already in cart
590        addAddonToCart(addon) {
591            var qty = addon.Qty * this.parentQuantity;
592
593            fetch(Utils.baseUrl + "shoppingcart/ShoppingCart/AddToShoppingCart", {
594                method: "POST",
595                headers: { "Content-Type": "application/json; charset=utf-8" },
596                body: JSON.stringify({ gpid: addon.Gpid, quantity: qty, parentGpid: this.gpid })
597            })
598            .then(response => {
599                if (!response.ok) throw new Error(`HTTP ${response.status}`);
600
601                // Update cart product count
602                const cartCountEl = document.querySelector('.js-cart-count');
603                if (cartCountEl) {
604                    const currentCount = parseInt(cartCountEl.textContent.replace(/\D/g, ""), 10) || 0;
605                    cartCountEl.textContent = currentCount + 1;
606                }
607
608                document.dispatchEvent(new CustomEvent('update-cart', { detail: { added: 1 }, bubbles: true }));
609            })
610            .catch(error => {
611                console.log(`Error adding addon GPID ${addon.Gpid} to cart: ${error}`);
612            });
613        },
614
615        // Removes single add-on from cart by GPID
616        removeAddonFromCart(gpid) {
617            fetch(Utils.baseUrl + "shoppingcart/ShoppingCart/RemoveCartItemByGpid", {
618                method: "POST",
619                headers: { "Content-Type": "application/json; charset=utf-8" },
620                body: JSON.stringify({ gpid: gpid })
621            })
622            .then(response => {
623                if (!response.ok) throw new Error(`HTTP ${response.status}`);
624
625                // Update cart product count
626                const cartCountEl = document.querySelector('.js-cart-count');
627                if (cartCountEl) {
628                    const currentCount = parseInt(cartCountEl.textContent.replace(/\D/g, ""), 10) || 0;
629                    cartCountEl.textContent = currentCount - 1;
630                }
631
632                document.dispatchEvent(new CustomEvent('update-cart', { detail: { added: -1 }, bubbles: true }));
633            })
634            .catch(error => {
635                console.log(`Error removing addon GPID ${gpid} from cart: ${error}`);
636            });
637        },
638
639        // Convert plural noun to its singular form by checking for common pluralization rules
640        convertToSingular(groupName) {
641            let word = groupName.trim();
642
643            // Primary Rule
644            if (word.endsWith('s') && !word.endsWith('ss')) {
645                // Rule 1: Remove "s" at end of plural word
646                word = word.slice(0, -1);
647            }
648
649            // Additional Rules
650            if (word.endsWith('ies')) {
651                // Rule 2: Replace "ies" with "y"
652                word = word.slice(0, -3) + 'y';
653            } else if (word.endsWith('es') && word.length > 3) {
654                // Rule 3: Replace "es" with "is"
655                word = word.slice(0, -2);
656            } else if (word.endsWith('ves')) {
657                // Rule 4: Replace "ves"" with "f" or "fe"
658                word = word.slice(0, -3) + 'f';
659            }
660
661            return word;
662        }
663    }));
664
665});
666
667$(document).ready(function () {
668    if (window.topSectionModel.cartQuantity != 0 &&
669        !window.topSectionModel.cartQuantityRequired &&
670        $('#quantitySelect').children().length == 0) {
671        const quantityDropdown = document.getElementById("quantitySelect");
672
673        for (let i = 1; i < 6; i++) {
674            let option = document.createElement("option");
675            let optionText = document.createTextNode(window.topSectionModel.cartQuantity * i);
676
677            option.setAttribute('value', window.topSectionModel.cartQuantity * i);
678            option.appendChild(optionText);
679            quantityDropdown.appendChild(option);
680        }
681    }
682});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.