PageSourceSearch

https://thesaddlelightcenter.com/wp-content/plugins/nextgen-gallery-pro/static/Commerce/Cart.js?ver=4.2.3

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