PageSourceSearch

https://www.stentor-music.com/sites/layout-one/js/scripts.js?v=255

js stentor-music.com collected 2026-10-10 01:02:48 UTC 85,341 bytes, 2,037 lines download raw bytes

1var webshop = {
2    '_': {},
3    extend: function (object, beforeOrAfter, after) {
4        var oldObject = eval('' + object + ';');
5        eval(object + ' = function() { \
6            if(typeof after === \'object\') { \
7                beforeOrAfter.apply(this, arguments); \
8            } else { after = beforeOrAfter; } \
9            \
10            var result = oldObject.apply(this, arguments); \
11            \
12            if(typeof result === \'function\') { \
13                var mainArguments = [].slice.call(arguments, 0); \
14                mainArguments.push(result);\
15                result = after.apply(this, mainArguments); \
16            }\
17            if(typeof after === \'function\') { \
18                after.apply(this, arguments); \
19            }\
20            \
21            return result; \
22        }');
23    },
24    calendarOptionsExtended: {},
25    googleAnalytics: {
26        createCartViewEvent: function (productDetails) {
27            if (shopconfig.gaEnabled) {
28                var ga4Event = {
29                    currency: shopconfig.currency_code,
30                    value: 0,
31                    items: []
32                };
33
34                // Loop through each item in the cart
35                for (var i = 0; i < productDetails.length; i++) {
36                    // Update the total value of the order by adding the items price to the total
37                    ga4Event.value += productDetails[i].Price * productDetails[i].Quantity;
38                    ga4Event.items.push({
39                        item_id: productDetails[i].SKU,
40                        item_name: productDetails[i].Name,
41                        price: productDetails[i].Price,
42                        quantity: productDetails[i].Quantity
43                    });
44                }
45
46                gtag("event", "view_cart", ga4Event);
47            }
48        },
49        /**
50         * Send a Google Analytics checkout event.
51         *
52         * Takes in the product details, an actionField object which specifies the checkout step and adds the items in the cart to the event
53         *
54         * @param {Object} productDetails - Has all the products that we will add to the checkout event.
55         * @param {Object} actionField    - Specify the step in the checkout process i.e. 1 = Checked out, 2 = Address details etc.
56         * @return {void}
57         */
58        createCheckoutEvent: function (productDetails, actionField) {
59            if (shopconfig.gaEnabled) {
60                var ga4Event = {
61                    currency: shopconfig.currency_code,
62                    value: 0,
63                    items: []
64                };
65
66                // Loop through each item in the cart
67                for (var i = 0; i < productDetails.length; i++) {
68                    // Update the total value of the order by adding the items price to the total
69                    ga4Event.value += productDetails[i].Price * productDetails[i].Quantity;
70                    ga4Event.items.push({
71                        item_id: productDetails[i].SKU,
72                        item_name: productDetails[i].Name,
73                        price: productDetails[i].Price,
74                        quantity: productDetails[i].Quantity
75                    });
76                }
77
78                switch (actionField.step) {
79                    case 1:
80                        gtag("event", "begin_checkout", ga4Event);
81                        break;
82                    case 2:
83                        gtag("event", "add_shipping_info", ga4Event);
84                        break;
85                    case 3:
86                        ga4Event.payment_type = actionField.option;
87                        gtag("event", "add_payment_info", ga4Event);
88                        break;
89                }
90            }
91        }
92    },
93    googleTagManager: {
94        /**
95         * Add step of the checkout event and the customers products to the dataLayer.
96         *
97         * Takes in the product details and an actionField object which specifies the checkout step
98         * and adds each product to the Google Tag Manager dataLayer.
99         *
100         * @param {Object} productDetails Has all the products that we will add to the dataLayer.
101         * @param {Object} actionField    Specify the step in the checkout process i.e. 1 = Checked out, 2 = Address details etc.
102         * @return {void}
103         */
104        addCheckoutStepToDataLayer: function (productDetails, actionField) {
105            if (typeof dataLayer === 'object') {
106                var dataLayerProducts = [];
107
108                let gtagEvent = '';
109                switch (actionField.step) {
110                    case 1:
111                        gtagEvent = 'begin_checkout';
112                        break;
113                    case 2:
114                        gtagEvent = 'add_shipping_info';
115                        break;
116                    case 3:
117                        gtagEvent = 'add_payment_info';
118                        break;
119                    default:
120                        gtagEvent = 'begin_checkout';
121                        break;
122                }
123
124                let value = 0;
125                for (var i = 0; i < productDetails.length; i++) {
126                    value += productDetails[i].Price * productDetails[i].Quantity;
127                    dataLayerProducts.push({
128                        'item_name': productDetails[i].Name,
129                        'item_id': productDetails[i].SKU,
130                        'price': productDetails[i].Price,
131                        'quantity': productDetails[i].Quantity
132                    });
133                }
134
135                dataLayer.push({
136                    'event': gtagEvent,
137                    'ecommerce': {
138                        'currency': shopconfig.currency_code,
139                        'value': value,
140                        'items': dataLayerProducts
141                    }
142                });
143            }
144        }
145    },
146    cart: {
147        ajaxAddToCartEvents: {
148            useToolTip: true,
149            onclick: function () {
150                if ($(this).attr('href') == 'javascript:;') {
151                    return false;
152                }
153
154                var packSize = webshop.cart.addToCart.validatePackSize($(this));
155                if (packSize.error) {
156                    alert(packSize.error);
157                    return false;
158                }
159
160                var holdpass = $(this);
161                var btn_html = $(this)[0].innerHTML;
162                $(this).html(webshop._.webshop.loading.text);
163
164                var addToHref = $(this).attr('href');
165
166                // Get quantity of added product
167                var quantity = addToHref.split('||').pop();
168
169                if (packSize.hasPackSize) {
170                    addToHref += '&packSize=' + packSize.PackSize.code + '&packSizeValue=' + packSize.PackSizeValue;
171                }
172
173                try {
174                    if (manpacks && manpacks.id) {
175                        addToHref += '&manpackid=' + manpacks.id;
176                    }
177                } catch (e) {
178                    //Manpack not activated
179                }
180
181                var extraParams = webshop.cart.addToCart.validateExtraAttributes($(this).data('productid'));
182                if (extraParams) {
183                    addToHref += extraParams;
184                }
185
186                webshop.cart.ajaxAddToCartEvents.queue.push({
187                    url: addToHref + '&route=ajax' + webshop.cart.ajaxAddToCartEvents.additionalRequestParam
187s,
188                    'dataType': 'json',
189                    success: webshop.cart.ajaxAddToCartEvents.success(holdpass, btn_html)
190                });
191
192                const multiAdd = addToHref.includes('addmulti');
193
194                // Check if Google Tag Manager is enabled
195                if (shopconfig.tagManagerEnabled && typeof dataLayer === 'object') {
196                    let price = parseFloat($(this).attr('data-analytics-value') || '0.00');
197                    let products = [{
198                        item_id: $(this).attr('data-product-sku'),
199                        item_name: decodeURIComponent(($(this).attr('data-product-name') + '').replace(/\+/g, '%20')),
200                        quantity: parseInt(quantity),
201                        price: price
202                    }];
203
204                    if (multiAdd) {
205                        const rawProducts = $(this).attr('data-gtm-add-to-cart');
206                        if (!rawProducts) {
207                            console.info('Cannot send analytics event as the items are missing')
208                            return false;
209                        }
210
211                        products = JSON.parse(rawProducts);
212                    }
213
214                    let value = products.reduce((sum, product) => {
215                        const price = parseFloat(product.price) || 0;
216                        const quantity = parseInt(product.quantity) || 0;
217                        return sum + (price * quantity);
218                    }, 0);
219
220                    // Add product data to dataLayer on product view
221                    dataLayer.push({
222                        'event': 'add_to_cart',
223                        'ecommerce': {
224                            'currency': shopconfig.currency_code,
225                            'value': value,
226                            'items': products
227                        }
228                    });
229                }
230
231                if (shopconfig.gaEnabled) {
232                    let price = parseFloat($(this).attr('data-analytics-value') || '0.00');
233                    let items = [{
234                        item_id: $(this).attr('data-product-sku'),
235                        item_name: decodeURIComponent(($(this).attr('data-product-name') + '').replace(/\+/g, '%20')),
236                        quantity: parseInt(quantity),
237                        price: price
238                    }];
239
240                    if (multiAdd) {
241                        const rawItems = $(this).attr('data-ga-add-to-cart');
242                        if (!rawItems) {
243                            console.info('Cannot send analytics event as the items are missing')
244                            return false;
245                        }
246
247                        items = JSON.parse(rawItems);
248                    }
249
250                    let value = items.reduce((sum, product) => {
251                        const price = parseFloat(product.price) || 0;
252                        const quantity = parseInt(product.quantity) || 0;
253                        return sum + (price * quantity);
254                    }, 0);
255
256                    gtag("event", "add_to_cart", {
257                        currency: shopconfig.currency_code,
258                        value: value,
259                        items: items
260                    });
261                }
262                return false;
263            },
264            queue: {
265                running: false,
266                list: [],
267                push: function (params) {
268                    params.complete = function () {
269                        webshop.cart.ajaxAddToCartEvents.queue.running = false;
270                        webshop.cart.ajaxAddToCartEvents.queue.next();
271                    };
272
273                    if (webshop.cart.ajaxAddToCartEvents.queue.running) {
274                        webshop.cart.ajaxAddToCartEvents.queue.list.push(params);
275                    } else {
276                        webshop.cart.ajaxAddToCartEvents.queue.run(params);
277
278                    }
279                },
280                next: function () {
281                    if (webshop.cart.ajaxAddToCartEvents.queue.list.length > 0) {
282                        var params = webshop.cart.ajaxAddToCartEvents.queue.list.pop();
283                        webshop.cart.ajaxAddToCartEvents.queue.run(params);
284                    }
285                },
286                run: function (params) {
287                    webshop.cart.ajaxAddToCartEvents.queue.running = $.ajax(
287params);
288                }
289            },
290            additionalRequestParams: '',
291            success: function ($btn, btnHtml) {
292                var _this = this;
293
294                return function (data) {
295                    // tooltip events
296                    if (webshop.cart.ajaxAddToCartEvents.useToolTip) {
297                        _this.handleResponse($btn, btnHtml, data);
298                    } else if ($btn) {
299                        $btn.html(webshop._.webshop.added);
300                    }
301                    // minicart
302                    if (data.mini_cart) {
303                        $('.js-cart-mini').replaceWith(data.mini_cart);
304                        intact_products();
305                    }
306
307                    if (shopconfig.isKlaviyoAvailable) {
308                        var quantity = $btn.attr('href').split('||').pop().replace('&', '');
309
310                        $.get('/system/klaviyo/triggerAddedToCartEvent.html',
311                            {
312                                productId: $btn.data('productid'),
313                                quantity: parseInt(quantity),
314                            }
315                        );
316                    }
317                };
318            },
319            handleResponse: function ($btn, btnHtml, data) {
320                if (data.type === "wishlist") {
321                    $btn.html(btnHtml);
322                    webshop.tooltip.show($btn.parent(), 'Added to Wedding List!');
323                } else if (!data.outOfStockErrors && !data.cart_check) {
324                    this.handleSuccessAdded($btn, btnHtml, data)
325                } else {
326                    this.handleCartErrors($btn, btnHtml, data);
327                }
328
329                webshop.tooltip.timeout($btn.parent(), 4000);
330            },
331            handleSuccessAdded: function ($btn, btnHtml, data) {
332                $btn.html(webshop._.webshop.added);
333                webshop.tooltip.show($btn.parent(), utils.decodeEntities(webshop._.global.addedtocart));
334
335                webshop.cart.ajaxAddToCartEvents.updateMiniCart(data);
336
337                webshop.accessibility.screenReaderMessage(webshop._.global.addedtocart);
338            },
339            handleCartErrors: function ($btn, btnHtml, data) {
340                var sku = $btn.attr('title');
341                $btn.html(webshop._.global.addtocart);
342                if (data.errors.hasOwnProperty(sku)) {
343                    webshop.tooltip.show($btn.parent(), data.errors[sku]);
344                }
345            },
346            updateMiniCart: function (data, useNetWithDelivery = false) {
347                const grossTotalContent = shopconfig.useFortaPlusNavStyling ? `${data.count} item(s) ${data.formatted.gross}` : data.formatted.gross;
348                let netTotalContent;
349
350                if (useNetWithDelivery) {
351                    netTotalContent = shopconfig.useFortaPlusNavStyling ? `${data.count} item(s) ${data.formatted.net_with_delivery}` : data.formatted.net_with_delivery;
352                } else {
353                    netTotalContent = shopconfig.useFortaPlusNavStyling ? `${data.count} item(s) ${data.formatted.net}` : data.formatted.net;
354                }
355
356                $('.items-total').html(data.count + ' <span class="items-total__text c-cart__copy"> item(s) - </span> ');
357                $('.gross-total').html(grossTotalContent);
358                $('.net-total').html(netTotalContent);
359            }
360        },
361        addToCart: {
362            _selectedUnitsForProducts: {/* populated once a selection is made */ },
363            _PackSizeOptions: {},
364            loginToSeePrice: function (redirectUrl) {
365                return '<span class="intact-pricelogin"><a href="/account/login.html?r=' + encodeURIComponent(redirectUrl) + '">' + webshop._.global.button.Login + '</a> ' + webshop._.global.toseeyourprice + '.</a>';
366            },
367            getProductIdFromButton: function (addButton) {
368                var productId = addButton.attr('data-productid');
369                if (addButton.attr('data-optionid')) {
370                    productId += '-' + addButton.attr('data-optionid');
371                }
372
373                return productId;
374            },
375            _optionsForPacks: [],
376            setPacksForOptions: function (set) {
377                this._optionsForPacks = set;
378            },
379            getPacksForOption: function (productId) {
380                return this._optionsForPacks[productId];
381            },
382            selectPackSizeOption: function (productId, optionsIndex) {
383                const _options = this.getPackSize(productId)[optionsIndex];
384                const options = JSON.parse(JSON.stringify(_options)); // Clone the object and use it
385
386                options.code = optionsIndex; // Used to identify which pack to add to cart, needs to be the index (cart.ajaxAddToCartEvents.onclick)
387
388                let label = options.label;
389                label += (options.showInputBox) ? ':' : '';
390                $('.products__add__selling-units-label[data-productid="' + productId + '"]').text(label);
391
392                const inputBox = this.getPackSizeInputForProduct(productId);
393                if (options.showInputBox) {
394                    inputBox.show();
395                } else {
396                    inputBox.hide();
397                }
398
399                this._selectedUnitsForProducts[productId] = options;
400
401                this.updatePriceWithPackSize(options, productId);
402                this.showPriceBreaksTable(options.customPricingValues?.SellingUnits);
403            },
404            updatePriceWithPackSize: function (options, productId) {
405                try {
406                    if (!options.lowestPrice && !options.customPricingValues?.Price) {
407                        // No price to update. Most likely a custom pricing processor
408                        return;
409                    }
410
411                    const hasLowerPrice = options.lowestPrice < options.customPricingValues.Price;
412                    const priceSplit = ('' + (options.lowestPrice || options.customPricingValues.Price)).split('.');
413                    const stock = options.customPricingValues.Stock;
414
415                    let $priceAndStock = $('.l-product__priceblock-price-and-stock'); // Product Page
416                    if (!$priceAndStock.length) { // Listing Page
417                        $priceAndStock = $('.products__add__selling-units-label[data-productid="' + productId + '"]').parents('.products__item');
418                    }
419
420                    const $from = $priceAndStock.find('.price__prefix');
421                    const $priceDigit = $priceAndStock.find('.price__digit');
422                    const $priceDecimal = $priceAndStock.find('.price__decimal');
423                    const $wasPrice = $priceAndStock.find('.was-price');
424                    const $stock = $priceAndStock.find('.stock-available-value');
425
426                    $priceDigit.text(priceSplit[0]);
427                    $priceDecimal.text(`.${priceSplit[1] || '00'}`);
428
429                    if ($from) {
430                        if (hasLowerPrice) {
431                            $from.show();
432                        } else {
433                            $from.hide();
434                        }
435                    }
436                    if ($wasPrice) {
437                        if (options.packValue != 1) {
438                            $wasPrice.hide();
439                        } else {
440                            $wasPrice.show();
441                        }
442                    }
443                    if ($stock && stock) {
444                        $stock.text(stock);
445                    }
446                } catch (e) {
447                    // console.error('Unable to set price of UoM', e);
448                }
449            },
450            showPriceBreaksTable: function (sellingUnit) {
451                try {
452                    $('.qtybreaks-table').hide();
453                    $(`[data-uom=${sellingUnit}]`).show();
454                } catch (e) {
455                    // console.error(e);
456                }
457            },
458            getPackSizeInputForProduct: function (productId) {
459                return $('.products__add__selling-units__input[data-productid="' + productId + '"]');
460            },
461            getPackSize: function (productId) {
462                return this._PackSizeOptions[productId];
463            },
464            setPackSizeOptions: function (productId, options) {
465                this._PackSizeOptions[productId] = options;
466            },
467            getPackSizeForProduct: function (productId) {
468                if (this._selectedUnitsForProducts[productId]) {
469                    return this._selectedUnitsForProducts[productId];
470                } else {
471                    return false;
472                }
473            },
474            validatePackSize: function (addButton) {
475                var prodId = this.getProductIdFromButton(addButton);
476                if (webshop.cart.addToCart.getPackSize(prodId)) {
477                    // product has selling units - ensure its selected
478                    var sellingOption = webshop.cart.addToCart.getPackSizeForProduct(prodId);
479                    if (!sellingOption) {
480                        return { 'error': 'You MUST select a pack size for this product before adding to your order.' };
481                    }
482
483                    // ensure the input was filled in > 0 also if this option requires it
484                    var sellingInput = webshop.cart.addToCart.getPackSizeInputForProduct(prodId);
485                    if (sellingOption.showInputBox) {
486                        if (parseInt(sellingInput.val()) <= 0) {
487                            return { 'error': 'You MUST enter a value for ' + sellingOption.label };
488                        }
489                    }
490
491                    return {
492                        'success': true,
493                        'hasPackSize': true,
494                        'PackSize': sellingOption,
495                        'PackSizeValue': sellingInput.val()
496                    };
497                }
498
499                return {
500                    'success': true,
501                    'hasPackSize': false
502                };
503            },
504            validateExtraAttributes: function (productid) {
505                if (!productid) return false;
506
507                var inputs = $('.addtocart-extra[data-productid="' + productid + '"]');
508                var set = [];
509                for (var i = 0; i < inputs.length; i++) {
510                    var input = $(inputs[i]);
511
512                    var value = encodeURIComponent(input.val());
513                    var label = input.data('extra').replace(/[\W]+/g, " ");
514                    set.push('&extra[' + label + ']=' + value);
515                }
516
517                return set.length > 0 ? set.join('') : false;
518            }
519        },
520        generateMultiAddToCartLink: function (inputs) {
521            var url = '/checkout.html?addmulti=';
522
523            for (var i = 0; i < inputs.length; i++) {
524                var product = inputs[i].getAttribute('data-productid');
525                var qty = inputs[i].value;
526                url += product + '|||' + qty;
527                if (i + 1 < inputs.length) {
528                    url += '||||';
529                }
530            }
531            return url;
532        },
533        generateAddToCartLink: function () {
534            //Find out the type of button it is
535            var isQtyBox = $(this).hasClass('addmultiinput');
536
537            var $addToCartInput = isQtyBox ? $(this) : $('.addmultiinput[data-productid="' + $(this).attr('data-productid') + '"]');
538            var $usingAddToCartButton = isQtyBox && $(this).next().is('.addbtn') ? $(this).next() : $('.addbtn[data-productid="' + $(this).attr('data-productid') + '"]');
539
540            $.each($usingAddToCartButton, function () {
541                if ($(this).hasClass('disabled')) {
542                    $(this).attr('href', 'javascript:;');
543                    return false;
544                }
545
546                var prodid = $(this).attr('data-productid');
547                var val = parseFloat($addToCartInput.val());
548                val = (val > 0) ? val : 1;
549
550                if ($(this).attr('data-optionid')) {
551                    prodid += '-' + $(this).attr('data-optionid');
552                }
553
554                var lineRef = '';
555                var lineRefInput = $('.addtocart-ref[data-productid="' + $(this).attr('data-productid') + '"]');
556                if (lineRefInput.is('*')) {
557                    lineRef += '&ref=' + lineRefInput.val();
558                }
559                var wishlistLink = '';
560                if ($(this).attr('data-wishlist-id')) {
561                    wishlistLink += '&wishlist_id=' + $(this).attr('data-wishlist-id');
562                }
563
564                var batchMatchLink = '',
565                    $batchMatch = $('input[name="batchMatch"]');
566                if ($batchMatch.length && $batchMatch.is(':checked')) {
567                    batchMatchLink = '&batchMatch=1';
568                }
569
570                var parts = $(this).attr('href').split('&');
571                var retained = [];
572                parts.forEach(function (part) {
573                    if (/extra.*/.test(part)) {
574                        retained.push(part);
575                    }
576                });
577                var extra = ('&' + retained.join('&'));
578
579                var href = '/checkout.html?addqty=' + prodid + '||' + val + lineRef + wishlistLink + batchMatchLink + extra;
580                $(this).attr('href', href);
581
582                var $_wish_list = $('.wish-list-btn[data-productid="' + $(this).attr('data-productid') + '"]');
583                var wishListID = $_wish_list.attr('data-wishlist-id');
584                $_wish_list.attr('href', '/account/wishlist/view.html?id=' + wishListID + '&add=' + prodid + '&qty=' + val);
585            });
586        }
587    },
588    delivery: {
589        addressBook: {
590            addresses: [],
591            GLSDeliveryEnabled: false,
592            hasInitialized: false,
593            init: function (data, isGLS, selected) {
594                var _this = this,
595                    $addressBookSelect = $('#frm-addressbook'),
596                    selectOptions = '';
597                data = webshop.delivery.addressBook.beforeInit(data);
598
599                selected = (selected || false);
600
601                _this.GLSDeliveryEnabled = isGLS || false;
602
603                $.each(data, function (key, address) {
604                    var labelArray = [];
605
606                    if (address['Address']['company_name']) {
607                        labelArray.push(address['Address']['company_name']);
608                    }
609
610
611                    if (address['Address']['custname']) {
612                        labelArray.push(address['Address']['custname']);
613                    }
614
615                    // address lines
616                    for (var i = 1; i < 6; i++) {
617                        if (address['Address']['address' + i]) {
618                            labelArray.push(address['Address']['address' + i]);
619                        }
620                    }
621
622                    // postcode
623                    if (address['Address']['postcode']) {
624                        labelArray.push(address['Address']['postcode']);
625                    }
626
627                    // phone
628                    if (address['Address']['phonenumber']) {
629                        labelArray.push(address['Address']['phonenumber']);
630                    }
631
632                    // email
633                    if (address['Address']['email']) {
634                        labelArray.push(address['Address']['email']);
635                    }
636
637                    delete address['Address']['Raw'];
638
639                    var selectedOption = (address.DeliveryAddressesID == selected ? 'selected="selected"' : '');
640                    selectOptions += '<option ' + selectedOption + ' value="' + address.DeliveryAddressesID + '">' + labelArray.join(', ') + '</option>';
641                    _this.addresses[address.DeliveryAddressesID] = address.Address;
642                });
643
644                // Set Select Options for Address Book
645                $addressBookSelect.append(selectOptions);
646
647                if ($('#frm-addressbook').is('*')) {
648                    var pseudoSelect = $('#frm-addressbook');
649                } else {
650                    var pseudoSelect = document.createElement('option');
651                    pseudoSelect.value = selected;
652                }
653
654                // default the address from the list based on what's in the session
655                var defaultAddressId = parseInt($($addressBookSelect).attr('data-id'));
656
657                if (defaultAddressId) {
658                    $addressBookSelect.val(defaultAddressId);
659                }
660
661                $('.delivery-details-section input').on('input', function () {
662                    $($addressBookSelect).val('');
663                });
664
665                if (shopconfig.delivery_contact_linked_inputs) {
666                    $(shopconfig.delivery_contact_linked_inputs).on('change', function () {
667                        $($addressBookSelect).val('');
668                    })
669                }
670
671                webshop.delivery.addressBook.fillAddress(pseudoSelect, false);
672                this.hasInitialized = true;
673                webshop.delivery.addressBook.afterInit();
674            },
675            afterInit: function () {
676                return; // placeholder to chain another action after the init script
677            },
678            beforeInit: function (data) {
679                return data;
680            },
681            fillAddress: function (el, updateCart) {
682                if (!this.hasInitialized) {
683                    return;
684                }
685
686                var updateCart = !!updateCart;
687
688                var addressId = $(el).val(),
689                    addressLines = this.addresses[addressId],
690                    addressKeys = ['address1', 'address2', 'address3', 'address4', 'address5', 'postcode', 'firstname', 'lastname', 'company_name', 'phone_number'];
691                $countryEl = $('#frm-address5');
692
693                if (addressLines) {
694                    if ($('#onePageDeliveryAddress').length !== 0) {
695                        var formattedAddress = '';
696                        $.each(addressLines, function (key) {
697                            if (addressLines[key] && (addressKeys.indexOf(key) !== -1) && addressLines[key].length > 0) formattedAddress += addressLines[key] + '\n';
698                        });
699
700                        $('#onePageDeliveryAddress').text(formattedAddress);
701
702                        if (updateCart) {
703                            checkout.unsetCart();
704                        }
705
706                    } else if (!this.GLSDeliveryEnabled) {
707                        //If the selected address has a different c
707ountry code then alert user other wise update form with selected address details
708                        // address lines
709                        for (var i = 1; i <= 4; i++) {
710                            // if address line is not exist fill it as empty string
711                            addressLines['address' + i] = addressLines['address' + i] || '';
712                        }
713                        // set all provided fields
714                        $.each(addressLines, function (key) {
715                            if (key !== 'address5') {
716                                var $input = $('#frm-' + key + ', #frm-delivery-' + key);
717                                if ($input.length) {
718                                    var len = addressLines[key] || '';
719                                    var unlen = $('<div>').html(len).text();
720                                    $input.val(unlen); // convert false value to empty string
721                                }
722                            }
723                        });
724
725                        if (shopconfig.delivery_edit_country) {
726                            $('#frm-editcountry').val(addressLines['address5']);
727                        }
728
729                        if (shopconfig.delivery_company_name) {
730                            $('#frm-company_name').val(addressLines['company_name']);
731                        }
732                    } else {
733                        //Address line 5 is always the country
734                        webshop.delivery.GLSDelivery.usedFindInAddress = false;
735
736                        if (!webshop.delivery.GLSDelivery.isIreland(addressLines) && $('#frm-address5').val() === '58') {
737                            alert('The Address you have chosen has a different Country. Please return to the Cart and select the correct Delivery country.');
738                            return false;
739                        }
740
741                        var foundCountry = $('#frm-address5').val();
742                        webshop.delivery.GLSDelivery.changeCountry(foundCountry, false, function () {
743                            //See if we have an address line 4, 3 or 2 which matches any county
744                            var foundCounty = webshop.delivery.GLSDelivery.findInAddress($('#frm-address4ie').find('option'), addressLines, addressLines['address3']);
745                            webshop.delivery.GLSDelivery.setCountyName(foundCounty, function () {
746                                var foundTownland = webshop.delivery.GLSDelivery.findInAddress($('#frm-address3ie').find('option'), addressLines, addressLines['address4']);
747                                webshop.delivery.GLSDelivery.setTownName(foundTownland);
748                            })
749                        });
750
751                        $('#frm-address1').val(addressLines['address1'] || '');
752                        $('#frm-address2').val(addressLines['address2'] || '');
753                        if ($('#frm-address5').val() !== '58') {
754                            //set the default values
755                            for (var k = 3; k < 5; k++) {
756                                $('#frm-address' + k).val(addressLines['address' + k] || '');
757                            }
758                            $('#frm-postcode').val(addressLines['postcode'] || '');
759                        }
760
761                        if ($('#frm-company_name').length) {
762                            $('#frm-company_name').val(addressLines['company_name']);
763                            $('#frm-contact_name').val($.trim(addressLines['custname'].split('.')[0]));
764                        }
765                    }
766                } else {
767                    // clear address lines fields
768                    for (var i = 1; i <= 4; i++) {
769                        $('#frm-address' + i).val('');
770                    }
771
772                    // clear postcode field
773                    $('#frm-postcode').val('');
774
775                    // clear phone field
776                    $('#frm-phonenumber').val('');
777                }
778            }
779        },
780        GLSDelivery: {
781            //Skip over a used county value in findInAddress
782            usedFindInAddress: false,
783            /**
784             * For reading from the address book - we don't know 
784what way the address might be formatted
785             * - Using this function we scan the address for any line 1-4 which might match county or townland
786             */
787            findInAddress: function (options, addressLines, defaultValue) {
788                var foundValue = defaultValue;
789                for (var i = 5; i > 1; i--) {
790                    if (webshop.delivery.GLSDelivery.usedFindInAddress == i) {
791                        continue;
792                    }
793
794                    var suggestedValue = addressLines['address' + i].toLowerCase();
795                    if (!suggestedValue) {
796                        continue;
797                    }
798
799                    options.each(function () {
800                        //Try to find a matching county
801                        var value = $(this).val();
802                        if (value.toLowerCase() === suggestedValue || $(this).text().toLowerCase() === suggestedValue) {
803                            foundValue = value;
804                            webshop.delivery.GLSDelivery.usedFindInAddress = i;
805                            return false;
806                        }
807                    });
808                }
809
810                return foundValue;
811            },
812            isIreland: function (addressLines) {
813                var foundValue = false;
814                for (var i = 5; i > 1; i--) {
815                    var suggestedValue = addressLines['address' + i].toLowerCase();
816                    //Try to find a matching county
817                    if (suggestedValue === 'ireland' || suggestedValue === 'ie') {
818                        foundValue = true;
819                    }
820                }
821
822                return foundValue;
823            },
824            setCountyName: function (value, callback) {
825                var $county = $('#frm-address4ie'),
826                    $town = $('#frm-address3ie'),
827                    $postcode = $('#frm-postcode');
828
829                // set county force
830                if (value) {
831                    $county.val(value);
832                }
833
834                $('#frm-address4').val($county.val());
835
836                $postcode.val('');
837                $town.html('<option>Loading...</option>');
838                $.get('/checkout/delivery/ajax/getTowns.html', { county: $county.val() }, function (data) {
839                    if (data.ret) {
840                        $town.html('<option value="">Select a Townland</option>');
841                        $.each(data.towns, function (key, value) {
842                            $town.append('<option value="' + value + '">' + value + '</option>');
843                        });
844                        if (callback) {
845                            callback();
846                        }
847                    } else {
848                        $town.html('<option></option>');
849                    }
850                });
851            },
852            setTownName: function (value) {
853                var $county = $('#frm-address4ie'),
854                    $town = $('#frm-address3ie'),
855                    $postcode = $('#frm-postcode');
856
857                // set town force
858                if (value) {
859                    $town.val(value);
860                }
861
862                $('#frm-address3').val($town.val());
863
864                $postcode.val('Loading...');
865                $.get('/checkout/delivery/ajax/getPostcode.html', {
866                    county: $county.val(),
867                    town: $town.val()
868                }, function (data) {
869                    if (data.ret) {
870                        $postcode.val(data.postcode);
871                    } else {
872                        $postcode.val('');
873                    }
874                });
875            },
876            changeCountry: function (value, notClear, callback) {
877                value = value || false;
878                notClear = notClear || false;
879
880                var $country = $('#frm-address5'),
881                    $ieCountyWrap = $('.control-group-frm-address4ie'),
882                    $ieTownlandWrap = $('.control-group-frm-address3ie'),
883                    $countyWrap = $('.control-group-frm-address4'),
884                    $townWrap = $('.control-group-frm-address3'),
885                    $postcode = $('#frm-postcode');
886
887                if (!notClear) {
888                    $ieCountyWrap.find('select').val('');
889                    $ieTownlandWrap.find('select').val('');
890                    $countyWrap.find('input').val('');
891                    $townWrap.find('input').val('');
892                    $postcode.val('');
893                }
894
895                // set country code force
896                if (value) {
897                    $country.val(value);
898                }
899
900                if ($country.val() === '58') {
901                    $countyWrap.hide();
902                    $townWrap.hide();
903                    $ieCountyWrap.show();
904                    $ieTownlandWrap.show();
905                    $postcode.attr('readonly', 'readonly');
906
907                    if (!notClear) {
908                        var $county = $ieCountyWrap.find('select');
909                        $county.html('<option>Loading...</option>');
910                        $.get('/checkout/delivery/ajax/getCounties.html', function (data) {
911                            if (data.ret) {
912                                $county.html('<option value="">Select a County</option>');
913                                $.each(data.counties, function (key, value) {
914                                    $county.append('<option value="' + value + '">' + value + '</option>');
915                                });
916                                if (callback) {
917                                    callback();
918                                }
919                            } else {
920                                $county.html('<option>Error</option>');
921                            }
922                        });
923                    }
924                } else {
925                    $ieCountyWrap.hide();
926                    $ieTownlandWrap.hide();
927                    $countyWrap.show();
928                    $townWrap.show();
929                    $postcode.attr('readonly', false);
930                }
931            }
932        },
933        ePostcode: {
934            requestLock: false,
935            init: function () {
936                $(window).on('click', function (event) {
937                    $('.epostcode-addresses-dropdown').hide();
938                });
939            },
940            showAddresses: function (el, type) {
941
942                var _this = this,
943                    $el = $(el),
944                    postcode = $el.closest('.controls').find('input').val(),
945                    buttonText = $el.text();
946
947                if (_this.requestLock) {
948                    return;
949                }
950
951                $el.text('Loading...');
952                _this.requestLock = true;
953                $.get('/checkout/delivery/epostcode/getAddressesByPostcode.html', { postcode: postcode }, function (data) {
954                    $el.text(buttonText);
955                    _this.requestLock = false;
956
957                    if (data.ret) {
958                        var $ul = $('<ul class="dropdown-menu epostcode-addresses-dropdown">')
959                        $.each(data.addresses, function (key, value) {
960                            $ul.append('<li><a href="javascript:" onclick="webshop.delivery.ePostcode.fillAddress(\'' + key + '\', \'' + type + '\')">' + value + '</a></li>');
961                        });
962
963                        $el.closest('.controls').append($ul);
964                        $ul.show();
965                    } else {
966                        alert('Please enter a valid postcode');
967                    }
968                });
969            },
970            fillAddress: function (addressNum, type) {
971                $.get('/checkout/delivery/epostcode/getAddress.html', { addressNum: addressNum }, function (data) {
972                    if (data.ret) {
973
974                        if (type === 'quote') {
975                            var prefix = '#quote-company-';
976                        }
977                        else {
978                            var prefix = '#frm-';
979                        }
980
981                        // address lines
982                        if (type === 'billing' || type === 'card') {
983                            var fieldName = 'card';
984                        } else if (type === 'signup') {
985                            var fieldName = 'billing';
986                        } else {
987                            var fieldName = 'address';
988                        }
989
990                        for (var i = 1; i <= 4; i++) {
991                            $(prefix + fieldName + i).val(data.address['AddressLine' + i]);
992                        }
993                        $(prefix + 'firstname').val(data.address['firstname']);
994                        if ($(prefix + 'custname').is("*")) {
995                            $(prefix + 'custname').val(data.address['firstname']);
996                        }
997
998                        // postcode
999                        var postcodeInputId = ((type === 'billing') ? prefix + 'billingpostcode' : prefix + 'postcode');
1000                        $(postcodeInputId).val(data.address['PostCode']);
1001
1002
1003                        // country
1004                        if (type === 'billing') {
1005                            $(prefix + 'billing5').val('GB');
1006                        }
1007                    }
1008                });
1009            }
1010        },
1011        onDeliveryDueDateChange: function (silent) {
1012            var selectedValue = document.querySelector('input[name="delivery-due-date"]:checked').value;
1013            if (selectedValue === 'custom_due_date') {
1014                $('.delivery-due-date-input').removeClass('delivery-due-date-input-hidden');
1015            } else {
1016                $('.delivery-due-date-input').addClass('delivery-due-date-input-hidden');
1017            }
1018
1019            if (!silent) {
1020                checkout.unsetCart();
1021            }
1022        }
1023    },
1024    favourites: {
1025        requestLock: false,
1026        updateFavourites: function (el) {
1027            var _this = this,
1028                $btn = $(el),
1029                productId = $btn.attr('data-product-id'),
1030                optionId = $btn.attr('data-option-id'),
1031                groupId = $btn.attr('data-group-id'),
1032                favouriteId = $btn.attr('data-favourite-id'),
1033                requestUrl;
1034
1035            if (_this.requestLock) {
1036                return;
1037            }
1038
1039            if (!$btn.is('.is-active')) {
1040                requestUrl = '/account/favourites/ajax/addToFavourites.html';
1041            } else if (!$('.table-favourites').is('*')) {
1042                //Don't remove it on the fly - redirect to the favourites page
1043                window.location = '/account/favourites.html';
1044                return;
1045            } else {
1046                //We're likely on the favourites page so we'll on the fly toggle the status of the button
1047                requestUrl = '/account/favourites/ajax/removeFromFavourites.html';
1048            }
1049
1050            _this.requestLock = true;
1051            var payload = {
1052                productId: productId,
1053                optionId: optionId,
1054                groupId: groupId,
1055                favouriteId: favouriteId
1056            };
1057            if (window.shopconfig.favouritesGroups) {
1058                payload = Object.assign(payload, {});
1059            }
1060            $.post(requestUrl, payload, function (data) {
1061                if (data.ret) {
1062                    _this.setButtonState($btn, !$btn.is('.is-active'));
1063                    _this.setAriaForFavourites($btn, $btn.is('.is-active'));
1064                    if (!$btn.is('.is-active')) {
1065                        _this.removeRow(productId);
1066                    }
1067                }
1068                _this.requestLock = false;
1069            });
1070        },
1071        setAriaForFavourites($btn, isNowFavourite) {
1072            var groupsEnabled = window.shopconfig && window.shopconfig.favouritesGroups;
1073
1074            // 1. Update button ARIA state
1075            $btn.attr('aria-pressed', isNowFavourite);
1076
1077            if (groupsEnabled) {
1078                $btn.attr(
1079                    'aria-label',
1080                    isNowFavourite ? $btn.data('aria-view') : $btn.data('aria-add')
1081                );
1082            } else {
1083                $btn.attr(
1084                    'aria-label',
1085                    isNowFavourite ? $btn.data('aria-remove') : $btn.data('aria-add')
1086                );
1087            }
1088
1089            // 2. Speak the result
1090            var addedMsg = $btn.data('aria-added');
1091            if (groupsEnabled && isNowFavourite) {
1092                webshop.accessibility.screenReaderMessage(addedMsg);
1093            } else {
1094                var removedMsg = $btn.data('aria-removed')
1095                webshop.accessibility.screenReaderMessage(
1096                    (isNowFavourite ? addedMsg : removedMsg)
1097                );
1098            }
1099        },
1100        checkButtonState: function (el) {
1101            var _this = this,
1102                $btn = $(el),
1103                productId = $btn.attr('data-product-id'),
1104                optionId = $btn.attr('data-option-id');
1105
1106            $.post('/account/favourites/ajax/getFavourite.html', {
1107                productId: productId,
1108                optionId: optionId
1109            }, function (data) {
1110                if (data.ret && (optionId == $btn.attr('data-option-id'))) {
1111                    _this.setButtonState($btn, data.isFavourite);
1112                }
1113            });
1114        },
1115        setButtonState: function (el, setActive) {
1116            var $btn = $(el),
1117                isBtnActive = $btn.is('.is-active');
1118
1119            setActive = setActive || false;
1120
1121            if (setActive && !isBtnActive) {
1122                $btn.addClass('is-active');
1123                $btn.html($btn.data('text-remove'));
1124            } else if (!setActive && isBtnActive) {
1125                $btn.removeClass('is-active');
1126                $btn.html($btn.data('text-add'));
1127            }
1128        },
1129        removeRow: function (productId) {
1130            $row = $('[data-product-id=' + productId + ']');
1131            if ($row.is('*')) {
1132                $row.remove();
1133            }
1134        },
1135        updateFavouritesGroupItems: function (el) {
1136            var _this = this,
1137                $btn = $(el),
1138                productId = $btn.attr('data-product-id'),
1139                optionId = $btn.attr('data-option-id'),
1140                groupId = $btn.attr('data-group-id'),
1141                favouriteId = $btn.attr('data-favourite-id'),
1142                groupItemId = $btn.attr('data-group-item-id'),
1143                requestUrl;
1144
1145            if (_this.requestLock) {
1146                return;
1147            }
1148
1149            if (!$btn.is('.is-active')) {
1150                requestUrl = '/account/favourites/ajax/addToFavourites.html';
1151            } else if (!$('.table-favourites').is('*')) {
1152                //Don't remove it on the fly - redirect to the favourites page
1153                window.location = '/account/favourites.html';
1154                return;
1155            } else {
1156                //We're likely on the favourites page so we'll on the fly toggle the status of the button
1157                requestUrl = '/account/favourites/ajax/removeFromFavourites.html';
1158            }
1159
1160            _this.requestLock = true;
1161            var payload = {
1162                productId: productId,
1163                optionId: optionId,
1164                groupId: groupId,
1165                favouriteId: favouriteId,
1166                groupItemId: groupItemId,
1167            };
1168            if (window.shopconfig.favouritesGroups) {
1169                payload = Object.assign(payload, {});
1170            }
1171            $.post(requestUrl, payload, function (data) {
1172                if (data.ret) {
1173                    _this.setButtonState($btn, !$btn.is('.is-active'));
1174                    if (!$btn.is('.is-active') && groupItemId) {
1175                        _this.removeRowItem(groupItemId);
1176                    }
1177                }
1178                _this.requestLock = false;
1179            });
1180        },
1181        checkButtonState: function (el) {
1182            var _this = this,
1183                $btn = $(el),
1184                productId = $btn.attr('data-product-id'),
1185                optionId = $btn.attr('data-option-id');
1186
1187            $.post('/account/favourites/ajax/getFavourite.html', {
1188                productId: productId,
1189                optionId: optionId
1190            }, function (data) {
1191                if (data.ret && (optionId == $btn.attr('data-option-id'))) {
1192                    _this.setButtonState($btn, data.isFavourite);
1193                }
1194            });
1195        },
1196        setButtonState: function (el, setActive) {
1197            var $btn = $(el),
1198                isBtnActive = $btn.is('.is-active');
1199
1200            setActive = setActive || false;
1201
1202            if (setActive && !isBtnActive) {
1203                $btn.addClass('is-active');
1204                $btn.html($btn.data('text-remove'));
1205            } else if (!setActive && isBtnActive) {
1206                $btn.removeClass('is-active');
1207                $btn.html($btn.data('text-add'));
1208            }
1209        },
1210        removeRowItem: function (groupItemId) {
1211            $row = $('[data-group-item-id=' + groupItemId + ']');
1212            if ($row.is('*')) {
1213                $row.remove();
1214            }
1215        }
1216    },
1217    cookies: {
1218        accept: function () {
1219            webshop.cookies.create('acceptCookies', true, 365);
1220            $('#cookie-notice').hide();
1221        },
1222        create: function (name, value, days) {
1223            var expires = "";
1224            if (days) {
1225                var date = new Date();
1226                date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
1227                expires = "; expires=" + date.toUTCString();
1228            }
1229            document.cookie = name + "=" + value + expires + "; path=/";
1230        },
1231        read: function (name) {
1232            var nameEQ = name + "=";
1233            var ca = document.cookie.split(';');
1234
1235            for (var i = 0; i < ca.length; i++) {
1236                var c = ca[i];
1237                while (c.charAt(0) === ' ') c = c.substring(1, c.length);
1238
1239                if (c.indexOf(nameEQ) === 0) {
1240                    return c.substring(nameEQ.length, c.length);
1241                }
1242            }
1243
1244            return null;
1245        },
1246        delete: function (name) {
1247            webshop.cookies.create(name, "", -1);
1248        }
1249    },
1250    newEvent: function (eventName) {
1251        var event;
1252        if (typeof (Event) === 'function') {
1253            event = new Event(eventName);
1254        } else {
1255            event = document.createEvent('Event');
1256            event.initEvent(eventName, true, true);
1257        }
1258        return event;
1259    },
1260    search: {
1261        searchEvents: [],
1262
1263        init: function () {
1264            if ($('.ws-filter__title').is('*')) {
1265                // there are filter options on the page, lets get going.
1266                webshop.search.bindEvents();
1267            }
1268        },
1269        /**
1270         * Take the click-event from the checkboxes on search the build up the new search results.
1271         */
1272        activeFilterTabs: {},
1273        bindEvents: function () {
1274            this.searchEvents.filterSearchComplete = webshop.newEvent("filterSearchComplete");
1275
1276            $('.sidebar').on('click', '.ws-filter__title', function () {
1277                $(this).parent().toggleClass('is-active');
1278
1279                var selected = $(this).parent().hasClass('is-active');
1280                var tabID = $(this).parent().data('tab-id');
1281                var accordionCookie = webshop.cookies.read('ws_filter') || false;
1282                var accordionActive = {};
1283                if (!accordionCookie) {
1284                    accordionActive[tabID] = selected;
1285                    webshop.cookies.create('ws_filter', JSON.stringify(accordionActive));
1286                } else {
1287                    accordionActive = JSON.parse(accordionCookie);
1288                    accordionActive[tabID] = selected;
1289                    webshop.cookies.create('ws_filter', JSON.stringify(accordionActive), 365);
1290                }
1291                webshop.search.activeFilterTabs = accordionActive;
1292
1293            });
1294
1295            $('.sidebar').on('click', '.ws-filter__checkbox', function () {
1296                // ensure the starting url is set
1297                if (!webshop.search._tmpUrl) {
1298                    webshop.search.setStartingURL();
1299                }
1300
1301                $(this).closest('.ws-filter__item').addClass('is-loading');
1302
1303                if ($(this).is(':checked')) {
1304                    // add to the query
1305                    webshop.search.addFilter($(this).val());
1306                } else {
1307                    // remove the query
1308                    webshop.search.removeFilter($(this).val());
1309                }
1310
1311                $(this).replaceWith('<img src="' + shopconfig.search_loading + '" alt="Loading..." />');
1312
1313                window.history.replaceState(null, null, webshop.search._tmpUrl);
1314
1315                if (webshop.search._tmpAjaxQuery) {
1316                    // there is a query running, cancel it
1317                    webshop.search._tmpAjaxQuery.abort();
1318                }
1319
1320                // todo: run a new ajax query
1321                webshop.search._tmpAjaxQuery = $.ajax({
1322                    'url': webshop.search._tmpUrl,
1323                    'success': function (data) {
1324                        $('.sidebar').html($(data).find('.sidebar').html());
1325                        const mainH1 = $('#mainh1');
1326                        const mainContentArea = $('#main-content-area');
1327                        mainContentArea.html($(data).find('#main-content-area').html());
1328                        if (mainH1 && mainH1.is('*')) {
1329                            // Set the title (brand/category) back.
1330                            mainH1.prependTo($('#main-content-area'));
1331                        }
1332
1333                        window.history.pushState(null, null, webshop.search._tmpUrl);
1334
1335                        var accordionCookie = webshop.cookies.read('ws_filter') || false;
1336                        if (accordionCookie) {
1337                            webshop.search.activeFilterTabs = JSON.parse(accordionCookie);
1338                        }
1339                        for (var i in webshop.search.activeFilterTabs) {
1340                            if (webshop.search.activeFilterTabs[i] === false) {
1341                                continue;
1342                            }
1343
1344                            var $tab = $('div [data-tab-id="' + i + '"]');
1345                            $tab.toggleClass('is-active');
1346                        }
1347
1348                        if ($(window).width() < 768) {
1349                            var $sortingBtn = $('#sorting-change').detach();
1350                            $('.col-sorting-options').append($sortingBtn);
1351                            $('#sorting-change .dropdown-toggle').prepend($('#sorting-change__label').text());
1352                        }
1353
1354                        // Update prices if B2B
1355                        intact_products();
1356                        var observer = lozad();
1357                        observer.observe();
1358
1359                        webshop.search.searchComplete();
1360                    }
1361
1362
1363                });
1364            });
1365
1366            if (shopconfig.categoriesFullWidthLayout) {
1367                $(document).on('click', '.ws-filter__dropdown', function (e) {
1368                    e.stopPropagation();
1368 // Prevent dropdown from closing on internal click.
1369                });
1370
1371                // Ensure other click events on child elements are allowed to propagate
1372                $(document).on('click', '.search-criteria-container .ws-filter__dropdown .ws-filter__checkbox', function (e) {
1373                    // ensure the starting url is set
1374                    if (!webshop.search._tmpUrl) {
1375                        webshop.search.setStartingURL();
1376                    }
1377
1378                    $(this).closest('.ws-filter__item').addClass('is-loading');
1379
1380                    if ($(this).is(':checked')) {
1381                        // add to the query
1382                        webshop.search.addFilter($(this).val());
1383                    } else {
1384                        // remove the query
1385                        webshop.search.removeFilter($(this).val());
1386                    }
1387
1388                    $(this).replaceWith('<img src="' + shopconfig.search_loading + '" alt="Loading..." />');
1389
1390                    window.history.replaceState(null, null, webshop.search._tmpUrl);
1391
1392                    if (webshop.search._tmpAjaxQuery) {
1393                        // there is a query running, cancel it
1394                        webshop.search._tmpAjaxQuery.abort();
1395                    }
1396
1397                    // todo: run a new ajax query
1398                    webshop.search._tmpAjaxQuery = $.ajax({
1399                        'url': webshop.search._tmpUrl,
1400                        'success': function (data) {
1401                            $('#main-content-area').html($(data).find('#main-content-area').html());
1402
1403                            initListView();
1404
1405                            window.history.pushState(null, null, webshop.search._tmpUrl);
1406
1407                            if ($(window).width() < 768) {
1408                                var $sortingBtn = $('#sorting-change').detach();
1409                                $('.col-sorting-options').append($sortingBtn);
1410                                $('#sorting-change .dropdown-toggle').prepend($('#sorting-change__label').text());
1411                            }
1412
1413                            // Update prices if B2B
1414                            intact_products();
1415                            var observer = lozad();
1416                            observer.observe();
1417
1418                            webshop.search.searchComplete();
1419                        }
1420                    });
1421                });
1422            }
1423        },
1424        searchComplete: function () {
1425            document.dispatchEvent(this.searchEvents.filterSearchComplete);
1426        },
1427        // holding place for the query URL with all search alterations before it returns
1428        _tmpUrl: false,
1429        // holding for ajax query currently running search.
1430        _tmpAjaxQuery: false,
1431        addFilter: function (filter) {
1432            this._tmpUrl += '&' + filter;
1433        },
1434        removeFilter: function (filter) {
1435            var filterClean = ('&' + filter).replace(/[-[\]{}()*+?.\\^$|#\s]/g, "\\$&");
1436            var replace = new RegExp(filterClean, 'g');
1437            this._tmpUrl = this._tmpUrl.replace(replace, '');
1438        },
1439        setStartingURL: function () {
1440            this._tmpUrl = window.location.href.replace(window.location.origin, '');
1441            if (this._tmpUrl.indexOf('?') == -1) {
1442                this._tmpUrl += '?';
1443            }
1444
1445            // clean urls to have proper non-encoded brackets
1446            var findStr = new RegExp('%5B(\\d+)%5D', 'g');
1447            var findPageNum = new RegExp('[?&]page=(\\d+)', 'g');
1448            var pageNumDelimeter = this._tmpUrl.includes('?page=') ? '?' : '&';
1449
1450            this._tmpUrl = this._tmpUrl.replace(findStr, '[]').replace(/%2C/g, ',').replace(findPageNum, pageNumDelimeter + 'page=1');
1451        }
1452    },
1453    newsletter: {
1454        //Signup form, send the email and hide the area.
1455        signup: function (email, area) {
1456            var mail = $('#' + email).val();
1457            var name = $('#news-letter__input-name').val() ? $('#news-letter__input-name').val() : null;
1458            var lname = $('#news-letter__input-lname').val() ? $('#news-letter__input-lname').val() : null;
1459            $(area).hide();
1460            $(area).next().show();
1461            $.ajax({
1462                url: '/system/newsletter.html',
1463                data: { mail, name, lname }
1464            });
1465            return false;
1466        }
1467    },
1468    utils: {
1469        isTouchDevice: function () {
1470            return 'ontouchstart' in window        // works on most browsers
1471                || navigator.maxTouchPoints;
1472        },
1473        dateToYmd: function (date) {
1474            var parsedDate = (new Date(Date.parse(date)));
1475            var month = (parsedDate.getMonth() + 1).toString().length === 1 ? '0' + (parsedDate.getMonth() + 1) : (parsedDate.getMonth() + 1);
1476            var day = parsedDate.getDate().toString().length === 1 ? '0' + parsedDate.getDate() : parsedDate.getDate();
1477            return (parsedDate.getFullYear() + '-' + month + '-' + day);
1478        }
1479    },
1480    customer: {
1481        type: '', // set in footer/js/main
1482        types: { // also update Core_Table_Customer_Account
1483            b2b: 'B2B',
1484            b2c: 'B2C'
1485        }
1486    },
1487    categories: {
1488        set: [],
1489        currentCategory: false,
1490        promise: false,
1491        requestListHtml: function (currentId) {
1492            webshop.categories.promise = new Promise(function (resolve, reject) {
1493                var listingHtml = '';
1494                var title = '';
1495                for (var c in webshop.categories.set['categories']) {
1496                    var category = webshop.categories.set['categories'][c];
1497                    if (category.parentId === webshop.categories.currentCategory) {
1498                        var currentIdClass = '';
1499                        if (currentId && currentId === category.id) {
1500                            currentIdClass = 'current-category';
1501                        }
1502                        var expandHtml = (category.hasChildren ? '<td style="width: 30px;"><a class="category-expand" href="javascript:;" onclick="webshop.categories.list(\'' + category.id + '\');"><i class="fa fa-angle-right" aria-hidden="true"></i></a></td>' : '');
1503                        listingHtml += '<tr>'
1504                            + '<td><a class="category-link ' + currentIdClass + '" href="' + category.link + '">' + category.title + '</a></td>'
1505                            + expandHtml
1506                            + '</tr>';
1507
1508                    }
1509
1510                    if (category.id === webshop.categories.currentCategory) {
1511                        if (!category.hasChildren) {
1512                            webshop.categories.list(webshop.categories.findParent(webshop.categories.currentCategory), category.id);
1513                            reject();
1514                        }
1515                        title = category.title;
1516                    }
1517                }
1518                resolve({ html: listingHtml, title: title });
1519            });
1520
1521            webshop.categories.promise.then(function (obj) {
1522                document.getElementsByClassName('sidebar-categories-children')[0].innerHTML = '<tbody>' + obj.html + '</tbody>';
1523                document.getElementsByClassName('sidebar-right-title')[0].innerHTML = obj.title;
1524
1525                if (webshop.categories.set['roots'].indexOf(webshop.categories.currentCategory) !== -1) {
1526                    document.getElementById('btn-category-back').classList.add('btn-back-hidden');
1527                } else {
1528                    document.getElementById('btn-category-back').classList.remove('btn-back-hidden');
1529                }
1530
1531                document.getElementsByClassName('loading-overlay')[0].classList.add('loading-hidden');
1532            })
1533        },
1534        list: function (id, currentId) {
1535            var id = id ? id : (document.getElementsByClassName('sidebar-categories-children')[0] ? document.getElementsByClassName('sidebar-categories-children')[0].getAttribute('data-category-root') : null);
1536            if (id == null) {
1537                return;
1538            }
1539            var currentId = currentId || false;
1540            document.getElementsByClassName('sidebar-categories-children')[0].innerHTML = '<tbody>';
1541            document.getElementsByClassName('loading-overlay')[0].classList.remove('loading-hidden');
1542            webshop.categories.currentCategory = id;
1543            webshop.categories.requestListHtml(currentId);
1544        },
1545        findParent: function (id) {
1546            for (var c in webshop.categories.set['categories']) {
1547                var category = webshop.categories.set['categories'][c];
1548                if (category.id === webshop.categories.currentCategory) {
1549                    return category.parentId;
1550                }
1551            }
1552
1553            return false;
1554        },
1555        showSidebar: function () {
1556            document.getElementsByClassName('sidebar-right')[0].classList.remove('sidebar-right-hidden');
1557            document.getElementsByTagName('body')[0].style.overflow = 'hidden';
1558            webshop.categories.list();
1559
1560            var $sidebarRight = $('.sidebar-right');
1561            if ($sidebarRight.is('*')) {
1562                $sidebarRight.stop().animate({
1563                    right: 0
1564                }, 300);
1565            }
1566        },
1567        hideSidebar: function () {
1568            document.getElementsByTagName('body')[0].style.overflow = 'visible';
1569
1570            var $sidebarRight = $('.sidebar-right');
1571            if ($sidebarRight.is('*')) {
1572                $sidebarRight.stop().animate({
1573                    right: -$(window).width()
1574                }, 300, function () {
1575                    document.getElementsByClassName('sidebar-right')[0].classList.add('sidebar-right-hidden');
1576                });
1577            }
1578        }
1579    },
1580    mobile: {
1581        showSearch: function (btn, e) {
1582            var e = e || window.event;
1583            e.preventDefault();
1584            e.stopPropagation();
1585            if ($(btn).hasClass('active')) {
1586                webshop.mobile.hideSearch(btn);
1587            } else {
1588                if ($('.nav-collapse.in.collapse').is('*')) {
1589                    $('#mobile-menu').trigger('click');
1590                }
1591                mobilesearch.stop().offset({ 'left': -windowwidth }).css({ 'display': 'block' }).animate({
1592                    left: 0
1593                });
1594                mobilesearch.addClass('is-active');
1595                $(btn).addClass('active');
1596
1597                $('body').css({ 'overflow': 'hidden' });
1598            }
1599        },
1600        hideSearch: function (btn) {
1601            mobilesearch.stop().animate({
1602                left: (windowwidth * -1)
1603            }, {
1604                complete: function () {
1605                    $(btn).removeClass('is-active');
1606                    $(btn).removeClass('active');
1607                }
1608            });
1609            $('#mobile-search').removeClass('active');
1610            $('html, body').css({ 'overflow': 'visible' });
1611        }
1612    },
1613    drawerMenu: {
1614        /**
1615         * Add event listener to load new menu categories
1616         *
1617         * @param {String} menuSelector - Must be an element selector e.g. '#mobileMenu' or '.mobileMenu'
1618         * @param {Object} urlParamsObj - An object containing a key => value pair of the URL parameters we wish to send
1619         */
1620        initCategoryLoader: function (menuSelector, urlParamsObj = {}) {
1621            var categoryAjax = {
1622                counter: {}
1623            };
1624            //get top level
1625            $(`${menuSelector} .mobile-nav`).on('click', '.mobile-nav__item', function () {
1626                var categoryID = $(this).data('category-id');
1627                if (!categoryAjax.counter[categoryID]) {
1628                    if ($(this).attr('name') == 'categoryMenuParent') {
1629                        // Add default 'category' parameter
1630                        urlParamsObj['category'] = categoryID;
1631                        $urlParams = new URLSearchParams(urlParamsObj).toString();
1632
1633                        categoryAjax.counter[categoryID] = true;
1634                        $target = $(this).find('> ul');
1635                        $spinner = $('<img style="margin-top: 20%; margin-left: 48%;" src="/common/images/loading.gif"/>');
1636                        $.ajax({
1637                            'url': '/system/service/mobile/menu.html?' + $urlParams,
1638                            'dataType': 'json',
1639                            'beforeSend': function () {
1640                                $target.append($spinner);
1641                            },
1642                            'success': function (result) {
1643                                $spinner.remove();
1644                                $target.append(result.menu);
1645                            }
1646                        });
1647                    }
1648                }
1649            });
1650        },
1651        /**
1652         * Add event listener to toggle menu on and off
1653         *
1654         * @param {String} menuSelector - Must be an element selector e.g. '#mobileMenu' or '.mobileMenu'
1655         */
1656        initMenuButton: function (menuSelector) {
1657            $(`#mobile-menu, ${menuSelector} #closeMenu`).on('click', function (e) {
1658                e.preventDefault();
1659                $(this).toggleClass('is-active');
1660                $(menuSelector).toggleClass('is-active');
1661            });
1662        },
1663
1664        /**
1665         * Add event listener to toggle the menu sub-menus
1666         *
1667         * @param {String} menuSelector - Must be an element selector e.g. '#mobileMenu' or '.mobileMenu'
1668         */
1669        initSubMenuToggle: function (menuSelector) {
1670            $(`${menuSelector} .mobile-nav`).on('click', '.mobile-nav__parent', function (e) {
1671                e.preventDefault();
1672                $(this).toggleClass('active');
1673                $(this).parent().siblings().toggle();
1674                $(this).parent().find('.mobile-nav__sub-menu:first').toggle();
1675            });
1676        }
1677    }
1678};
1679
1680webshop.tooltip = {
1681    usingPopover: false,
1682    create: function (element, text, placement, trigger) {
1683        $(element).tooltip('destroy');
1684        placement = placement || 'bottom';
1685        trigger = trigger || 'manual';
1686
1687        var options = {
1688            'title': text,
1689            'placement': placement,
1690            'trigger': trigger
1691        };
1692
1693        return element.tooltip(options);
1694    },
1695    show: function (element, text, placement, trigger) {
1696        var tooltip_instance = this.create(element, text, placement, trigger);
1697        tooltip_instance.tooltip('show');
1698    },
1699    timeout: function (element, milliseconds) {
1700        setTimeout(function () {
1701            element.tooltip('hide');
1702        }, milliseconds);
1703    }
1704};
1705
1706webshop.popover = {
1707    create: function (element, text, html, placement, trigger) {
1708        $(element).popover('destroy');
1709
1710        placement = placement || 'bottom';
1711        trigger = trigger || 'manual';
1712
1713        var options = {
1714            'title': text,
1715            'content': html,
1716            'placement': placement,
1717            'trigger': trigger,
1718            'html': true,
1719        };
1720
1721        return element.popover(options);
1722    },
1723    show: function (element, text, html, placement, trigger) {
1724        var popover_instance = this.create(element, text, html, placement, trigger);
1725        popover_instance.popover('show');
1726    },
1727    timeout: function (element, milliseconds) {
1728        setTimeout(function () {
1729            element.popover('hide');
1730        }, milliseconds);
1731    }
1732};
1733
1734webshop.accessibility = {
1735    screenReaderMessage: function (message) {
1736        const el = document.createElement("div");
1737        const id = "speak-" + Date.now();
1738        el.setAttribute("id", id);
1739        el.setAttribute("aria-live", "polite");
1740        el.classList.add("visually-hidden");
1741        document.body.appendChild(el);
1742
1743        window.setTimeout(function () {
1744            document.getElementById(id).innerHTML = message;
1745        }, 100);
1746
1747        window.setTimeout(function () {
1748            document.body.removeChild(document.getElementById(id));
1749        }, 1000);
1750    },
1751}
1752
1753$(document).ready(function () {
1754    $('.rusure').on('click', function () {
1755        var confirmMessage = $(this).data('confirmmessage');
1756        var extraMessage = $(this).data('rusure');
1757
1758        if (!confirmMessage) {
1759            confirmMessage = 'Are you sure you wish to continue?'
1760        }
1761
1762        extraConfirm = true;
1763        if (extraMessage && extraMessage != '') {
1764            extraConfirm = confirm(extraMessage);
1765        }
1766
1767        return extraConfirm ? confirm(confirmMessage) : false;
1768    });
1769
1770    $('.logoutbtn').on('click', function () {
1771        return confirm(webshop._.webshop.logout.confirmation + "\n" + webshop._.webshop.logout.notification);
1772    });
1773
1774    // these must be global events as we sometimes dynamically add add to cart links ...
1775    $('body').on('keyup', '.addmultiinput', webshop.cart.generateAddToCartLink);
1776    $('body').on('change', '.addmultiinput', webshop.cart.generateAddToCartLink);
1777    $('.addtocart-ref').on('keyup', webshop.cart.generateAddToCartLink);
1778    $('.addtocart-ref').on('change', webshop.cart.generateAddToCartLink);
1779    $('input[name="batchMatch"]').on('change', webshop.cart.generateAddToCartLink);
1780
1781    if (webshop.utils.isTouchDevice()) {
1782        $('.js-force-hover').one('click', function (event) {
1783            event.preventDefault();
1784        });
1785    }
1786
1787    var classes = {
1788        grid: 'product-grid products--grid',
1789        list: 'product-list products--list',
1790        detailed: 'product-detailed products--detailed'
1791    };
1792
1793    if ($('#products-results').is('*')) {
1794        //a.btn-inverse i.icon-white
1795        $(document).on('click', '.btn-group.view a', function () {
1796            $('.btn-group.view a.btn-inverse').removeClass('btn-inverse');
1797            $('.btn-group.view a i.icon-white').removeClass('icon-white');
1798            $(this).addClass('btn-inverse');
1799            $(this).find('i').addClass('icon-white');
1800
1801            var setclass;
1802            var setcookie;
1803            if ($(this).attr('title') == 'Grid view') {
1804                setclass = 'grid';
1805                setcookie = 'product-grid';
1806            }
1807            if ($(this).attr('title') == 'Detailed view') {
1808                setclass = 'detailed';
1809                setcookie = 'product-detailed';
1810            }
1811            if ($(this).attr('title') == 'List view') {
1812                setclass = 'list';
1813                setcookie = 'product-detailed';
1814            }
1815
1816            $('#products-results').removeClass('product-list products--list');
1817            $('#products-results').removeClass('product-detailed products--detailed');
1818            $('#products-results').removeClass('product-grid products--grid');
1819            $('#products-results').addClass(classes[setclass]);
1820
1821            if ($('#products-results-sku').is('*')) {
1822                $('#products-results-sku').removeClass('product-list products--list');
1823                $('#products-results-sku').removeClass('product-detailed products--detailed');
1824                $('#products-results-sku').removeClass('product-grid products--grid');
1825                $('#products-results').addClass(classes[setclass]);
1826            }
1827            if ($('#products-results-name').is('*')) {
1828                $('#products-results-name').removeClass('product-list products--list');
1829                $('#products-results-name').removeClass('product-detailed products--detailed');
1830                $('#products-results-name').removeClass('product-grid products--grid');
1831                $('#products-results').addClass(classes[setclass]);
1832            }
1833
1834            $.cookie('product_grid', setclass);
1835
1836            return false;
1837        });
1838
1839        if ($.cookie('product_grid')) {
1840            $('a.product-' + $.cookie('product_grid')).trigger('click');
1841        } else if (!$.cookie('product_grid') && !$('#products-results').is('[class^=product]')) {
1842            $('#products-results').addClass(shopconfig.defaultlistview);
1843        }
1844    }
1845
1846    $('.b2c-menu > li').on({
1847        mouseenter: function () {
1848            $(this).addClass('active');
1849        },
1850        mouseleave: function () {
1851            $(this).removeClass('active');
1852        }
1853    });
1854
1855    $('body').on('click', '.addajax', webshop.cart.ajaxAddToCartEvents.onclick);
1856
1857    var filters = $('.ws-filter');
1858    if ((filters.length) == 1) {
1859        $(filters[0]).addClass('is-active');
1860    }
1861
1862    // disable any buttons which should not be pressed more than once
1863    $('body').on('submit', 'form', function () {
1864        $(this).find('.btn-disable-onclick').attr('disabled', true).addClass('disabled');
1865        return true;
1866    });
1867
1868    var id = document.getElementsByClassName('sidebar-categories-children')[0] ? document.getElementsByClassName('sidebar-categories-children')[0].getAttribute('data-category-root') : null;
1869    if (id != null) {
1870        $.get('/system/categories.html', {}, function (data) {
1871            webshop.categories.set = JSON.parse(data);
1872            webshop.categories.list();
1873        });
1874    }
1875
1876
1877    if (document.getElementsByClassName('delivery-due-date').length) {
1878        $('.delivery-due-date').each(function () {
1879            this.addEventListener('change', webshop.delivery.onDeliveryDueDateChange);
1880        });
1881
1882        if (document.querySelector('input[name="delivery-due-date"]:checked').value === 'custom_due_date') {
1883            $('.delivery-due-date-input').removeClass('delivery-due-date-input-hidden');
1884        }
1885
1886        var calendarOptions = {
1887            field: document.getElementsByClassName('delivery-due-date-input')[0],
1888            firstDay: 1,
1889            disableWeekends: false,
1890            minDate: new Date(),
1891            maxDate: new Date(2020, 12, 31),
1892            yearRange: [2000, 2020],
1893            disableDayFn: function (date) {
1894                var parsedDate = (new Date(Date.parse(date)));
1895                var month = (parsedDate.getMonth() + 1).toString().length === 1 ? '0' + (parsedDate.getMonth() + 1) : (parsedDate.getMonth() + 1);
1896                var day = parsedDate.getDate().toString().length === 1 ? '0' + parsedDate.getDate() : parsedDate.getDate();
1897                var formattedDate = parsedDate.getFullYear() + '-' + month + '-' + day;
1898                var formattedDateAbstract = 'YYYY' + '-' + month + '-' + day;
1899
1900                var weekdays = [
1901                    'Sunday',
1902                    'Monday',
1903                    'Tuesday',
1904                    'Wednesday',
1905                    'Thursday',
1906                    'Friday',
1907                    'Saturday'
1908                ];
1909                var dayString = weekdays[date.getDay()];
1910                var predefinedCheck = (webshop.delivery.predefinedDeliveryDays.indexOf(dayString) === -1);
1911
1912                return (
1913                    predefinedCheck ||
1914                    webshop.delivery.excludedDueDates.indexOf(formattedDate) !== -1) ||
1915                    (webshop.delivery.excludedDueDates.indexOf(formattedDateAbstract) !== -1
1916                    );
1917            }
1918        };
1919
1920        var picker = new Pikaday(Object.assign(calendarOptions, webshop.delivery.calendarOptionsExtended));
1921    }
1922
1923    webshop.search.init();
1924});
1925
1926function refreshCart() {
1927    $.ajax({
1928        'url': '/checkout.html?refreshCart=true&route=ajax',
1929        'dataType': 'json',
1930        'success': function (data) {
1931            $('.gross-total').html(data.formatted.gross);
1932            $('.items-total').html(data.count + ' Items');
1933        }
1934    });
1935}
1936
1937$(document).on('change', '.btn-file :file', function () {
1938    var input = $(this),
1939        numFiles = input.get(0).files ? input.get(0).files.length : 1,
1940        label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
1941    input.trigger('fileselect', [numFiles, label]);
1942});
1943
1944$(document).ready(function () {
1945    $('.btn-file :file').on('fileselect', function (event, numFiles, label) {
1946        $('#filename').html(': ' + label);
1947    });
1948
1949    var accordionCookie = webshop.cookies.read('ws_filter') || false;
1950    if (accordionCookie) {
1951        webshop.search.activeFilterTabs = JSON.parse(accordionCookie);
1952    }
1953
1954    for (var i in webshop.search.activeFilterTabs) {
1955        var $tab = $('div [data-tab-id="' + i + '"]');
1956
1957        if (webshop.search.activeFilterTabs[i] === false) {
1958            $tab.removeClass('is-active');
1959        } else {
1960            $tab.addClass('is-active');
1961        }
1962    }
1963});
1964
1965function brokenImg(el) {
1966    $.post('/system/s3notify.html', { imgURL: $(el).attr('src') });
1967}
1968
1969$(function () {
1970    var isIE = window.ActiveXObject || "ActiveXObject" in window;
1971    if (isIE) {
1972        $('.modal').removeClass('fade');
1973    }
1974});
1975
1976
1977if (typeof Object.assign != 'function') {
1978    // Must be writable: true, enumerable: false, configurable: true
1979    Object.defineProperty(Object, "assign", {
1980        value: function assign(target, varArgs) { // .length of function is 2
1981            'use strict';
1982            if (target == null) { // TypeError if undefined or null
1983                throw new TypeError('Cannot convert undefined or null to object');
1984            }
1985
1986            var to = Object(target);
1987
1988            for (var index = 1; index < arguments.length; index++) {
1989                var nextSource = arguments[index];
1990
1991                if (nextSource != null) { // Skip over if undefined or null
1992                    for (var nextKey in nextSource) {
1993                        // Avoid bugs when hasOwnProperty is shadowed
1994                        if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
1995                            to[nextKey] = nextSource[nextKey];
1996                        }
1997                    }
1998                }
1999            }
2000            return to;
2001        },
2002        writable: true,
2003        configurable: true
2004    });
2005}
2006
2007
2008
2009$(document).ready(function () {
2010    webshopLoaded();
2011
2012    // will remember the user using a cookie in the browser to keep the category scroll position int the sidebar for each category view page
2013    if (window.shopconfig && window.shopconfig.category_scroll_sticky) {
2014
2015        var scrollCategoryArea = ($.cookie('scrollAble') || '{}');
2016        scrollCategoryArea = JSON.parse(scrollCategoryArea);
2017
2018        $('.search-filter-box ul').on('scroll', function (e) {
2019            var scrollTopValue = $(this).scrollTop();
2020            var classes = $(this).parent().attr('class').split(/\s+/);
2021            var foundClass = false;
2022
2023            for (var i = 0; i < classes.length; i++) {
2024                foundClass = classes[i].match(/search-base-category-(\d+)/);
2025                if (foundClass != null) {
2026                    scrollCategoryArea[classes[i]] = scrollTopValue;
2027                    $.cookie('scrollAble', JSON.stringify(scrollCategoryArea), { expires: 7, path: '/' });
2028                    break;
2029                }
2030            }
2031        });
2032
2033        for (var i in scrollCategoryArea) {
2034            $('.' + i + ' ul').scrollTop(scrollCategoryArea[i]);
2035        }
2036    }
2037});

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.