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