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.