PageSourceSearch

https://janngredig.ch/wp-content/plugins/nextgen-gallery-pro/static/Commerce/Cart.js?ver=4.5.1

js janngredig.ch collected 2026-10-02 01:21:14 UTC 84,954 bytes, 1,998 lines download raw bytes

1// This is a polyfill for IE11 and it's lack of Object.assign
2// Credit goes to MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
3if (typeof Object.assign !== 'function') {
4    // Must be writable: true, enumerable: false, configurable: true
5    Object.defineProperty(Object, "assign", {
6        value: function assign(target, varArgs) { // .length of function is 2
7            'use strict';
8            if (target === null || target === undefined) {
9                throw new TypeError('Cannot convert undefined or null to object');
10            }
11
12            var to = Object(target);
13
14            for (var index = 1; index < arguments.length; index++) {
15                var nextSource = arguments[index];
16
17                if (nextSource !== null && nextSource !== undefined) {
18                    for (var nextKey in nextSource) {
19                        // Avoid bugs when hasOwnProperty is shadowed
20                        if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
21                            to[nextKey] = nextSource[nextKey];
22                        }
23                    }
24                }
25            }
26            return to;
27        },
28        writable: true,
29        configurable: true
30    });
31}
32
33(function($) {
34
35    // Namespace and utility functions
36    Ngg_Pro_Cart = {
37        Models: {},
38        Views: {}
39    };
40
41    // Define image model
42    Ngg_Pro_Cart.Models.Image = Backbone.Model.extend({
43        idAttribute: 'pid',
44
45        subtotal: function() {
46            var retval = 0.0;
47            this.get('items').each(function(item){
48                retval += item.subtotal();
49            });
50            return retval;
51        },
52
53        get_full_exif_dimensions: function() {
54            return this.get('crop_dimensions');
55        }
56    });
57
58    // Define pricelist item model
59    Ngg_Pro_Cart.Models.PricelistItem = Backbone.Model.extend({
60        idAttribute: 'ID',
61
62        defaults: {
63            quantity: 0,
64            shippable_to: []
65        },
66
67        subtotal: function() {
68            return parseFloat(this.get('price')) * parseInt(this.get('quantity'));
69        }
70    });
71
72    // Define Image Collection
73    Ngg_Pro_Cart.Models.ImageCollection = Backbone.Collection.extend({
74        model: Ngg_Pro_Cart.Models.Image,
75
76        subtotal: function() {
77            var retval = 0.0;
78            this.each(function(image){
79                retval += image.subtotal();
80            });
81            return retval;
82        }
83    });
84
85    // Define Pricelist Item Collection
86    Ngg_Pro_Cart.Models.PricelistItemCollection = Backbone.Collection.extend({
87        model: Ngg_Pro_Cart.Models.PricelistItem
88    });
89
90
91    // Define Cart Model
92    Ngg_Pro_Cart.Models.Cart = Ngg_Pro_Cart.Models.ImageCollection.extend({
93        shipping: 0.0,
94        total: 0.0,
95        sub_total: 0.0,
96        undiscounted_subtotal: 0.0,
97        discount_given: 0.0,
98        tax: 0.0,
99        settings: {
100            shipping_address: {
101                name: '',
102                street_address: '',
103                address_line: '',
104                country: '',
105                state: '',
106                zip: '',
107            },
108            studio_address: {
109                name: '',
110                street_address: '',
111                address_line: '',
112                country: '',
113                state: '',
114                zip: '',
115            }
116        },
117        coupon: '',
118        has_shippable_items: false,
119        shipping_methods: [], /* should probably be an empty object */
120
121        empty_cart: function() {
122            this.reset();
123            this.storage.set('ngg_pro_cart', this.getFullCartData());
124            this.crop_storage.reset();
125            return this;
126        },
127
128        initialize: function() {
129            this.ready               = false;
130            this.storage             = this.get_storage();
131            this.crop_storage        = this.get_storage('nextgen-gallery-crop');
132
133            this.triggerReady        = this.triggerReady.bind(this);
134            this._updateCartItems    = this._updateCartItems.bind(this);
135            this._updateCartSettings = this._updateCartSettings.bind(this);
136            this.save                = this.save.bind(this);
137            this.loadData            = this.loadData.bind(this);
138
139            let delay = (timeout) => new Promise((resolve, reject) => {
140                setTimeout(resolve, timeout);
141            });
142
143            // We need to delay to give a chance for views to register there event handlers
144            delay(100)
145                .then(this.loadData)
146                .then(this.triggerReady);
147        },
148
149        triggerReady: function() {
150            this.ready = true;
151            this.trigger('ready');
152            this.trigger('cartReady');
153        },
154
155        getMinimalCartData: function() {
156            var cartData = this.storage.get('ngg_pro_cart');
157            if (_.isObject(cartData)) {
158                return cartData;
159            } else if (_.isString(cartData)) {
160                // A cookie corrupted by a js-cookie version conflict ("[object Object]") must not
161                // throw here, or the checkout page renders permanently empty.
162                try {
163                    return JSON.parse(cartData);
164                } catch (e) {
165                    return null;
166                }
167            } else {
168                return null
169            }
170        },
171
172        getFullCartData: function() {
173            var cart = {
174                image_ids: [],
175                images: {},
176                coupon: this.coupon
177            };
178
179            this.each(function(image_obj) {
180                cart.image_ids.push(image_obj.id);
181                var stored_image = {
182                    item_ids: [],
183                    items: {}
184                };
185                image_obj.get('items').each(function(item) {
186                    stored_image.item_ids.push(item.id);
187                    stored_image.items[item.id] = {quantity: item.get('quantity')};
188                });
189
190                cart.images[image_obj.id] = stored_image;
191            });
192
193            return cart;
194        },
195
196        getCartData : function() {
197            return !this.ready ? this.getMinimalCartData() : this.getFullCartData();
198        },
199
200        requestCartData: async function() {
201            const request = {
202                cart: this.getCartData(),
203                settings: this.settings
204            };
205
206            const response = await fetch(
207                window.photocrati_ajax.rest_url + 'nggpro/v1/cart/items',
208                {
209                    body: JSON.stringify(request),
210                    cache: 'no-cache',
211                    headers: { 'Content-Type': 'application/json' },
212                    method: 'POST'
213                }
214            );
215
216            return response.json();
217        },
218
219        getImageFromCart: function(imageId) {
220            var imageModel = this.get(imageId);
221            if (!imageModel) {
222                imageModel = this.findWhere({id: imageId})
223            }
224            if (!imageModel) {
225                imageModel = new Ngg_Pro_Cart.Models.Image({pid: imageId, items: new Ngg_Pro_Cart.Models.PricelistItemCollection()});
226            }
227            return imageModel;
228        },
229
230        _updateCartSettings: function(response) {
231            this.settings                       = response.settings;
232            this.currency                       = response.currency_code || 'USD';
233            this.has_shippable_items            = response.has_shippable_items;
234            this.shipping_methods               = response.shipping_methods;
235            this.shipping                       = parseFloat(response.shipping);
236            this.sub_total                      = parseFloat(response.subtotal);
237            this.total                          = parseFloat(response.total);
238            this.tax                            = parseFloat(response.tax);
239            this.undiscounted_subtotal          = parseFloat(response.undiscounted_subtotal);
240
241            if (_.isObject(response.coupon)) {
242                this.coupon                     = response.coupon.code;
243                this.discount_given             = parseFloat(response.coupon.discount_given);
244            } else {
245                this.coupon = '';
246                this.discount_given = 0;
247            }
248
249            return this;
250        },
251
252        _updateCartItems: function(response) {
253            // Add the images to the cart
254            for (var image_index=0; image_index<response.image_ids.length; image_index++) {
255                var imageId                 = parseInt(response.image_ids[image_index]);
256                var updatedImageProperties  = response.images[imageId];
257                var imageModel              = this.getImageFromCart(imageId);
258
259                for (var item_index=0; item_index<updatedImageProperties.item_ids.length; item_index++) {
260                    var itemId                          = parseInt(updatedImageProperties.item_ids[item_index]);
261                    if (itemId > 0) {
262                        var updatedItemProperties       = updatedImageProperties.items[itemId];
263                        updatedItemProperties.item_id   = itemId;
264                        updatedItemProperties.image_id  = imageId;
265                        itemModel                       = imageModel.get('items').get(itemId);
266                        if (!itemModel) {
267                            itemModel = new Ngg_Pro_Cart.Models.PricelistItem(updatedItemProperties)
268                        } else {
269                            itemModel.set(updatedItemProperties);
270                        }
271                        imageModel.get('items').add(itemModel, {merge: true});
272                    }
273                }
274                delete updatedImageProperties.item_ids;
275                delete updatedImageProperties.items;
276                imageModel.set(updatedImageProperties);
277                this.add(imageModel, {merge: true});
278            }
279
280            return this;
281        },
282
283        // We call loadData() when we need something that we're missing.
284        // loadData() can be called with a minimal representation of a cart, and in turn, have the server
285        // return a fully populated cart. Or we can send a full representation of the cart, have the server
286        // do some computations on that data, and return an updated representation. We might do that to get
287        // updated shipping methods, taxes, shipping costs, etc.
288        loadData: function() {
289            this.trigger('beforeLoadData');
290
291            var cart = this;
292            var $overlay = $("#ngg_pro_cart_subitems_overlay");
293
294            return this.requestCartData()
295                .then(function(response) {
296                    cart._updateCartItems(response);
297                    cart._updateCartSettings(response);
298                    return response;
299                })
300                .then(function(response) {
301                    cart.save();
302                    return response;
303                })
304                .then(function(response) {
305                    cart.trigger('dataLoaded', response);
306                })
307                .catch(function(err) {
308                    console.log(err);
309                })
310                .finally(function() {
311                    $overlay.removeClass('ngg_pro_cart_subitems_overlay_open');
312                });
313        },
314
315        get_storage: function(storage_id) {
316            if (typeof(storage_id) === "undefined" || storage_id == null)
317                storage_id = 'nextgen-gallery-cart';
318
319            // js-cookie is loaded from an external CDN; if that request is blocked, offline, or
320            // beaten by consent tooling, window.Cookies is undefined and dereferencing it throws,
321            // blanking the cart (see #223). Fall through to the Basil-backed storage below when it
322            // is missing so the cart degrades gracefully instead of breaking.
323            if (Ngg_Pro_Cart_Settings.use_cookies == "true" && typeof Cookies !== 'undefined') {
324                var cookie_name = function(key) { return storage_id + '.' + key; };
325                return {
326                    // Another plugin (e.g. WooCommerce) can register the shared 'js-cookie' handle
327                    // first with js-cookie v3, which stores objects as "[object Object]" instead of
328                    // JSON like the bundled v2 does. The namespaced get/set below serialize and
329                    // parse explicitly, so the cart survives whichever version owns window.Cookies.
330                    get: function(key) {
331                        var raw = Cookies.get(cookie_name(key));
332                        if (typeof raw === 'undefined' || raw === null) {
333                            // Migrate a value saved by a pre-namespace release under the flat key.
334                            var legacy = Cookies.get(key);
335                            if (typeof legacy === 'undefined' || legacy === null) return null;
336                            var migrated;
337                            try {
338                                migrated = JSON.parse(legacy);
339                            } catch (e) {
340                                // Corrupt object write is unrecoverable; a plain legacy string (e.g. crop offsets) is kept as-is.
341                                if (legacy === '[object Object]') { Cookies.remove(key, { path: '/' }); return null; }
342                                migrated = legacy;
343                            }
344                            Cookies.set(cookie_name(key), JSON.stringify(migrated), { expires: 10, path: '/' });
345                            Cookies.remove(key, { path: '/' });
346                            return migrated;
347                        }
348                        if (typeof raw !== 'string') return raw;
349                        // Drop values not written by us (e.g. a js-cookie 3.x global that skipped serialization).
350                        try { return JSON.parse(raw); } catch (e) { Cookies.remove(cookie_name(key), { path: '/' }); return null; }
351                    },
352                    set: function(key, value) { return Cookies.set(cookie_name(key), JSON.stringify(value), { expires: 10, path: '/' }); },
353                    del: function(key) { Cookies.remove(cookie_name(key), { path: '/' }); return !this.has(key); },
354                    has: function(key) { var value = this.get(key); return typeof(value) != 'undefined' && value != null; },
355                    save: function() { return true; },
356                    reset: function() {
357                        Object.keys(Cookies.get())
358                            .filter(function(name) { return name.indexOf(storage_id + '.') === 0; })
359                            .forEach(function(name) { Cookies.remove(name, { path: '/' }); });
360                    }
361                };
362            } else {
363                var cart_storage_options = {
364                    namespace: storage_id,
365                    storages: ['local', 'cookie'],
366                    storage: 'local',
367                    expiresDays: 10,
368                    secure: false
369                };
370                var storage = new window.Basil(cart_storage_options);
371                return {
372                    get: function(key) { return storage.get(key); },
373                    set: function(key, value) { return storage.set(key, value); },
374                    del: function(key) { storage.remove(key); return !this.has(key); },
375                    has: function(key) { var value = this.get(key); return typeof(value) != 'undefined' && value != null; },
376                    save: function() { return true; },
377                    reset: function() { storage.reset(); }
378                };
379            }
380        },
381
382        getFormattedCurrency: function(field) {
383            if (field == 'subtotal') {
384                field = 'sub_total';
385            }
386
387            var val = this[field];
388
389            // _.isNumber() returns true for NaN
390            if (!_.isNumber(val) || _.isNaN(val)) {
391                val = 0.0;
392            }
393
394            return sprintf(Ngg_Pro_Cart_Settings.currency_format, val);
395        },
396
397        /**
398         * Saves the representation of the cart in the local browser storage
399         */
400        save: function() {
401            this.storage.set('ngg_pro_cart', this.getCartData());
402            return this;
403        },
404
405        updateQuantity: function(image_id, item, options) {
406            if (!_.isObject(options))        options = {};
407            if (!_.has(options, 'loadData')) options.loadData = true;
408
409            var remove_item = (parseInt(item.get('quantity')) > 0) ? false : true;
410            var imageModel  = this.getImageFromCart(image_id);
411
412            var cart_items = imageModel.get('items');
413            var cart_item  = cart_items.get(item.id);
414            if (cart_item) {
415                if (remove_item) {
416                    cart_items.remove(cart_item);
417                } else {
418                    cart_item.set(item.attributes);
419                }
420            } else if (!remove_item) {
421                cart_items.add(item.attributes);
422                /* GA4 add_to_cart: NextGEN tracking script listens for ngg_ga4_add_to_cart */
423                var currency = (typeof this.currency === 'string' && this.currency) ? this.currency : 'USD';
424                var price = parseFloat(item.get('price')) || 0;
425                var qty = parseInt(item.get('quantity'), 10) || 1;
426                var value = price * qty;
427                var itemName = item.get('title') || imageModel.get('filename') || imageModel.get('alttext') || '';
428                try {
429                    document.dispatchEvent(new CustomEvent('ngg_ga4_add_to_cart', {
430                        detail: {
431                            currency: currency,
432                            value: value,
433                            items: [{ item_id: String(image_id), item_name: itemName, price: price, quantity: qty }]
434                        }
435                    }));
436                } catch (e) {}
437            }
438
439            if (!remove_item) {
440                this.add(imageModel, {merge: true});
441            }
442
443            if (cart_items.length == 0) {
444                this.remove(imageModel);
445            }
446
447            this.save();
448
449            if (options.loadData) {
450                this.loadData();
451            }
452        },
453
454        item_count: function() {
455            var retval = 0;
456            this.each(function(image){
457                retval += image.get('items').length
458            });
459            return retval;
460        }
461    });
462
463    Ngg_Pro_Cart.get_instance = function() {
464        if (typeof(Ngg_Pro_Cart['instance']) == 'undefined') {
465            Ngg_Pro_Cart.instance = new Ngg_Pro_Cart.Models.Cart();
466        }
467        return Ngg_Pro_Cart.instance;
468    };
469
470    // Define a type of view that has template capabilities
471    Ngg_Pro_Cart.Views.TemplateView = Backbone.View.extend({
472        render_template: function(params){
473            var template = $('#' + this.template).html();
474            for (var key in this.model.attributes) {
475                if (typeof(key) == 'string') {
476                    var value = this.model.get(key);
477                    var placeholder = '{' + this.object_name + '.' + key + '}';
478                    while (template.indexOf(placeholder) >= 0) {
479                        template = template.replace(placeholder, value);
480                    }
481                }
482            }
483            if (typeof(params) != 'undefined') {
484                for (var key in params) {
485                    var placeholder = '{' + key + '}';
486                    while (template.indexOf(placeholder) >= 0) {
487                        template = template.replace(placeholder, params[key]);
488                    }
489                }
490            }
491
492            this.$el.html(template);
493        }
494    });
495
496    // Define the Pricelist Item Row View
497    Ngg_Pro_Cart.Views.Item_Row = Ngg_Pro_Cart.Views.TemplateView.extend({
498        tagName: 'tr',
499        className: 'ngg_pro_cart_image_item',
500        template: 'ngg_pro_cart_item_tmpl',
501        object_name: 'item',
502
503        initialize: function(params){
504            this.image_id = params.image_id;
505            this.model.on('change:quantity', this.update_subtotal, this);
506            this.image = Ngg_Pro_Cart.get_instance().get(this.image_id);
507        },
508
509        is_lab_item: function() {
510            var retval = false;
511            var source = this.model.get('source');
512            if (Ngg_Pro_Cart_Settings.sources.hasOwnProperty(source)) {
513                retval = Ngg_Pro_Cart_Settings.sources[source];
514            }
515            return retval;
516        },
517
518        update_subtotal: function() {
519            this.$el.find('.subtotal_column span').html(sprintf(Ngg_Pro_Cart_Settings.currency_format, this.model.subtotal()));
520        },
521
522        render: function() {
523            this.render_template({
524                'item.id' : this.model.id,
525                'image.filename': this.image.get('filename'),
526                'image.thumbnail_url': this.image.get('thumbnail_url'),
527                'image.full_url': this.image.get('full_url'),
528                'image.width': this.image.get('width'),
529                'image.height': this.image.get('height'),
530                'image.alttext': this.image.get('alttext')
531            });
532
533            // Update price
534            this.$el.find('.price_column').html(sprintf(Ngg_Pro_Cart_Settings.currency_format, this.model.get('price')));
535
536            // Update subtotal
537            this.update_subtotal();
538
539            var updateCropPreview = function (item) {
540                var crop_offset = item.model.get('crop_offset');
541                var cropPoints = crop_offset ? crop_offset.split(',').map(function (str) { return str.trim(); }) : [];
542
543                if (cropPoints.length < 4)
544                    return;
545
546                var sourceData = item.model.get('source_data');
547                var printRatio = sourceData['lab_properties']['aspect']['ratio'];
548                var imgDims = item.image.get_full_exif_dimensions();
549                var $thumbCont = item.$el.find('.thumbnail_column .thumbnail-container');
550                var $thumb = $thumbCont.find('img');
551                var thumbWidth = $thumb.width();
552                var thumbHeight = $thumb.height();
553                var $crop_preview = $thumbCont.find('.crop-preview');
554
555                if ($crop_preview.length == 0)
556                    $crop_preview = $('<div class="crop-preview" />').appendTo($thumbCont);
557
558                var ratioX = thumbWidth / imgDims.width;
559                var ratioY = thumbHeight / imgDims.height;
560                var left = cropPoints[0] * ratioX;
561                var top = cropPoints[1] * ratioY;
562                var width = Math.ceil(((cropPoints[2] - cropPoints[0]) * ratioX));
563                var height = Math.ceil(((cropPoints[3] - cropPoints[1]) * ratioY));
564
565                var borderWidth = parseInt($crop_preview.css("border-left-width"), 10);
566
567                width = width - (borderWidth * 2);
568                height = height - (borderWidth * 2);
569
570                $crop_preview.css({
571                    'left': left,
572                    'top': top
573                });
574                $crop_preview.width(width);
575                $crop_preview.height(height);
576            };
577
578            var _this = this;
579            var $cropBtn = this.$el.find('.ngg-edit-crop');
580            if (this.is_lab_item()) {
581                var imgDims = this.image.get('crop_dimensions');
582                var sourceData = this.model.get('source_data');
583                var printRatio = parseFloat(sourceData['lab_properties']['aspect']['ratio']);
584                var imgRatio = imgDims.width / imgDims.height;
585                var $thumbCol = this.$el.find('.thumbnail_column');
586                var $cropBtn = this.$el.find('.ngg-edit-crop');
587                var $crop_offset = this.$el.find('.thumbnail_column :input[name$="[crop_offset]"]');
588                var printW = sourceData['lab_properties']['W'];
589                var printH = sourceData['lab_properties']['H'];
590                var printSuffix = printW.toString() + 'w_' + printH.toString() + 'h';
591                var storeKey = 'image_crop_' + this.image_id.toString() + '_' + printSuffix;
592                var crop_offset = Ngg_Pro_Cart.get_instance().crop_storage.get(storeKey);
593                if (crop_offset != null && crop_offset != '') {
594                    this.model.set('crop_offset', crop_offset);
595                }
596                var $img = $thumbCol.find('img');
597                $img.width(imgDims.width);
598                $img.height(imgDims.height);
599                var imgW = $img.width();
600                var imgH = $img.height();
601                $cropBtn.data('imageSrc',this.image.get('crop_url'));
602                $cropBtn.data('printRatio', printRatio);
603                $cropBtn.data('storeKey', storeKey);
604                $cropBtn.data('imgRatio', imgRatio);
605                $cropBtn.data('cropZoom', imgW / this.image.get_full_exif_dimensions().width);
606                $cropBtn.data('cropInput', $crop_offset.attr('name'));
607
608                var item = this;
609                $cropBtn.on('click', function (e) {
610                    e.preventDefault();
611                    var jthis = $(this);
612                    var src = jthis.data('imageSrc');
613                    var printRatio = jthis.data('printRatio');
614                    var storeKey = jthis.data('storeKey');
615                    var imgRatio = jthis.data('imgRatio');
616                    var cropZoom = jthis.data('cropZoom');
617                    var crop_offset = item.model.get('crop_offset');
618                    var cropPoints = crop_offset ? crop_offset.split(',').map(function (str) { return str.trim(); }) : [];
619                    cropPoints = cropPoints.map(function (pt) { return pt * cropZoom; });
620                    var cropInput = jthis.data('cropInput');
621                    var $cropUI = $('#ngg_crop_ui').clone().detach();
622                    $cropUI.width(1000);
623                    $cropUI.height(600);
624                    $cropUI.show();
625
626                    var resizeCropBox = function ($content) {
627                        var w = 1000;
628                        var h = 600;
629                        var borderW = 40;
630                        var borderH = 80;
631                        var browserW = $(window).width() - borderW;
632                        var browserH = $(window).height() - borderH;
633                        var maxW = browserW - Math.min(browserW * 0.018, 25);
634                        var maxH = browserH - Math.min(browserH * 0.016, 20);
635                        if (w > maxW) {
636                            w = maxW;
637                        }
638                        if (h > maxH) {
639                            h = maxH;
640                        }
641                        $content.width(w);
642                        $content.height(h);
643                    };
644
645                    var applyCroppie = function ($canvas) {
646                        // It is necessary to provide some buffer or the edges of the boundary will not appear
647                        // on browsers with a small display size
648                        var canvasBuffer = 100;
649                        var canvasWidth  = $canvas.width()  - canvasBuffer;
650                        var canvasHeight = $canvas.height() - canvasBuffer;
651
652                        // Limit our dimensions to the visible region while maintaining crop & aspect ratios
653                        var ratioX = canvasWidth / imgW;
654                        var ratioY = canvasHeight / imgH;
655                        var baseRatio  = Math.min(ratioX, ratioY);
656                        var cropWidth  = imgH * baseRatio;
657                        var cropHeight = imgH * baseRatio;
658
659                        var cropRatio = cropWidth / cropHeight;
660
661                        if ((printRatio < 1 && imgRatio > 1) || (printRatio > 1 && imgRatio < 1)) {
662                            printRatio = 1 / printRatio;
663                        }
664
665                        var ratioDiff = cropRatio - printRatio;
666
667                        if (ratioDiff > 1) {
668                            cropHeight = cropWidth / printRatio;
669                        } else {
670                            cropWidth = cropHeight * printRatio;
671                        }
672
673                        // prevent recursion in croppieArgs.update
674                        var counter = 0;
675
676                        var croppieArgs = {
677                            viewport: {
678                                type: 'square',
679                                width: cropWidth,
680                                height: cropHeight
681                            },
682                            enableZoom: true,
683                            mouseWheelZoom: false,
684                            showZoomer: false,
685                            enableOrientation: true,
686                            update: function (croppie) {
687                                // we must force the zoom to remain all the way so that images aren't distorted
688                                counter++;
689                                if (counter === 2) {
690                                    $canvas.croppie('setZoom', '0');
691                                }
692
693                                var cropPoints = croppie.points;
694                                cropPoints = cropPoints.map(function (pt) {
695                                    return pt / cropZoom;
696                                });
697                                var crop_offset = cropPoints.join(',');
698                                _this.model.set('crop_offset', crop_offset);
699                                var $cropInput = $(':input[name="' + cropInput + '"]');
700                                $cropInput.val(crop_offset);
701                                Ngg_Pro_Cart.get_instance().crop_storage.set(storeKey, crop_offset);
702                            }
703                        };
704
705                        var bindArgs = {
706                            url: src
707                        };
708
709                        if (cropPoints.length == 4) {
710                            bindArgs.points = cropPoints;
711                        }
712
713                        $canvas.empty();
714                        // this generates an error deep in croppie.js if we just call $canvas.croppie('destroy')
715                        try {
716                            $('#ngg_crop_ui .crop-canvas').croppie('destroy');
717                        }
718                        catch (err) {
719                            // It will fail if we try to remove an instance of croppie if one doesn't already exist
720                        }
721                        $canvas.croppie(croppieArgs);
722                        $canvas.croppie('bind', bindArgs); //.then(function() {
723                    };
724
725                    $(window).on('resize orientationchange onfullscreenchange onmozfullscreenchange onwebkitfullscreenchange', function() {
726                        var $content = $('.featherlight').find('#ngg_crop_ui');
727                        resizeCropBox($content);
728
729                        var $canvas = $content.find('.crop-canvas');
730                        if ($canvas.length > 0) {
731                            applyCroppie($canvas);
732                        }
733                    });
734
735                    resizeCropBox($cropUI);
736                    $cropUI.show();
737
738                    $.featherlight($cropUI, {
739                        closeOnClick: false,
740                        otherClose: '.crop-buttons .crop-button-close',
741
742                        afterContent: function (e) {
743                            var $canvas = this.$content.find('.crop-canvas');
744                            applyCroppie($canvas);
745                        },
746
747                        afterClose: function (e) {
748                            var image = Ngg_Pro_Cart.get_instance().get(_this.image_id);
749                            var cart_items = image.get('items');
750                            var cart_item  = cart_items.get(_this.model.id);
751                            if (cart_item) {
752                                cart_item.set(_this.model.attributes);
753                            }
754                            updateCropPreview(_this);
755                        },
756
757                        onResize: function (e) {
758                        }
759                    });
760
761                    return false;
762                });
763
764                this.$el.find('.thumbnail_column .thumbnail-container img').on('load', function () {
765                    setTimeout(function () { updateCropPreview(_this); }, 250);
766                });
767
768                $(window).on('resize orientationchange onfullscreenchange onmozfullscreenchange onwebkitfullscreenchange', function() {
769                    updateCropPreview(_this);
770                });
771            } else {
772                $cropBtn.remove();
773            }
774
775            // It's nonsensical for download items to have a quantity > 1
776            if (this.model.get('source') === 'ngg_digital_downloads') {
777                this.$el.find('.nggpl-quantity_field_wrapper').addClass('nggpl-digital-download-source');
778                this.$el.find('.nggpl-quantity_field').attr('max', 1);
779            }
780
781            // Delete button events
782            var delete_button = this.$el.find('.ngg_pro_delete_item');
783            delete_button.on('click', function(e) {
784                e.preventDefault();
785                
786                if (!_this.collection.busy) {
787                    _this.collection.busy = true;
788                    _this.collection.remove(_this.model.id);
789                    _this.model.set('quantity', 0);
790                    _this.$el.fadeOut(400, function() {
791                        $(this).remove();
792                        _this.collection.busy = false;
793                    })
794                }                
795            });
796
797            // Use -/+ buttons to adjust item quantity
798            var quantity_adjusters = this.$el.find('.quantity_column i');
799            var debounced_quantity_changed = _.throttle(function(el) {
800                var $field = $(el);
801                var raw    = $field.val();
802
803                // An empty or otherwise unreadable entry is not a request to remove the row, so
804                // restore what the model holds rather than reading it as a quantity of zero.
805                var quantity = parseInt(raw, 10);
806                if (!raw || raw.length === 0 || isNaN(quantity)) {
807                    $field.val(_this.model.get('quantity'));
808                    return;
809                }
810
811                // Typed input has to honour the same bounds as the -/+ buttons, which clamp against
812                // the field's own min/max. Without this a typed negative removes the model while
813                // leaving the row on screen, and a typed value above max bypasses the cap.
814                var min = parseInt($field.attr('min'), 10);
815                var max = parseInt($field.attr('max'), 10);
816                if (!isNaN(min) && quantity < min) { quantity = min; }
817                if (!isNaN(max) && quantity > max) { quantity = max; }
818                $field.val(quantity);
819
820                if (quantity == 0) {
821                    _this.collection.remove(_this.model.id);
822                    _this.$el.fadeOut(400, function() {
823                        $(this).remove();
824                    });
825                }
826
827                _this.model.set('quantity', quantity);
828                Ngg_Pro_Cart.get_instance().updateQuantity(_this.image_id, _this.model);
829
830            }, 1500);
831
832            quantity_adjusters.on('click', function(event) {
833                var $target = $(event.currentTarget);
834                var $input = $target.siblings('input');
835                var step = $target.hasClass('fa-minus') ? -1 : 1;
836                var newvalue = parseInt($input.val(), 10) + step;
837                var min = parseInt($input.attr('min'), 10);
838                var max = parseInt($input.attr('max'), 10);
839                if (newvalue < min) { newvalue = min; }
840                if (newvalue > max) { newvalue = max; }
841                $input.val(newvalue);
842                debounced_quantity_changed($input);
843            });
844
845            // Quantity field events. Typed input takes the same path as the -/+ steppers; the
846            // previous call named a method that does not exist, so a typed value threw.
847            this.$el.find('.nggpl-quantity_field').on('change', function() {
848                debounced_quantity_changed(this);
849            });
850
851            return this.el;
852        }
853    });
854
855    Ngg_Pro_Cart.Views.Coupon_Row = Backbone.View.extend({
856        el: '#ngg_pro_cart_coupon_tr',
857
858        code: '',
859
860        events: {
861            'click #ngg_pro_cart_coupon_apply' : 'handle_apply_click',
862            'keypress #ngg_pro_cart_coupon_field' : 'handle_apply_key'
863        },
864
865        initialize: function() {
866            this.model = Ngg_Pro_Cart.get_instance();
867            this.model.on('dataLoaded', this.handle_server_update, this);
868            if (this.model.ready && this.model.coupon && this.model.discount_given) {
869                this.handle_server_update();
870            }
871        },
872
873        handle_server_update_with_notice: function() {
874            this.handle_server_update();
875
876            var cart = this.model;
877            var $notice = $('#ngg_pro_cart_coupon_errors');
878
879            if (cart.coupon && cart.discount_given) {
880                $notice.hide();
881                $('#ngg_pro_cart_coupon_notice').fadeIn().delay(1500).fadeOut(500);
882                $('#ngg_pro_cart_coupon_field').blur();
883
884            } else if (!cart.coupon || !cart.discount_given) {
885                cart.coupon = '';
886                $notice.text('Invalid coupon');
887                $notice.fadeIn().delay(1500).fadeOut(500);
888                $('#ngg_pro_cart_coupon_field').trigger('focus');
889            }
890        },
891
892        handle_server_update: function() {
893            var cart = this.model;
894            var $summaryrows = $('#ngg_pro_cart_coupon_undiscounted_subtotal_tr, #ngg_pro_cart_coupon_discount_amount_tr');
895
896            if (cart.coupon && cart.discount_given) {
897                $('#ngg_pro_cart_coupon_hidden_field').val(cart.coupon);
898                $('#nggpl-undiscounced_subtotal_field').html(this.model.getFormattedCurrency('undiscounted_subtotal'));
899                $('#nggpl-discount_amount_field').html(this.model.getFormattedCurrency('discount_given'));
900                $summaryrows.show();
901
902            } else if (!cart.coupon || !cart.discount_given) {
903                $('#ngg_pro_cart_coupon_hidden_field').val('');
904                cart.coupon = '';
905                cart.discount_given = 0;
906                $summaryrows.hide();
907            }
908        },
909
910        handle_apply_click: function(event) {
911            event.preventDefault();
912            this.apply();
913        },
914
915        handle_apply_key: function(event) {
916            if (event.keyCode === 13) {
917                event.preventDefault();
918                this.apply();
919                return false;
920            }
921        },
922
923        apply: function() {
924            var $field = $('#ngg_pro_cart_coupon_field');
925            this.code  = this.model.coupon = $field.val();
926            var self   = this;
927            $field.val('');
928            this.model.loadData().then(function() {
929                self.handle_server_update_with_notice();
930            });
931        }
932    });
933
934    // Define Cart View
935    Ngg_Pro_Cart.Views.Cart = Backbone.View.extend({
936        el: '#ngg_pro_checkout',
937
938        initialize: function() {
939            var _this = this;
940
941            this.getRawCountryList()
942                .then(function(data){
943                    _this.country_list = data;
944                    _this.model = Ngg_Pro_Cart.get_instance();
945                    _this.model.on('ready', _this.render, _this);
946                    _this.model.on('dataLoaded', _this.refreshed_from_server, _this);
947                    _this.model.on('beforeDataLoaded', _this.refreshing_from_server(), _this);
948                    _this.model.on('change:quantity', _this.update_totals, _this);
949                    if (_this.model.ready) {
950                        _this.refreshed_from_server();
951                        _this.render();
952                    }
953                })
954        },
955
956        events: {
957            'keyup .nggpl-quantity_field' : 'sanitize_quantity',
958            'input #ngg_pro_cart_fields input' : 'shipping_address_changed',
959            'change #ngg_pro_cart_fields select' : 'shipping_address_changed',
960            'click #recalculate' : 'recalculate_shipping_and_taxes',
961        },
962
963        get_cart_images_el: function() {
964            var $images_table = this.$el.find('.ngg_pro_cart_images');
965
966            // Fix IE11 DOM representation
967            if ($images_table.length == 0) {
968                $images_table = $('.ngg_pro_cart_images').parent().detach();
969                this.$el.append($images_table);
970                $images_table = this.$el.find('.ngg_pro_cart_images');
971            }
972
973            return $images_table;
974        },
975
976        fix_ie_dom: function() {
977            if (this.$el.find('#ngg_pro_links_wrapper').length == 0) {
978                var $links = $('#ngg_pro_links_wrapper').detach();
979                this.$el.prepend($links);
980            }
981
982            if (this.$el.find('#ngg_pro_checkout_buttons').length == 0) {
983                var $buttons = $('#ngg_pro_checkout_buttons').detach();
984                this.$el.append($buttons);
985            }
986        },
987
988        sanitize_quantity: function(e){
989            if (!(e.keyCode == 8 || e.keyCode == 37 || e.keyCode == 39 || e.keyCode == 9 || e.keyCode == 46 || (e.charCode >= 48 && e.charCode <= 57))) {
990                e.preventDefault();
991                return false;
992            }
993            return true;
994        },
995
996        get_shippable_countries: function() {
997            var codes = [];
998            this.model.each(function(image){
999                image.get('items').each(function(item){
1000                    _.each(item.get('shippable_to'), function(countryCode){
1001                        codes.push(countryCode);
1002                    });
1003                });
1004            });
1005            return codes;
1006        },
1007
1008        populate_country_list: function (data) {
1009            var $shipping_fields = $('#ngg_pro_cart_fields');
1010            var $country = $shipping_fields.find('select.shipping_country');
1011            var homeCountry = this.model.settings.studio_address.country;
1012            var $regions_col = $shipping_fields.find('.ngg-field-state .ngg-field-input');
1013            var $region_input = $regions_col.find('input');
1014            var countryCount = 0;
1015            var shippable_country_codes = this.get_shippable_countries();
1016
1017            if (typeof(data) === 'undefined') {
1018                data = this.country_data;
1019            } else {
1020                this.country_data = data;
1021            }
1022          
1023            // if we have no valid data or Int'l shipping is disabled and have no homeCountry, return
1024            if (this.country_populated || !data)
1025                return;
1026
1027            this.country_populated = true;
1028
1029            for (var i = 0; i < data.length; i++) {
1030                var country = data[i];
1031                var countryCode = country[1];
1032
1033                if (shippable_country_codes.indexOf(countryCode) === -1) {
1034                    continue;
1035                }
1036
1037                var regions = country[2];
1038                var postcodeRe = typeof(country[3]) != "undefined" ? country[3] : '';
1039                var $option = $('<option />');
1040                $option.val(countryCode);
1041                $option.data('postCodeRegex', postcodeRe);
1042                $option.append(country[0]);
1043                $country.append($option);
1044                countryCount += 1;
1045
1046                if (regions.length > 0) {
1047                    var $region = $('<select />');
1048                    $region.append($('<option value=""/>').append(Ngg_Pro_Cart_Settings.i18n.select_region));
1049                    $region.attr('class', 'shipping_state');
1050                    $region.data('name', $region_input.data('name'));
1051                    $region.data('id', $region_input.data('id'));
1052                    $region.data('countryId', countryCode);
1053
1054                    for (var l = 0; l < regions.length; l++) {
1055                        var region = regions[l];
1056                        var $option_r = $('<option />');
1057                        $option_r.val(region[1]);
1058                        $option_r.append(region[0]);
1059                        $region.append($option_r);
1060                    }
1061
1062                    $regions_col.append($region);
1063                }
1064            }
1065
1066            if (countryCount > 1) {
1067                var $optionDef = $country.find('option[value="' + homeCountry + '"]');
1068                var $option = $('<option />').attr('value', '');
1069                $option.append(Ngg_Pro_Cart_Settings.i18n.select_country);
1070
1071                if ($optionDef.length > 0) {
1072                    $optionDef.attr('selected', 'selected');
1073                    $country.val(homeCountry);
1074                } else {
1075                    $option.attr('selected', 'selected');
1076                }
1077
1078                $country.prepend($option);
1079            }
1080            this.update_country_bound_fields();
1081        },
1082        
1083        shipping_address_changed: function (e) {
1084            var $target = $(e.currentTarget);
1085            var targetName = $target.attr('name');
1086
1087            this.update_country_bound_fields();
1088
1089            // We only disable the checkout buttons here; render_shipping_methods() is responsible for exposing them.
1090            // This prevents customers from checking out before a shipping method can be selected
1091            var valid = !this.shipping_fields_validate(true, true);
1092            if (!valid || (valid && !recalculate)) {
1093                this.maybeDisableCheckoutButtons(valid);
1094            }
1095        },
1096
1097        recalculate_shipping_and_taxes: function() {
1098            this.update_country_bound_fields();
1099            this.shipping_fields_validate(true, true);
1100        },
1101
1102        shipping_fields_validate: function(show_errors, recalculate) {
1103            if (typeof(show_errors) === "undefined")
1104                show_errors = false;
1105            if (typeof(recalculate) === "undefined")
1106                recalculate = false;
1107
1108            var i18n = Ngg_Pro_Cart_Settings.i18n;
1109            var $shipping_fields = $('#ngg_pro_cart_fields');
1110            var $fullName = $shipping_fields.find('input[name="settings[shipping_address][name]"]');
1111            var $email = $shipping_fields.find('input[name="settings[shipping_address][email]"]');
1112            var $street_address = $shipping_fields.find('input[name="settings[shipping_address][street_address]"]');
1113            var $address_line = $shipping_fields.find('input[name="settings[shipping_address][address_line]"]');
1114            var $city = $shipping_fields.find('input[name="settings[shipping_address][city]"]');
1115            var $country = $shipping_fields.find(':input[name="settings[shipping_address][country]"]');
1116            var $state = $shipping_fields.find(':input[name="settings[shipping_address][state]"]:visible');
1117            var $zip = $shipping_fields.find('input[name="settings[shipping_address][zip]"]');
1118            var $phone = $shipping_fields.find('input[name="settings[shipping_address][phone]"]');
1119
1120            var getFieldName = function (field) {
1121                $parent = field.parentsUntil('tr', 'td.ngg-field-input').siblings('td.ngg-field-label');
1122                return $parent.find('label').text();
1123            };
1124
1125            var validationError = function (field, error) {
1126                if (!show_errors) {
1127                    return;
1128                }
1129
1130                $parent = field.parentsUntil('tr', 'td.ngg-field-input');
1131                $errorCont = $parent.find('.ngg-field-error-container');
1132
1133                if ($errorCont.length == 0) {
1134                    $errorCont = $('<span class="ngg-field-error-container"></span>');
1135                    $icon = $('<i class="fas fa-exclamation-triangle ngg-error-icon" aria-hidden="true"></i>');
1136                    $errorCont.append($icon);
1137                    $errorCont.insertAfter(field);
1138                } else {
1139                    $errorCont.insertAfter(field);
1140                }
1141
1142                if (field.is('input') && $.inArray(field.attr('type'), ['checkbox', 'radio']) == -1) {
1143                    $errorCont.addClass('ngg-field-error-container-input');
1144                } else {
1145                    $errorCont.removeClass('ngg-field-error-container-input');
1146                }
1147
1148                if (error != '') {
1149                    $errorCont.attr('title', error);
1150                    $errorCont.css('display', 'inline');
1151                } else {
1152                    $errorCont.css('display', 'none');
1153                }
1154            };
1155
1156            var err = false;
1157            $shipping_fields.find('.ngg-field-error-container').css('display', 'none');
1158
1159            if (!$fullName.val() || $fullName.val().length === 0) {
1160                validationError($fullName, sprintf(i18n.error_invalid, getFieldName($fullName), 3));
1161                err = true;
1162            }
1163
1164            // Validating email correctly is basically impossible. As long as the user provides
1165            // (anything)@(domain).(tld) we should accept it: [email protected] is a valid address and domain (owned by Google)
1166            if (!$email.val() || $email.val().length < 5 || !/\S+@\S+\.\S+/.test($email.val())) {
1167                validationError($email, sprintf(i18n.error_invalid, getFieldName($email)));
1168                err = true;
1169            }
1170
1171            if (this.model.has_shippable_items) {
1172                if (!$street_address.val() && !$address_line.val()) {
1173                    validationError($street_address, sprintf(i18n.error_empty, getFieldName($street_address)));
1174                    err = true;
1175                }
1176
1177                if (!$city.val()) {
1178                    validationError($city, sprintf(i18n.error_empty, getFieldName($city)));
1179                    err = true;
1180                }
1181
1182                if (!$country.val()) {
1183                    validationError($country, sprintf(i18n.error_empty, getFieldName($country)));
1184                    err = true;
1185                }
1186
1187                // A state/region is required only for US/CA (mirrors Order.php::validate()); it is
1188                // not part of EU/GB postal addresses, so requiring it would wrongly block checkout (#154).
1189                var _shipping_country = $country.val();
1190                if (!$state.val() && $state.is('select') && (_shipping_country === 'US' || _shipping_country === 'CA')) {
1191                    validationError($state, sprintf(i18n.error_empty, getFieldName($state)));
1192                    err = true;
1193                }
1194
1195                // The shortest possible 'local' phone number is all of three digits. Until we can implement
1196                // a phone validation routine for every country the most accurate international phone number validator
1197                // is simply one that just checks for the existence of three digits.
1198                if ($phone.val().length >= 1 && !(/^\d{3,}$/).test($phone.val().replace(/[\s()+\-\.]|ext/gi, ''))) {
1199                    validationError($phone, sprintf(i18n.error_invalid, getFieldName($phone)));
1200                    err = true;
1201                }
1202
1203                // A postal code is required wherever the country defines one (mirrors
1204                // Order.php::validation()); only its FORMAT is checked US/CA-only below, as
1205                // national formats vary too widely to validate reliably elsewhere.
1206                var postCodeRegex = $country.find('option[value="' + $country.val() + '"]').data('postCodeRegex');
1207                if (postCodeRegex && !$zip.val().trim()) {
1208                    validationError($zip, sprintf(i18n.error_empty, getFieldName($zip)));
1209                    err = true;
1210                } else if ((_shipping_country === 'US' || _shipping_country === 'CA') && postCodeRegex != '' && !(new RegExp(postCodeRegex, 'i')).test($zip.val().trim())) {
1211                    validationError($zip, sprintf(i18n.error_invalid, getFieldName($zip)));
1212                    err = true;
1213                }
1214            }
1215
1216            if (!err && recalculate) {
1217                // The below debouncedLoadData() will ping the server to fetch shipping information.
1218                $("#ngg_pro_cart_subitems_overlay").addClass('ngg_pro_cart_subitems_overlay_open');
1219                var settings = this.model.settings;
1220                settings.shipping_address.country = $country.val();
1221                settings.shipping_address.state = $state.val();
1222                settings.shipping_address.zip = $zip.val();
1223                settings.shipping_address.name = $fullName.val();
1224                settings.shipping_address.street_address = $street_address.val();
1225                settings.shipping_address.address_line = $address_line.val();
1226                settings.shipping_address.city = $city.val();
1227                settings.shipping_method = null;
1228
1229                this.model.settings = settings;
1230                this.debouncedLoadData();
1231            }
1232
1233            return err;
1234        },
1235
1236        debouncedLoadData: _.debounce(function() {
1237            this.model.loadData();
1238        }, 750),
1239
1240        getRawCountryList: function() {
1241            return new Promise(function (resolve, reject) {
1242                try {
1243                    $.getJSON(Ngg_Pro_Cart_Settings.country_list_json_url, {}, function (data) {
1244                        resolve(data);
1245                    });
1246                }
1247                catch (err) {
1248                    reject(err);
1249                }
1250            });
1251        },
1252
1253        maybeDisableCheckoutButtons: function(isValidShipping) {
1254            var $checkout_buttons = $('#ngg_pro_checkout_buttons a, #ngg_pro_checkout_buttons button, #ngg_pro_checkout_buttons input');
1255            if (!isValidShipping) {
1256                $checkout_buttons.each(function() {
1257                    $(this).prop('disabled', 'disabled');
1258                    $(this).attr('title', Ngg_Pro_Cart_Settings.i18n.error_form_invalid);
1259                });
1260            } else {
1261                $checkout_buttons.each(function() {
1262                    $(this).prop('disabled', false);
1263                    $(this).attr('title', '');
1264                });
1265            }
1266
1267            Ngg_Pro_Cart.get_instance().trigger('disable_checkout_buttons', isValidShipping);
1268        },
1269
1270        update_country_bound_fields: function () {
1271            var $shipping_fields = $('#ngg_pro_cart_fields');
1272            var $country = $shipping_fields.find('.shipping_country');
1273            var country = $country.val();
1274
1275            var $region_input = $shipping_fields.find('input.shipping_state');
1276            var $regions = $shipping_fields.find(':input');
1277            var $region_field = null;
1278            $regions.each(function (index) {
1279                var $this = $(this);
1280                var is_region = $this.data('name') == $region_input.data('name');
1281                var countryId = $this.data('countryId');
1282                if (countryId) {
1283                    if (countryId == country) {
1284                        $this.show();
1285                        if (is_region)
1286                            $region_field = $this;
1287                    } else {
1288                        if (is_region) {
1289                            $this.attr('id', '');
1290                            $this.attr('name', '');
1291                        }
1292                        $this.hide();
1293                    }
1294                }
1295            });
1296
1297            if ($region_field != null) {
1298                $region_field.attr('id', $region_input.data('id')).attr('name', $region_input.data('name'));
1299                $region_input.hide();
1300            } else {
1301                $region_input.attr('id', $region_input.data('id')).attr('name', $region_input.data('name')).show();
1302            }
1303
1304            // Show the international-duties (delivered-duty-unpaid) notice for non-US/CA destinations.
1305            var $duties_notice = $('#international_duties_notice');
1306            if (country && country !== 'US' && country !== 'CA') {
1307                $duties_notice.show();
1308            } else {
1309                $duties_notice.hide();
1310            }
1311        },
1312
1313        update_totals: function(urgent, validShipping){
1314            if (typeof(urgent) == 'undefined') urgent = false;
1315            if (typeof(validShipping) == 'undefined') validShipping = true;
1316
1317            var $images_table = this.get_cart_images_el();
1318
1319            // Hide/show no items message
1320            var $no_items = $('#ngg_pro_no_items');
1321            var $checkout_buttons = $('#ngg_pro_checkout_buttons');
1322
1323            var empty_cart = this.model.item_count() <= 0;
1324            if (!empty_cart) {
1325                if (urgent) {
1326                    $images_table.show();
1327                    $no_items.hide();
1328                    $checkout_buttons.show();
1329                } else {
1330                    $images_table.fadeIn('fast');
1331                    $no_items.fadeOut('fast');
1332                    $checkout_buttons.fadeIn('fast');
1333                }
1334            } else {
1335                if (urgent) {
1336                    $images_table.hide();
1337                    $checkout_buttons.hide();
1338                    $no_items.show();
1339                } else {
1340                    $images_table.fadeOut('fast');
1341                    $checkout_buttons.fadeOut('fast');
1342                    $no_items.fadeIn('fast');
1343                }
1344            }
1345
1346            // Allows gateways to hide their button if they can't handle free orders
1347            // TODO: is toggleClass appropriate here?
1348            $('#ngg_pro_checkout').toggleClass('ngg_cart_shippable_items', this.model.has_shippable_items)
1349                                  .toggleClass('ngg_cart_free', (parseFloat(this.model.total) == 0));
1350
1351            // Update totals
1352            this.$el.find('#nggpl-subtotal_field').html(this.model.getFormattedCurrency('sub_total'));
1353            this.$el.find('#nggpl-shipping_field').html(
1354                validShipping ? this.model.getFormattedCurrency('shipping') : Ngg_Pro_Cart_Settings.i18n.tbd
1355            );
1356            this.$el.find('#nggpl-total_field').html(this.model.getFormattedCurrency('total'));
1357
1358            // If there are no taxes we just hide this table row
1359            if (this.model.tax === 0) {
1360                this.$el.find('#tax_field_row').hide();
1361            } else {
1362                this.$el.find('#tax_field_row').show();
1363            }
1364
1365            this.$el.find('#nggpl-tax_field').html(this.model.getFormattedCurrency('tax'));
1366        },
1367
1368        refreshed_from_server: function(response) {
1369            var invalidShipping = !this.shipping_fields_validate();
1370
1371            if (response && 'string' === typeof response.error && response.error.length >= 1) {
1372                invalidShipping = false;
1373                alert(response.error);
1374            }
1375
1376            this.populate_country_list(this.country_list);
1377            this.shipping_fields_validate(true);
1378
1379            invalidShipping = this.render_shipping_methods(invalidShipping);
1380
1381            // Update totals
1382            this.update_totals(true, invalidShipping);
1383
1384            this.toggle_shipping_fields();
1385        },
1386
1387        refreshing_from_server: function() {
1388            var i18n = Ngg_Pro_Cart_Settings.i18n;
1389            $('#nggpl-shipping_field').text(i18n.calculating);
1390            var $item = $('<option/>').text(i18n.calculating)
1391            $('#nggpl-ship_via_field select').empty().append($item);
1392            $('#nggpl-tax_field').text(i18n.calculating);
1393        },
1394
1395        render_shipping_methods: function(validShipping) {
1396            var $ship_via_field = $('#ship_via_row select').empty();
1397            var _this = this;
1398
1399            if (validShipping) {
1400                _.each(this.model.shipping_methods, function(shipping_method) {
1401                    var $option = $('<option/>').val(shipping_method.name).text(shipping_method.title).attr('data-amount', shipping_method.amount);
1402                    if (_this.model.settings.hasOwnProperty('shipping_method') && _this.model.settings.shipping_method == shipping_method.name) {
1403                        $option.attr('selected', 'selected');
1404                    }
1405                    $ship_via_field.append($option);
1406                });
1407            } else {
1408                this.model.shipping = 0.0;
1409            }
1410
1411            if (this.model.shipping_methods.length > 0) {
1412                $('#ship_via_row select').show();
1413                $('#unshippable_notice').hide();
1414                this.maybeDisableCheckoutButtons(validShipping);
1415            } else {
1416                this.model.settings.shipping_method = false;
1417                this.model.save();
1418
1419                if (this.model.has_shippable_items) {
1420                    var $notice = $('#unshippable_notice');
1421                    // When an international destination is blocked, name the offending items with a
1422                    // reason picked per item: the "too large" wording only fits US/CA-only lab items
1423                    // (large-format pieces excluded from the EU); anything else — a manual-source
1424                    // item, or a country the lab doesn't serve at any size — gets a generic reason.
1425                    var country = $('#ngg_pro_cart_fields .shipping_country').val();
1426                    var oversizeBlockers = [];
1427                    var otherBlockers = [];
1428                    if (country && country !== 'US' && country !== 'CA') {
1429                        this.model.each(function(image) {
1430                            image.get('items').each(function(item) {
1431                                var to = item.get('shippable_to') || [];
1432                                var title = item.get('title');
1433                                if (!title || to.indexOf(country) !== -1) {
1434                                    return;
1435                                }
1436                                var usCaOnly = to.length === 2 && to.indexOf('US') !== -1 && to.indexOf('CA') !== -1;
1437                                var list = usCaOnly ? oversizeBlockers : otherBlockers;
1438                                if (list.indexOf(title) === -1) {
1439                                    list.push(title);
1440                                }
1441                            });
1442                        });
1443                    }
1444                    var noticeParts = [];
1445                    if (oversizeBlockers.length > 0) {
1446                        noticeParts.push(Ngg_Pro_Cart_Settings.i18n.unshippable_intl_items.replace('%s', oversizeBlockers.join(', ')));
1447                    }
1448                    if (otherBlockers.length > 0) {
1449                        noticeParts.push(Ngg_Pro_Cart_Settings.i18n.unshippable_country_items.replace('%s', otherBlockers.join(', ')));
1450                    }
1451                    if (noticeParts.length > 0) {
1452                        $notice.text(noticeParts.join(' '));
1453                    } else {
1454                        $notice.text(Ngg_Pro_Cart_Settings.i18n.unshippable);
1455                    }
1456                    validShipping ? $notice.show() : $notice.hide();
1457                    validShipping = false;
1458                }
1459
1460                $('#ship_via_row select').hide();
1461                this.maybeDisableCheckoutButtons(validShipping);
1462            }
1463
1464            (this.model.has_shippable_items && validShipping) ? $('#ship_via_row').show() : $('#ship_via_row').hide();
1465
1466            return validShipping;
1467        },
1468
1469        toggle_shipping_fields: function() {
1470            var fields = [
1471                $('tr.ngg-shipping-field.ngg-field-street_address'),
1472                $('tr.ngg-shipping-field.ngg-field-address_line'),
1473                $('tr.ngg-shipping-field.ngg-field-city'),
1474                $('tr.ngg-shipping-field.ngg-field-country'),
1475                $('tr.ngg-shipping-field.ngg-field-state'),
1476                $('tr.ngg-shipping-field.ngg-field-zip'),
1477                $('tr.ngg-shipping-field.ngg-field-phone'),
1478                $('#shipping_field_row'),
1479                $('#ship_via_row select')
1480            ];
1481            if (!this.model.has_shippable_items) {
1482                fields.forEach(function(element) {
1483                    element.hide();
1484                });
1485            } else {
1486                fields.forEach(function(element) {
1487                    element.show();
1488                });
1489            }
1490        },
1491
1492        render: function() {
1493            var _this = this;
1494
1495            $('#ship_via_row select').on('change', function() {
1496                _this.maybeDisableCheckoutButtons(false);
1497                $("#ngg_pro_cart_subitems_overlay").addClass('ngg_pro_cart_subitems_overlay_open');
1498                var settings = _this.model.settings;
1499                settings.shipping_method = $(this).val();
1500                _this.model.settings = settings;
1501                _this.model.loadData();
1502            });
1503
1504            // Display images
1505            this.model.each(function(image){
1506                var $images_table = _this.get_cart_images_el();
1507                var items = image.get('items');
1508                items.each(function(item) {
1509                    var item_row = new Ngg_Pro_Cart.Views.Item_Row({
1510                        model: item,
1511                        collection: items,
1512                        image_id: image.id
1513                    });
1514                    $images_table.append(item_row.render());
1515                }, this);
1516            });
1517
1518            new Ngg_Pro_Cart.Views.Coupon_Row();
1519
1520            // Fix IE10
1521            this.fix_ie_dom();
1522
1523            $('#nggpl-shipping_field').text(Ngg_Pro_Cart_Settings.i18n.tbd);
1524
1525            this.toggle_shipping_fields();
1526
1527            // Show the cart
1528            this.$el.css('visibility', 'visible');
1529        }
1530    });
1531
1532    Ngg_Pro_Cart.Views.Add_To_Cart = Backbone.View.extend({
1533        tagName: 'div',
1534
1535        id: 'ngg_add_to_cart_container',
1536
1537        className: 'scrollable',
1538
1539        events: function() {
1540            // To prevent methods being run twice when touched (thanks to browser emitting a click event as well)
1541            // any methods bound here should call event.stopPropogation() and event.preventDefault()
1542            return {
1543                'touchstart #ngg_checkout_btn' : 'redirect_to_checkout',
1544                'click #ngg_checkout_btn' : 'redirect_to_checkout',
1545                'touchstart .nggpl-cart_count' : 'redirect_to_checkout',
1546                'click .nggpl-cart_count' : 'redirect_to_checkout',
1547                'keyup .nggpl-quantity_field' : 'sanitize_quantity',
1548                'blur .nggpl-quantity_field': 'quantity_lost_focus',
1549                'focusout .nggpl-quantity_field': 'quantity_lost_focus',
1550                'touchstart #ngg_update_cart_btn': 'update_cart',
1551                'click #ngg_update_cart_btn': 'update_cart',
1552                'click .nggpl-quantity_field i': 'update_quantity'
1553            };
1554        },
1555
1556        // Use -/+ buttons to adjust item quantity
1557        update_quantity: function(e) {
1558            var $target = $(e.currentTarget);
1559            var $input = $target.siblings('input');
1560            var step = $target.hasClass('fa-minus') ? -1 : 1;
1561            var newvalue = parseInt($input.val(), 10) + step;
1562            var min = parseInt($input.attr('min'), 10);
1563            var max = parseInt($input.attr('max'), 10);
1564            if (newvalue < min) { newvalue = min; }
1565            if (newvalue > max) { newvalue = max; }
1566            $input.val(newvalue);
1567        },
1568
1569        quantity_lost_focus: function(event) {
1570            event.stopPropagation();
1571            event.preventDefault();
1572
1573            this.limit_quantity($(event.target));
1574
1575            // iOS does not fire this event when the onscreen keyboard is finished
1576            $(window).trigger('resize');
1577            $('.galleria-sidebar-container').trigger('focus');
1578        },
1579
1580        getItem: function(image_id, item_id){
1581            var retval = null;
1582
1583            _.each(
1584                this.tables,
1585                function(table) {
1586                    if (!retval) {
1587                        if (table.image_id == image_id) {
1588                            retval = table.items.get(item_id)
1589                        }
1590                    }
1591                },
1592                this
1593            );
1594
1595            return retval;
1596        },
1597
1598        update_cart: function(e) {
1599            e.stopPropagation();
1600            e.preventDefault();
1601
1602            var _this = this;
1603            $('#nggpl-items_for_sale td.nggpl-quantity_field input').each(function() {
1604                var item_id = $(this).parents('tr').data('item-id');
1605                var item = _this.getItem(_this.image_id, item_id)
1606                if (item) {
1607                    var quantity = $(this).val();
1608                    if (isNaN(quantity)) {
1609                        quantity = 0;
1610                    } else {
1611                        quantity = parseInt(quantity);
1612                    }
1613                    item.set('quantity', quantity);
1614                } else {
1615                    item = new Ngg_Pro_Cart.Models.PricelistItem({
1616                        ID: item_id,
1617                        quantity: 0
1618                    });
1619                }
1620
1621                Ngg_Pro_Cart.get_instance().updateQuantity(_this.image_id, item, {loadData: false});
1622            });
1623            this.model.save();
1624            this.model.loadData();
1625        },
1626
1627        // Ensures the min/max values are respected by browsers without input type=number limitations
1628        limit_quantity: function($input) {
1629            var min = parseInt($input.attr('min'), 10);
1630            var max = parseInt($input.attr('max'), 10);
1631            var curval = parseInt($input.val(), 10);
1632            if (curval < min) { curval = min; }
1633            if (curval > max) { curval = max; }
1634            $input.val(curval);
1635        },
1636
1637        sanitize_quantity: function(e) {
1638            e.stopPropagation();
1639
1640            // Only allow the following keys to be used in our
1641            // 8: backspace
1642            // 9: tab
1643            // 37: left arrow
1644            // 39: right arrow
1645            // 46: delete
1646            // 48: 0
1647            // 57: 9
1648            if (!(e.keyCode === 8 || e.keyCode === 37 || e.keyCode === 39 || e.keyCode === 9 || e.keyCode === 46 || (e.charCode >= 48 && e.charCode <= 57))) {
1649                e.preventDefault();
1650                return false;
1651            }
1652
1653            this.limit_quantity($(e.target));
1654
1655            return true;
1656        },
1657
1658        initialize: function(params) {
1659            this.tables = {};
1660            this.image_id = params.image_id;
1661            this.container = params.container;
1662            this.datacache = params.datacache;
1663            this.model = Ngg_Pro_Cart.get_instance();
1664            this.listenTo(this.model, 'ready', this.render);
1665            this.listenTo(this.model, 'dataLoaded', this.update_and_animate_cart_summary);
1666            if (this.model.ready) {
1667                this.render();
1668            }
1669        },
1670
1671        update_and_animate_cart_summary: function() {
1672            this.update_cart_summary(true);
1673        },
1674
1675        redirect_to_checkout: function(event) {
1676            event.stopPropagation();
1677            event.preventDefault();
1678            var referrer = encodeURIComponent(parent.location.toString());
1679            var url = Ngg_Pro_Cart_Settings.checkout_url;
1680            if (url.indexOf('?') > 0) {
1681                url += "&referrer="+referrer;
1682            } else {
1683                url += "?referrer="+referrer;
1684            }
1685            parent.location = url;
1686        },
1687
1688        update_cart_summary: function(animate) {
1689            var $summary = this.$el.find('.nggpl-cart_summary');
1690
1691            $summary.find('.nggpl-cart_count')
1692                    .text(ngg_cart_i18n.item_count.replace('%d', this.model.item_count()));
1693
1694            $summary.find('.nggpl-cart_total')
1695                    .html(this.model.getFormattedCurrency('sub_total'));
1696
1697            this.set_add_or_update_cart_text();
1698
1699            if (animate) {
1700                $('#nggpl-cart_updated_wrapper').addClass('nggpl-cart_updated_wrapper_visible');
1701                setTimeout(function() {
1702                    $('#nggpl-cart_updated_wrapper').removeClass('nggpl-cart_updated_wrapper_visible');
1703                }, 1500);
1704            }
1705        },
1706
1707        // If the cart is empty we display "Add to Cart" - otherwise "Update Cart"
1708        set_add_or_update_cart_text: function() {
1709            var $btn = $('#ngg_update_cart_btn');
1710            if ($btn.length > 0) {
1711                if (this.model.item_count() > 0) {
1712                    $btn.val($btn.data('update-string'));
1713                } else {
1714                    $btn.val($btn.data('add-string'));
1715                }
1716            }
1717        },
1718
1719        render: function() {
1720            var image_id = this.image_id;
1721
1722            this.container.empty();
1723
1724            this.$el.empty();
1725            this.$el.attr('data-image-id', image_id);
1726
1727            this.$el.append(ngg_add_to_cart_templates.add_to_cart_wrapper);
1728            var _this = this;
1729
1730            // Update cart total
1731            this.update_cart_summary(false);
1732
1733            // Render the tables
1734            this.tables = {};
1735            this.$el.find('.nggpl-pricelist_category_wrapper').find('.nggpl-category_contents').each(function() {
1736                var table = new Ngg_Pro_Cart.Views.Add_To_Cart.Items_Table({
1737                    image_id: image_id,
1738                    datacache: _this.datacache
1739                });
1740                $(this).empty().append(table.render());
1741                _this.tables[$(this).attr('id')] = table;
1742            });
1743
1744            var cart = Ngg_Pro_Cart.get_instance();
1745
1746            // The items currently in the cart. We will be overriding cached data with quantities from this variable
1747            var current_items = _.has(cart.getCartData().images, image_id) ? cart.getCartData().images[image_id].items : [];
1748
1749            // Items in the cart when the XHR request was made to fetch the sidebar data for this image
1750            var items = _.has(this.datacache.image_items, 'items') ? this.datacache.image_items.items : [];
1751
1752            // Adjust updated quantities of items still in the cart
1753            _.each(current_items, function(current_item, current_index) {
1754                _.each(items, function(item, index) {
1755                    if (item.ID == current_index) {
1756                        items[index].quantity = current_item.quantity;
1757                    }
1758                });
1759            });
1760
1761            // Remove pricelist items from cached data no longer present in cart
1762            _.each(items, function(item, index) {
1763                if (!_.has(current_items, item.ID)) {
1764                    items[index].quantity = 0;
1765                }
1766            });
1767
1768            // Add items to each table
1769            _.each(
1770                _.filter(items, function(item) {
1771                    return item.title !== "";
1772                }),
1773                function(item) {
1774                    _this.tables[item.category].items.add(item);
1775                },
1776                _this
1777            );
1778
1779            // Set the generated content so we can inspect and manipulate it further
1780            this.container.append(this.el);
1781
1782            // Hide headers for categories without items
1783            _.each(_this.tables, function(table) {
1784                var id = table.$el.parent().attr('id');
1785                var $header = $('#' + id + '_header').parent();
1786                if (_this.tables[id].items.length === 0) {
1787                    $header.hide();
1788                } else {
1789                    var $link = $('<h3>' + $header.html() + '</h3>');
1790                    $link.attr('data-ngg-category', id);
1791                    $('#nggpl-category-headers').append($link);
1792                    $link.on('click', function() {
1793                        $('#npl_sidebar').animate({ scrollTop: $header.position().top });
1794                    });
1795                }
1796            }, _this);
1797
1798            // Are there items?
1799            if (items.length > 0) {
1800                _this.$el.find('#nggpl-not_for_sale').css('display', 'none');
1801                _this.$el.find('#nggpl-items_for_sale').css('display', 'inline-block');
1802            } else {
1803                _this.$el.find('#nggpl-items_for_sale').css('display', 'none');
1804                _this.$el.find('#nggpl-not_for_sale').css('display', 'block');
1805            }
1806
1807            if ($.nplModal('get_setting', 'sidebar_button_color')) {
1808                _this.$el.find('#ngg_checkout_btn, #ngg_update_cart_btn').css({'color': $.nplModal('get_setting', 'sidebar_button_color')});
1809            }
1810            if ($.nplModal('get_setting', 'sidebar_button_background')) {
1811                _this.$el.find('#ngg_checkout_btn, #ngg_update_cart_btn').css({'background-color': $.nplModal('get_setting', 'sidebar_button_background')});
1812            }
1813
1814            this.set_add_or_update_cart_text();
1815
1816            // Updates the sidebar image thumbnail for low width browsers
1817            $('#npl_content').trigger('npl_sidebar_rendered');
1818
1819            $('#nggpl-cart_sidebar_checkout_buttons, #nggpl-cart-static-header').css({
1820                background: $.nplModal('get_setting', 'sidebar_background_color')
1821            });
1822
1823            // Images not for sale won't have this cache entry
1824            if (typeof _this.datacache.digital_download_settings !== 'undefined') {
1825                // The full selector here is *necessary* : the (pricelist_category)_header ID is present more than once!
1826                $('.nggpl-pricelist_category_wrapper #ngg_category_digital_downloads_header').html(
1827                    _this.datacache.digital_download_settings.header
1828                );
1829                _.each($('#nggpl-category-headers h3'), function(header) {
1830                    if ($(header).data('ngg-category') === 'ngg_category_digital_downloads') {
1831                        $(header).find('span').html(_this.datacache.digital_download_settings.header);
1832                    }
1833                });
1834            }
1835
1836            cart.trigger('rendered');
1837            $('#npl_wrapper').removeClass('npl-sidebar-overlay-open');
1838        }
1839    });
1840
1841    Ngg_Pro_Cart.Views.Add_To_Cart.Items_Table = Backbone.View.extend({
1842        tagName: 'table',
1843
1844        class: 'items_table',
1845
1846        initialize: function(params) {
1847            this.image_id = params.image_id;
1848            this.datacache = params.datacache;
1849            this.items = new Ngg_Pro_Cart.Models.PricelistItemCollection();
1850            this.items.on('add', this.render_row, this);
1851        },
1852
1853        render: function() {
1854            this.$el.hide();
1855            this.$el.html(ngg_add_to_cart_templates.add_to_cart_header);
1856            this.$el.attr('data-image-id', this.image_id);
1857            return this.el;
1858        },
1859
1860        render_row: function(item) {
1861            var row = new Ngg_Pro_Cart.Views.Add_To_Cart.Item_Row({
1862                model: item,
1863                image_id: this.image_id,
1864                datacache: this.datacache
1865            });
1866            this.$el.find('tbody').append(row.render());
1867            this.$el.show();
1868        }
1869    });
1870
1871    Ngg_Pro_Cart.Views.Add_To_Cart.Item_Row = Backbone.View.extend({
1872        tagName: 'tr',
1873
1874        events: {
1875            'updated_quantity input': 'update_quantity',
1876            'click .nggpl-add-download-button': 'update_download_items'
1877        },
1878
1879        initialize: function(params) {
1880            this.image_id = params.image_id;
1881            this.datacache = params.datacache;
1882            this.model.on('change:quantity', this.update_subtotal, this);
1883            this.skip_checkout = (this.datacache.digital_download_settings.skip_checkout === '1');
1884        },
1885
1886        update_download_items: function() {
1887            var price = this.model.get('price');
1888            var button = this.$el.find('.nggpl-add-download-button');
1889
1890            if (price === 0 && this.skip_checkout) {
1891                var url = window.Ngg_Pro_Cart_Settings.download_url;
1892                url = url.replace('{image_id}', this.image_id);
1893                url = url.replace('{item_id}',  this.model.get('ID'));
1894
1895                // We do not need to specify the 'download' filename / attribute here because that is provided
1896                // by the XHR controller via the Content-Disposition header which takes priority
1897                var anchor = document.createElement("a");
1898                anchor.style.display = 'none';
1899                anchor.href = url;
1900                document.body.appendChild(anchor);
1901                anchor.click();
1902                document.body.removeChild(anchor);
1903                delete anchor;
1904
1905            } else {
1906                if (this.model.get('quantity') === 0) {
1907                    button.html(button.data('remove-text'));
1908                    this.model.set('quantity', 1);
1909                } else {
1910                    button.html(button.data('add-text'));
1911                    this.model.set('quantity', 0);
1912                }
1913
1914                Ngg_Pro_Cart.get_instance().updateQuantity(this.image_id, this.model);
1915            }
1916        },
1917
1918        render: function() {
1919            var price = this.model.get('price');
1920
1921            // Download items do not need a quantity input, just "buy / don't buy"
1922            if (this.model.get('source') === 'ngg_digital_downloads') {
1923                this.$el.html(ngg_add_to_cart_templates.add_to_cart_download_item);
1924                var button = this.$el.find('.nggpl-add-download-button');
1925                if (price === 0 && this.skip_checkout) {
1926                    button.html(button.data('free-text'));
1927                } else {
1928                    if (this.model.get('quantity') > 0) {
1929                        button.html(button.data('remove-text'));
1930                    }
1931                }
1932            } else {
1933                this.$el.html(ngg_add_to_cart_templates.add_to_cart_normal_item);
1934            }
1935
1936            this.$el.attr('data-item-id', this.model.id);
1937            this.$el.find('.nggpl-quantity_field input').val(this.model.get('quantity'));
1938            this.$el.find('.nggpl-description_field').text(this.model.get('title'));
1939
1940            var price_field = this.$el.find('.nggpl-price_field');
1941            if (price === 0 && this.skip_checkout) {
1942                price_field.html(price_field.data('free-label'));
1943            } else {
1944                price_field.html(sprintf(Ngg_Pro_Cart_Settings.currency_format, price));
1945            }
1946
1947            this.$el.find('.nggpl-total_field').html(sprintf(Ngg_Pro_Cart_Settings.currency_format, this.model.subtotal()));
1948
1949            return this.el;
1950        },
1951
1952        update_quantity: function(e, options) {
1953            var quantity = $(e.target).val();
1954            if (isNaN(quantity)) {
1955                quantity = 0;
1956            } else {
1957                quantity = parseInt(quantity);
1958            }
1959
1960            this.model.set('quantity', quantity);
1961            Ngg_Pro_Cart.get_instance().updateQuantity(this.image_id, this.model, options);
1962        },
1963
1964        update_subtotal: function() {
1965            this.$el.find('.nggpl-total_field')
1966                    .html(sprintf(Ngg_Pro_Cart_Settings.currency_format, this.model.subtotal()));
1967        }
1968    });
1969
1970    if (typeof(window.Ngg_Pro_Cart) !== "undefined") {
1971        window.Ngg_Pro_Cart.get_instance().on('dataLoaded', function () {
1972            $('i.nextgen-menu-cart-icon').each(function (index) {
1973                var $this = $(this);
1974                var show_cart_icon = false;
1975                var $menu_item = $this.parents('li').first();
1976                var placeholder = $menu_item.find('.nextgen-menu-cart-placeholder');
1977
1978                if (($this.hasClass('nextgen-menu-cart-icon-icon_and_total_with_items') || $this.hasClass('nextgen-menu-cart-icon-icon_with_items')) && Ngg_Pro_Cart.get_instance().subtotal() > 0) {
1979                    show_cart_icon = true;
1980                } else if ($this.hasClass('nextgen-menu-cart-icon-icon_and_total') || $this.hasClass('nextgen-menu-cart-icon-icon')) {
1981                    show_cart_icon = true;
1982                }
1983
1984                if (placeholder.length > 0) {
1985                    placeholder.html(' (' + Ngg_Pro_Cart.get_instance().getFormattedCurrency('sub_total') + ')');
1986                }
1987
1988                if (show_cart_icon) {
1989                    $this.show();
1990                    $menu_item.show();
1991                } else {
1992                    $menu_item.hide();
1993                }
1994            });
1995        });
1996    }
1997
1998})(jQuery);

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.