PageSourceSearch

https://defensiefotografie.nl/wp-content/plugins/nextgen-gallery-pro/static/Commerce/Cart.js?ver=3.31.9

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