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