1/*! MUI 2 * https://merchello.com 3 * Copyright (c) 2018 Across the Pond, LLC. 4 * Licensed 5 */ 6 7 8// Generic Merchello UI scripts. 9// Requires JQuery version 1.10.2 or higher 10// JQuery unobrusive 11// underscore.js 12var MUI = (function() { 13 14 // If DEBUG_MODE is true allows messages to be written to the console 15 // THESE SHOULD be set to false before deploying to production!!!!! 16 var DEBUG_MODE = { 17 events: false, 18 console: false 19 }; 20 21 // Private members 22 var eventHandlers = []; 23 24 // Initialization 25 function init() { 26 $(document).ready(function() { 27 // initialize the logger module 28 MUI.Logger.init(); 29 // intialize the notifications 30 MUI.Notify.init() 31 // initialize the forms 32 MUI.Forms.init(); 33 // initialize the add item module 34 MUI.AddItem.init(); 35 // initialize the basket module 36 MUI.Basket.init(); 37 // initialize the wish list module 38 MUI.WishList.init(); 39 // initialize the checkout module 40 MUI.Checkout.init(); 41 // initialize the labels 42 MUI.Labels.init(); 43 }); 44 } 45 46 function hasLogger() { 47 return MUI.Logger.hasLogger; 48 } 49 50 //// Registers an MUI event 51 function registerEvent(evt, callback) { 52 try 53 { 54 var existing = _.find(eventHandlers, function(ev) { return ev.event === evt; }); 55 if (existing !== undefined && existing !== null) { 56 if (callback !== undefined && typeof callback === "function") { 57 existing.callbacks.push(callback); 58 } 59 } else { 60 var callbacks = []; 61 if (callback !== undefined && typeof callback === "function") { 62 callbacks.push(callback); 63 } 64 eventHandlers.push({ event: evt, callbacks: callbacks }); 65 } 66 } 67 catch(err) { 68 MUI.Logger.captureError(err); 69 } 70 } 71 72 /// emit the event 73 function trigger(name, args) { 74 var existing = _.find(eventHandlers, function(ev) { return ev.event === name; }); 75 if (existing === undefined || existing === null) return; 76 77 // execute each of the registered callbacks 78 _.each(existing.callbacks, function(cb) { 79 try { 80 cb(name, args); 81 } 82 catch(err) { 83 MUI.Logger.captureError(err, { 84 extra: { 85 eventName: name, 86 args: args 87 } 88 }); 89 } 90 }); 91 } 92 93 // create a generic cache of functions, where fn is the function to retrieve and execute for a value. 94 // also ensures, the function is executed once and a single value is returned. 95 function createCache(fn) { 96 var cache = {}; 97 return function( key, callback ) { 98 if ( !cache[ key ] ) { 99 cache[ key ] = $.Deferred(function( defer ) { 100 fn( defer, key ); 101 }).promise(); 102 } 103 return cache[ key ].done( callback ); 104 }; 105 } 106 107 // utility method to map the event name from an alias so they can be 108 // more easily referenced in other modules 109 function getEventNameByAlias(events, alias) { 110 var evt = _.find(events, function(e) { return e.alias === alias}); 111 return evt === undefined ? '' : evt.name; 112 } 113 114 // writes events to debug console 115 function debugConsoleEvents(events) { 116 if (DEBUG_MODE.events) { 117 _.each(events, function(ev) { 118 MUI.on(ev.name, function(name, args) { 119 console.info(ev); 120 console.info(args === undefined ? 'No args' : args); 121 }); 122 }); 123 } 124 } 125 126 // write to debug console if in debug mode 127 function debugConsole(obj) { 128 if (DEBUG_MODE.console) { 129 console.info(obj); 130 } 131 } 132 133 // exposed members 134 return { 135 // ensures the settings object is created 136 Settings: { 137 Notifications: {}, 138 Endpoints: {} 139 }, 140 // ensures the services object is created 141 Services: {}, 142 init: init, 143 hasLogger: hasLogger, 144 createCache: createCache, 145 on: registerEvent, 146 emit: trigger, 147 getEventNameByAlias: getEventNameByAlias, 148 debugConsoleEvents : debugConsoleEvents, 149 debugConsole: debugConsole 150 } 151 152})(); 153 154//// The logger 155// If you have a remove logger you can wire it in here. 156MUI.Logger = { 157 158 hasLogger: false, 159
160 // Initializes the Sentry (Raven) logger 161 init: function() { 162 return; 163 }, 164 165 // Sets the module context 166 setSiteContext: function(siteAlias) { 167 return; 168 }, 169 170 setUserContext: function(email) { 171 return; 172 }, 173 174 // Captures an error 175 captureError: function(e, args) { 176 var consoleLog = args === undefined ? e : { error: e, args: args }; 177 MUI.debugConsole(consoleLog); 178 return; 179 }, 180 181 captureMessage: function(msg, args) { 182 var consoleLog = args === undefined ? message : { message: msg, args: args }; 183 MUI.debugConsole(consoleLog); 184 return; 185 }, 186 187 isReady: function() { 188 return MUI.Logger.hasLogger; 189 // this can be used to test a logger is available ex. getsentry.com -> Raven.isSetup(); 190 } 191}; 192 193MUI.Services.Braintree = { 194 195 initialized: false, 196 197 acceptCards: [], 198 199 currentCard: {}, 200 201 events : [ 202 { attempt: 'unbindValidation', name: 'Braintree.UnbindValidation' }, 203 { attempt: 'cardTypeChanged', name: 'Braintree.CardTypeChange' }, 204 { attempt: 'verified', name: 'Braintree.CardVerified' } 205 ], 206 207 // Load the required assets 208 loadAssets: function(callback) { 209 // load the braintree script and validation for cc with a promise 210 // this also asserts if the customer goes back and changes the method to another 211 // braintree method, that these are only loaded once 212 $.when( 213 MUI.Assets.cachedGetScript('/App_Plugins/Merchello/client/lib/card-validator.min.js'), // Verify we don't need this anymore 214 MUI.Assets.cachedGetScript('//js.braintreegateway.com/web/3.38.1/js/client.min.js'), 215 MUI.Assets.cachedGetScript('//js.braintreegateway.com/web/3.38.1/js/hosted-fields.min.js'), 216 MUI.Assets.cachedGetScript('//js.braintreegateway.com/web/3.38.1/js/paypal.min.js') 217 // MUI.Assets.cachedGetScript('//js.braintreegateway.com/v2/braintree.js') 218 ).then(function() { 219 MUI.Services.Braintree.initialized = true; 220 if (callback !== undefined) callback(); 221 }, 222 function(err) { MUI.Logger.captureError(err); }); 223 224 } 225}; 226 227//// A class to deal with AddItem box JQuery functions 228//// This looks for a form with data attribute "data-muifrm='additem'" 229MUI.AddItem = { 230 231 events : [ 232 { alias: 'added', name: 'AddItem.added' }, 233 { alias: 'tableCreated', name: 'AddItem.tableCreated' } 234 ], 235 236 addItemSuccess: 'Successfully added item to basket', 237 238 postUrl: '', 239 240 forms: [], 241 242 // Initializes the AddItem object 243 init: function() { 244 245 if (MUI.Settings.Endpoints.basketSurface !== undefined && MUI.Settings.Endpoints.basketSurface !== '') { 246 MUI.AddItem.postUrl = MUI.Settings.Endpoints.basketSurface + 'AddBasketItem'; 247 } 248 249 // find all the product keys to load the data tables 250 var containers = $('[data-muivalue="product"]'); 251 252 if (containers.length > 0) { 253 $.each(containers, function(idx, c) { 254 var key = ''; 255 key = $(c).data('muikey'); 256 if (key !== undefined && key !== '') { 257 // verify the key does not already exist 258 // this can happen if the product exists on the page more than once 259 var found = _.find(MUI.AddItem.bind.keys, function(k) { return k === key }); 260 if (found === undefined) { 261 MUI.AddItem.bind.keys.push(key); 262 } 263 } 264 }); 265 } 266 267 // keys are all loaded now get the data tables 268 loadData(); 269 270 // loads the product data tables after the keys have been acquired 271 function loadData() { 272 if (MUI.Settings.Endpoints.productTableApi !== undefined && MUI.Settings.Endpoints.productTableApi
272=== '') return; 273 if (MUI.AddItem.bind.keys.length > 0) { 274 var url = MUI.Settings.Endpoints.productTableApi + 'PostGetProductDataTables'; 275 276 $.ajax({ 277 type: 'POST', 278 url: url, 279 dataType: 'json', 280 contentType: "application/json; charset=utf-8", 281 data: JSON.stringify(MUI.AddItem.bind.keys) 282 }).then(function (tables) { 283 284 // build the product data tables as JS objects so we can use prototypes 285 if (tables.length > 0) { 286 $.each(tables, function(tindx, tbl) { 287 var pdt = new MUI.AddItem.ProductDataTable(); 288 pdt.productKey = tbl.productKey; 289 $.each(tbl.rows, function(rindx, dataRow) { 290 pdt.rows.push($.extend(new MUI.AddItem.ProductDataTableRow(), dataRow)); 291 }); 292 MUI.emit('AddItem.tableCreated', pdt); 293 MUI.AddItem.dataTables.push(pdt); 294 //MUI.AddItem.bind.controls(pdt); 295 }); 296 } 297 298 299 // find all of the AddItem forms 300 MUI.AddItem.bind.forms(); 301 302 }, function (err) { 303 MUI.Notify.error(err); 304 MUI.Logger.captureError(err); 305 }); 306 } 307 } 308 309 // Debug 310 MUI.debugConsoleEvents(MUI.AddItem.events); 311 }, 312 313 bind: { 314 315 // An array of product keys that should be bound 316 keys : [], 317 318 forms: function() { 319 var frms = $('[data-muifrm="additem"]'); 320 if (frms.length > 0) { 321 MUI.AddItem.Forms = frms; 322 $.each(frms, function(idx, frm) { 323 MUI.AddItem.bind.form(frm); 324 }); 325 } 326 }, 327 328 // Bind the form; 329 form: function(frm) { 330 331 // If the endpoint is set, override the form to do AJAX posts 332 if (MUI.AddItem.postUrl !== '') { 333 334 $(frm).submit(function () { 335 $.ajax({
336 type: 'POST', 337 url: MUI.AddItem.postUrl, 338 data: $(this).serialize() 339 }).then(function(result) { 340 MUI.emit('AddItem.added', result); 341 MUI.Notify.info(MUI.AddItem.addItemSuccess); 342 }, function(err) { 343 MUI.Logger.captureError(err); 344 }); 345 return false; 346 }); 347 } 348 349 var key = $(frm).closest('[data-muivalue="product"]').data('muikey'); 350 351 if (key !== undefined) { 352 MUI.AddItem.bind.controls(frm, key); 353 } 354 }, 355 356 // Bind the form 357 // This also requires the API promise to be completed. 358 // parameter: pdt - Product Data Table object 359 // TODO - this will need to change in 2.1.0 360 // TODO - when we can designate which sort of form element to use for option choice selction 361 controls: function(frm, key) { 362 var options = MUI.AddItem.getOptionsForProduct(frm, key); 363 364 if (options.length > 0) { 365 $.each(options, function(idx, o) { 366 $(o).change(function() { 367 // TODO filter lists to ensure all choices are available 368 MUI.AddItem.updateVariantPricing(frm, key); 369 MUI.AddItem.ensureInventorySettings(frm, key); 370 }); 371 }); 372 } 373 // initial pricing (on load) 374 MUI.AddItem.updateVariantPricing(frm, key); 375 MUI.AddItem.ensureInventorySettings(frm, key); 376 } 377 }, 378 379 380 // Product data tables 381 // This is essentially a cache of product variant values so that we do not have to hit the API 382 // on every option value change 383 dataTables : [], 384 385 // updates the variant pricing from the product data table data 386 updateVariantPricing: function(frm, key) { 387 // find the append point 388 var well = $(frm).closest('[data-muivalue="product"]'); 389 if (well.length === 0) return; 390 var appendTo = $(well).find('[data-muivalue="variantprice"]'); 391 if (appendTo.length > 0) { 392 appendTo = appendTo[0]; 393 } else { 394 // There is not reason to continue if the append point cannot be found 395 return; 396 } 397 398 var row = MUI.AddItem.findProductDataTableRow(frm, key); 399 400 if (row !== undefined) { 401 402 // update the price 403 var html = ''; 404 if (row.onSale) { 405 html = "<span class='sale-price'>" + row.formattedSalePrice + "</span> <span class='original-price'>" + row.formattedPrice + "</span>"; 406 } else { 407 html = "<span>" + row.formattedPrice + "</span>"; 408 } 409 410 $(appendTo).html(html); 411 } 412 413 }, 414 415 ensureInventorySettings: function(frm, key) { 416 var row = MUI.AddItem.findProductDataTableRow(frm, key); 417 if (row !== undefined) { 418 var appendTo = $(frm).find('[data-muiinv="' + row.productKey + '"]'); 419 if (appendTo.length > 0) { 420 421 var showBtn = row.outOfStockPurchase || row.inventoryCount > 0; 422 423 var html = ''; 424 if (row.inventoryCount > 0) { 425 html = MUI.Settings.Labels.InStock.replace('@0', row.inventoryCount); 426 } else { 427 428 if (row.outOfStockPurchase) { 429 html = MUI.Settings.Labels.OutOfStockAllowPurchase; 430 } else { 431 html = MUI.Settings.Labels.OutOfStock; 432 } 433 html = "<span>" + html + "</span>"; 434 } 435 436 $(appendTo).html(html); 437 var btn = $(frm).find(':submit'); 438 439 if (showBtn) { 440 $(btn).show(); 441 } else { 442 $(btn).hide(); 443 } 444 } 445 } 446 }, 447 448 getOptionsForProduct: function(frm, key) { 449 var container = $('[data-muikey="' + key + '"]'); 450 if (container.length > 0) { 451 var parents = $(frm).find('[data-muioption]'); 452 var options = []; 453 $.each(parents, function(idx, p) { 454 // find the select 455 options.push($(p).find('select')); 456 }); 457 return options; 458 } else { 459 return []; 460 } 461 }, 462 463 findProductDataTableRow: function(frm, key) { 464 // get all of the options associated with this product so we can 465 // find the matching data row in the product data table 466 var options = MUI.AddItem.getOptionsForProduct(frm, key); 467 468 var row = undefined; 469 if (options.length > 0) { 470 // get the current selections 471 var keys = []; 472 $.each(options, function (idx, o) { 473 keys.push($(o).val()); 474 }); 475 476 var pdt = MUI.AddItem.getProductDataTable(key); 477 if (pdt !== undefined) { 478 if (keys.length > 0) { 479 row = pdt.getRowByMatchKeys(keys); 480 } 481 } 482 } 483 484 return row; 485 }, 486 487 // Gets the data table 488 // uses underscore 489 getProductDataTable: function (pkey) { 490 return _.find(MUI.AddItem.dataTables, function (t) { return t.productKey === pkey; }); 491 } 492}; 493 494MUI.Assets = { 495 496 // gets a script ensuring it is only ever loaded once. 497 // currently used in loading Braintree scripts 498 // https://learn.jquery.com/code-organization/deferreds/examples/ 499 cachedGetScript: MUI.createCache(function(defer, url) { 500 $.getScript( url ).then( defer.resolve, defer.reject ); 501 }) 502 503}; 504 505//// A class to deal with basket JQuery functions 506//// This looks for a form with data attribute "data-muifrm='basket'" 507MUI.Basket = { 508 509 // initialize the basket 510 init: function() { 511 if (MUI.Settings.Endpoints.basketSurface === undefined || MUI.Settings.Endpoints.basketSurface === '') return; 512 513 var frm = $('[data-muifrm="basket"]'); 514 if (frm.length > 0) { 515 MUI.Basket.bind.form(frm[0]); 516 } 517 }, 518 519 // binds the form 520 bind: { 521 form: function(frm) { 522 523 // Watch for changes in the input fields 524 $(frm).find(':input[data-muiaction="updatequantity"]').change(function() { 525 var frmRef = $(this).closest('form'); 526 527 // post the form to update the basket quantities 528 var url = MUI.Settings.Endpoints.basketSurface + 'UpdateBasket'; 529 $.ajax({
530 type: 'POST', 531 url: url, 532 data: $(frmRef).serialize() 533 }).then(function(results) { 534 535 // update the line items sub totals 536 $.each(results.UpdatedItems, function(idx, item) { 537 var hid = $('input:hidden[value="' + item.Key + '"]') 538 if (hid.length > 0) { 539 //var subtotal = $(hid).closest('tr').find('[data-muivalue="linetotal"]'); 540 var subtotal = $(hid).closest('.row').find('[data-muivalue="linetotal"]'); 541 if (subtotal.length > 0) { 542 $(subtotal).html(item.FormattedTotal); 543 } 544 } 545 }); 546 547 // set the new basket total 548 var total = $(frmRef).find('[data-muivalue="total"]'); 549 if (total.length > 0) { 550 $(total).html(results.FormattedTotal); 551 } 552 553 // Emit the event so that labels can update if handled 554 MUI.emit('AddItem.added', results); 555 556 }, function(err) { 557 MUI.Logger.captureError(err); 558 }); 559 }); 560 561 562 var btn = $(frm).find('[data-muibtn="update"]'); 563 if (btn.length > 0) { 564 $(btn).hide(); 565 } 566 567 } 568 } 569}; 570 571MUI.Checkout = { 572 573 Payment: {}, 574 575 init: function() { 576 // Initialize the address module 577 MUI.Checkout.Address.init(); 578 // Initialize the payment module 579 MUI.Checkout.Payment.Braintree.init(); 580 } 581 582}; 583 584MUI.Forms = { 585 init: function() { 586 587 // add the custom validator for MVC unobtrusive validation 588 $.validator.addMethod('validateexpiresdate', function(value, element, params) { 589 590 // Make sure the value has a length of 5 591 if (value.length != 5) { 592 return false; 593 } 594 595 var today = new Date(); 596 var thisYear = today.getFullYear() - 2000; 597 var expMonth = +value.substr(0, 2); 598 var expYear = +value.substr(3, 4); 599 600 return (expMonth >= 1 601 && expMonth <= 12 602 && (expYear >= thisYear && expYear < thisYear + 20) 603 && (expYear == thisYear ? expMonth >= (today.getMonth() + 1) : true)); 604 605 }); 606 607 $.validator.unobtrusive.adapters.addBool('validateexpiresdate'); 608 609 }, 610 611 // Post a form and return the promise 612 post: function(frm, url) { 613 return MUI.resourcePromise(url, $(frm).serialize()); 614 }, 615 616 // rebinds a form unobtrusive validation 617 rebind: function(frm) { 618 $.validator.unobtrusive.parse(frm); 619 }, 620 621 // validates the form 622 validate: function(frm) { 623 624 // obtain validator 625 var validator = $(frm).validate(); 626 627 var isValid = true; 628 $(frm).find("input").each(function () { 629 630 // validate every input element inside this step 631 if (!validator.element(this)) { 632 isValid = false; 633 } 634 }); 635 636 return isValid; 637 }, 638 639 // tests a string to see if it is in a valid email format 640 isValidEmail: function(email) { 641 var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 642 return re.test(email); 643 } 644}; 645 646//// A class to assist in updating labels in the UI 647//// This looks for a form with data attribute "data-muilabel='VALUE'" 648MUI.Labels = { 649 650 init: function() { 651 // Event listener 652 MUI.on('AddItem.added', MUI.Labels.update.basketItemCount); 653 }, 654 655 update: { 656 basketItemCount: function(evt, args) { 657 if (args.Success) { 658 var labelCount = $('[data-muilabel="basketcount"]'); 659 var labelPrice = $('[data-muilabel="basketprice"]'); 660 if (labelCount.length > 0) { 661 $(labelCount).html(args.ItemCount); 662 //$(labelPrice).html(args.formattedSalePrice); 663 $(labelPrice).html(args.FormattedTotal); 664 } 665 } 666 } 667 } 668}; 669 670MUI.Notify = { 671 672 types: [ 673 'success', 674 'info', 675 'error', 676 'warn' 677 ], 678 679 // Value to check to see if notifications are enabled and the bar has been appended to the page 680 enabled: false, 681 682 bar: undefined, 683 684 overlay: undefined, 685
686 hasOverlay: false, 687 688 // initializes the Notify class 689 init: function() { 690 MUI.Notify.appendNotifyBar(); 691 }, 692 693 toggleOverlay: function() { 694 if (MUI.Notify.hasOverlay) { 695 var overlay = MUI.Notify.overlay; 696 $(overlay).toggle(); 697 } 698 }, 699 700 701 // renders an info message 702 info: function(msg) { 703 MUI.Notify.message(msg, 'info', 1000); 704 }, 705 706 // renders a success message 707 success: function(msg) { 708 MUI.Notify.message(msg, 'success', 500); 709 }, 710 711 // renders an error message 712 error: function(msg) { 713 MUI.Notify.message(msg, 'error', 1000); 714 }, 715 716 // renders a warning message 717 warn: function(msg) { 718 MUI.Notify.message(msg, 'warn', 750); 719 }, 720 721 // renders a message 722 message: function(msg, type, delay) { 723 if (MUI.Notify.bar !== undefined && MUI.Notify.enabled) { 724 725 if(delay === undefined) delay = 500; 726 727 var bar = MUI.Notify.bar; 728 var container = $(bar).find('[data-muivalue="nofity"]'); 729 if (container.length > 0) { 730 731 // removes previous class from notify bar 732 reset(); 733 // get the current css class 734 if (type === undefined) type = 'info'; 735 var ref = MUI.Notify.getClassRef(type); 736 var css = MUI.Notify.getCssClass(ref); 737 $(bar).addClass(css); 738 $(container).html(msg); 739 $(bar).fadeIn().delay(delay).fadeOut(); 740 } 741 } 742 743 // removes all the css classes 744 function reset() { 745 var bar = MUI.Notify.bar; 746 var refs = MUI.Notify.getClassRef('all'); 747 $.each(refs, function(ref) { 748 var css = MUI.Notify.getCssClass(ref); 749 $(bar).removeClass(css); 750 }); 751 } 752 }, 753 754 755 getCssClass: function(ref) { 756 try { 757 var css = MUI.Settings.Notifications[ref]; 758 if (css === undefined) { 759 MUI.Logger.captureMessage('Failed to find CSS class for ' + ref + '. Returning alert-info'); 760 return 'alert-info'; 761 } else { 762 return css; 763 } 764 } catch(err) { 765 MUI.Logger.captureError(err); 766 } 767 }, 768 769 getClassRef: function (type) { 770 if (type === 'all') { 771 var refs = []; 772 $.each(MUI.Notify.types, function(t) { 773 refs.push(t + 'Css'); 774 }); 775 return refs; 776 } else { 777 var found = _.find(MUI.Notify.types, function(t) { return t === type }); 778 return found === undefined ? 'infoCss' : type + 'Css'; 779 } 780 }, 781 782 // Appends the notify bar to the bottom of the current page 783 appendNotifyBar: function() { 784 if (MUI.Settings.Notifications.enabled !== undefined && 785 MUI.Settings.Notifications.enabled === true && 786 MUI.Settings.Notifications.template !== undefined && 787 MUI.Settings.Notifications.template !== '') { 788 789 // ensure not exists 790 if($('[data-muinotify="notifybar"]').length === 0) 791 { 792 var div = MUI.Settings.Notifications.template; 793 794 $('body').append(div); 795 MUI.Notify.bar = $('[data-muinotify="notifybar"]'); 796 $(MUI.Notify.bar).hide(); 797 MUI.Notify.enabled = true; 798 } 799 } 800 801 802 if (MUI.Settings.Notifications.enabled !== undefined && 803 MUI.Settings.Notifications.enabled === true && 804 MUI.Settings.Notifications.overlay !== undefined && 805 MUI.Settings.Notifications.overlay !== '') { 806 807 if($('[data-muinotify="overaly"]').length == 0) { 808 var div = MUI.Settings.Notifications.overlay; 809 810 $('body').append(div); 811 MUI.Notify.overlay = $('[data-muinotify="overlay"]'); 812 $(MUI.Notify.overlay).hide(); 813 MUI.Notify.hasOverlay = true; 814 } 815 } 816 } 817}; 818 819MUI.Utilities = { 820 // Ensures a null or undefined value has either a value or a default value 821 // defaultValue itself defaults to an empty string. 822 EnsureNullAsValue: function(value, defaultValue) { 823 if (defaultValue === undefined) { 824 defaultValue = ''; 825 } 826 827 return value === null || value === undefined ? defaultValue : value; 828 }, 829 830 // Gets a query string parameter value 831 // http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript 832 getQueryStringValue: function(name, url) { 833 if (!url) url = window.location.href; 834 url = url.toLowerCase(); // This is just to avoid case sensitiveness 835 name = name.replace(/[\[\]]/g, "\\$&").toLowerCase();// This is just to avoid case sensitiveness for query parameter name 836 var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), 837 results = regex.exec(url); 838 if (!results) return undefined; 839 if (!results[2]) return ''; 840 return decodeURIComponent(results[2].replace(/\+/g, " "));
841 }, 842 843 844 // Credit card types 845 // Adapted from https://github.com/braintree/credit-card-type 846 CardTypes : { 847 848 types: [ 849 { 850 niceType: 'Visa', 851 type: 'visa', 852 pattern: '^4\\d*$', 853 gaps: [4, 8, 12], 854 lengths: [16], 855 code: { 856 name: 'CVV', 857 size: 3 858 } 859 }, 860 { 861 niceType: 'MasterCard', 862 type: 'master-card', 863 pattern: '^5[1-5]\\d*$', 864 gaps: [4, 8, 12], 865 lengths: [16], 866 code: { 867 name: 'CVC', 868 size: 3 869 } 870 }, 871 { 872 niceType: 'American Express', 873 type: 'american-express', 874 pattern: '^3[47]\\d*$', 875 isAmex: true, 876 gaps: [4, 10], 877 lengths: [15], 878 code: { 879 name: 'CID', 880 size: 4 881 } 882 }, 883 { 884 niceType: 'DinersClub', 885 type: 'diners-club', 886 pattern: '^3(0[0-5]|[689])\\d*$', 887 gaps: [4, 10], 888 lengths: [14], 889 code: { 890 name: 'CVV', 891 size: 3 892 } 893 }, 894 { 895 niceType: 'Discover', 896 type: 'discover', 897 pattern: '^6(011|5|4[4-9])\\d*$', 898 gaps: [4, 8, 12], 899 lengths: [16], 900 code: { 901 name: 'CID', 902 size: 3 903 } 904 }, 905 { 906 niceType: 'JCB', 907 type: 'jcb', 908 pattern: '^(2131|1800|35)\\d*$', 909 gaps: [4, 8, 12], 910 lengths: [16], 911 code: { 912 name: 'CVV', 913 size: 3 914 } 915 }, 916 { 917 niceType: 'UnionPay', 918 type: 'unionpay', 919 pattern: '^62\\d*$', 920 gaps: [4, 8, 12], 921 lengths: [16, 17, 18, 19], 922 code: { 923 name: 'CVN', 924 size: 3 925 } 926 }, 927 { 928 niceType: 'Maestro', 929 type: 'maestro', 930 pattern: '^(50|5[6-9]|6)\\d*$', 931 gaps: [4, 8, 12], 932 lengths: [12, 13, 14, 15, 16, 17, 18, 19], 933 code: { 934 name: 'CVC', 935 size: 3 936 } 937 } 938 ], 939 940 getCardType: function (cardNumber) { 941 var key, value; 942 var noMatch = {}; 943 944 if (!isString(cardNumber)) { 945 return noMatch; 946 } 947 948 for (key in types) { 949 if (!MUI.Utilities.CardTypes.hasOwnProperty(key)) { 950 continue; 951 } 952 953 value = MUI.Utilities.CardTypes.types[key]; 954 955 if (RegExp(value.pattern).test(cardNumber)) { 956 return clone(value); 957 } 958 } 959 960 return noMatch; 961 }, 962 963 getCardByType: function (type) { 964 return _.find(MUI.Utilities.CardTypes.types, function (t) { 965 return t.type === type; 966 }); 967 } 968 } 969}; 970 971//// A class to deal with basket JQuery functions 972//// This looks for a form with data attribute "data-muifrm='wishlist'" 973MUI.WishList = { 974 975 init: function() { 976 if (MUI.Settings.Endpoints.wishListSurface === undefined || MUI.Settings.Endpoints.wishListSurface === '') return; 977 978 var frm = $('[data-muifrm="wishlist"]'); 979 if (frm.length > 0) { 980 MUI.WishList.bind.form(frm[0]); 981 } 982 }, 983 984 bind: { 985 986 form: function(frm) { 987 // Watch for changes in the input fields 988 $(frm).find(':input[data-muiaction="updatequantity"]').change(function() { 989 var frmRef = $(this).closest('form'); 990 991 // post the form to update the basket quantities 992 var url = MUI.Settings.Endpoints.wishListSurface + 'UpdateWishList'; 993 $.ajax({
994 type: 'POST', 995 url: url, 996 data: $(frmRef).serialize() 997 }).then(function(results) { 998 999 // update the line items sub totals 1000 $.each(results.UpdatedItems, function(idx, item) { 1001 var hid = $('input:hidden[value="' + item.Key + '"]') 1002 if (hid.length > 0) { 1003 var subtotal = $(hid).closest('tr').find('[data-muivalue="linetotal"]'); 1004 if (subtotal.length > 0) { 1005 $(subtotal).html(item.FormattedTotal); 1006 } 1007 } 1008 }); 1009 1010 // set the new basket total 1011 var total = $(frmRef).find('[data-muivalue="total"]'); 1012 if (total.length > 0) { 1013 $(total).html(results.FormattedTotal); 1014 } 1015 1016 }, function(err) { 1017 MUI.Logger.captureError(err); 1018 }); 1019 }); 1020 1021 1022 var btn = $(frm).find('[data-muibtn="update"]'); 1023 if (btn.length > 0) { 1024 $(btn).hide(); 1025 } 1026 } 1027 } 1028 1029} 1030 1031//// Cart model 1032MUI.AddItem.ProductDataTable = function() { 1033 var self = this; 1034 self.productKey = ''; 1035 self.rows = []; 1036}; 1037 1038/// Cart Model Prototypes 1039// ProductDataTable 1040MUI.AddItem.ProductDataTable.prototype = (function() { 1041 1042 // used to match a pricing row with the drop down selection 1043 function getRowByMatchKeys(keys) { 1044 var row = _.find(this.rows, function (r) { 1045 var test = _.intersection(r.matchKeys, keys); 1046 return test.length === keys.length; 1047 }); 1048 return row; 1049 } 1050 1051 return { 1052 getRowByMatchKeys: getRowByMatchKeys 1053 }; 1054 1055}()); 1056 1057MUI.AddItem.ProductDataTableRow = function() { 1058 var self = this; 1059 self.productKey = '', 1060 self.productVariantKey = '', 1061 self.sku = ''; 1062 self.isForVariant = false; 1063 self.onSale = false; 1064 self.salePrice = 0; 1065 self.salePriceIncVat = 0; 1066 self.price = 0; 1067 self.priceIncVat = 0; 1068 self.isAvailable = true; 1069 self.matchKeys = []; 1070 self.inventoryCount = 0; 1071 self.outOfStockPurchase = false; 1072}; 1073 1074MUI.Checkout.Address = { 1075 1076 addressType: '', 1077 1078 init: function() { 1079 if (MUI.Settings.Endpoints.countryRegionApi === undefined || MUI.Settings.Endpoints.countryRegionApi === '') return; 1080 1081 var frm = $('[data-muistage="BillingAddress"]'); 1082 1083 if (frm.length > 0) { 1084 MUI.Checkout.Address.addressType = "Billing"; 1085 MUI.Checkout.Address.bind.form(frm[0]); 1086 var countryddl = frm.find(':input[data-muiaction="populateregion"]'); 1087 //if country not selected, set to default 1088 if (countryddl.val().length == 0) { 1089 if (MUI.Settings.Defaults.BillingCountryCode.length > 0) { 1090 countryddl.val(MUI.Settings.Defaults.BillingCountryCode); 1091 } 1092 } 1093 MUI.Checkout.Address.populateRegion(countryddl); 1094 1095 } else { 1096 frm = $('[data-muistage="ShippingAddress"]'); 1097 if (frm.length > 0) { 1098 MUI.Checkout.Address.addressType = "Shipping"; 1099 MUI.Checkout.Address.bind.form(frm[0]); 1100 var countryddl = frm.find(':input[data-muiaction="populateregion"]'); 1101 //if country not selected, set to default 1102 if (countryddl.val().length == 0) { 1103 if (MUI.Settings.Defaults.ShippingCountryCode.length > 0) { 1104 countryddl.val(MUI.Settings.Defaults.ShippingCountryCode); 1105 } 1106 } 1107 MUI.Checkout.Address.populateRegion(countryddl); 1108 } 1109 } 1110 }, 1111 1112 bind: { 1113 1114 form: function (frm) { 1115 // Watch for changes in the input fields 1116 $(frm).find(':input[data-muiaction="populateregion"]').change(function () { 1117 MUI.Checkout.Address.populateRegion($(this)); 1118 }); 1119 1120 $(frm).find(':input[data-muiaction="updateregion"]').change(function () { 1121 var frmRef = $(this).closest('form'); 1122 //keep the select list and the region textbox in sync 1123 frmRef.find(':input[data-muivalue="region"]').val(frmRef.find(':input[data-muiaction="updateregion"]').val()); 1124 }); 1125 } 1126 }, 1127 1128 populateRegion: function (countryCode) { 1129 var frmRef = countryCode.closest('form'); 1130 // post the country to get the regions for that country 1131 var url = MUI.Settings.Endpoints.countryRegionApi + 'PostGetRegionsForCountry?countryCode=' + countryCode.val(); 1132 $.ajax({
1133 type: 'POST', 1134 url: url 1135 }).then(function (results) { 1136 var regionddl = frmRef.find(':input[data-muiaction="updateregion"]'); 1137 var regiontb = frmRef.find(':input[data-muivalue="region"]'); 1138 if (results.length > 0) { 1139 //remove all but the first option 1140 $('option', regionddl).not(':eq(0)').remove(); 1141 $.each(results, function (idx, item) { 1142 regionddl.append($("<option></option>").attr("value", item.code).text(item.name)); 1143 }); 1144 //if region already defined and is present in the drop down, select it 1145 if (regiontb.val().length > 0 && regionddl.find('option[value="' + regiontb.val() + '"]').length > 0) {//and it exists in the drop down) { 1146 //set ddl to current value 1147 regionddl.val(regiontb.val()); 1148 } 1149 else { 1150 regiontb.val(''); 1151 } 1152 //show region ddl and hide region textbox 1153 regionddl.show(); 1154 regiontb.hide(); 1155 //$('#UseForShipping').attr('disabled', false); 1156 } 1157 else { 1158 //hide region ddl and show region textbox 1159 regionddl.hide(); 1160 regiontb.val('').show(); 1161 //also, if there is no region then billing and shipping must be different?? 1162 //$('#UseForShipping').prop('checked', false).attr('disabled', true); 1163 } 1164 }, function (err) { 1165 MUI.Logger.captureError(err); 1166 }); 1167 } 1168 1169 1170}; 1171 1172//// A class to manage braintree payments 1173MUI.Checkout.Payment.Braintree = { 1174 1175 invoiceKey: '', 1176 1177 token: '', 1178 1179 events : [ 1180 { alias: 'btsuccess', name: 'Braintree.success' } 1181 ], 1182 1183 // initialize payment form 1184 init: function() { 1185 var btforms = MUI.Checkout.Payment.Braintree.getBraintreeForm(); 1186 if ($(btforms).length > 0) { 1187 if (!MUI.Services.Braintree.initialized) { 1188 MUI.Services.Braintree.loadAssets(function() { 1189 MUI.Checkout.Payment.Braintree.bind.allForms(); 1190 }); 1191 } else { 1192 MUI.Checkout.Payment.Braintree.bind.allForms(); 1193 } 1194 } 1195 }, 1196 1197 bind: { 1198 1199 allForms: function() { 1200 1201 if($('.paypal-button').length > 0) { 1202 // binds the PayPal on time transaction 1203 MUI.Checkout.Payment.Braintree.bind.btpaypal.init(); 1204 } else { 1205 // binds the standard transaction 1206 MUI.Checkout.Payment.Braintree.bind.btstandard.init(); 1207 } 1208 }, 1209 1210 btstandard: { 1211 1212 init: function() { 1213 MUI.debugConsole('Initializing Braintree CC form'); 1214 1215 var form = MUI.Checkout.Payment.Braintree.getBraintreeForm(); 1216 1217 if (form.length > 0) { 1218 1219 1220 var token = MUI.Checkout.Payment.Braintree.getBraintreeToken(); 1221 1222 braintree.client.create({ 1223 authorization: token 1224 }, function (err, clientInstance) { 1225 if (err) { 1226 console.error(err); 1227 return; 1228 } 1229 braintree.hostedFields.create({ 1230 client: clientInstance, 1231 styles: { 1232 'input': { 1233 'font-size': '14px', 1234 'font-family': 'helvetica, tahoma, calibri, sans-serif', 1235 'color': '#3a3a3a' 1236 }, 1237 ':focus': { 1238 'color': 'black' 1239 } 1240 }, 1241 fields: { 1242 number: { 1243 selector: '#card-number', 1244 placeholder: '4111 1111 1111 1111' 1245 }, 1246 cvv: { 1247 selector: '#cvv', 1248 placeholder: '123' 1249 }, 1250 expirationMonth: { 1251 selector: '#expiration-month', 1252 placeholder: 'MM' 1253 }, 1254 expirationYear: { 1255 selector: '#expiration-year', 1256 placeholder: 'YY' 1257 }, 1258 postalCode: { 1259 selector: '#postal-code', 1260 placeholder: '90210' 1261 } 1262 } 1263 }, function (err, hostedFieldsInstance) { 1264 if (err) { 1265 console.error(err); 1266 return; 1267 } 1268 1269 hostedFieldsInstance.on('validityChange', function (event) { 1270 var field = event.fields[event.emittedBy]; 1271 1272 if (field.isValid) { 1273 if (event.emittedBy === 'expirationMonth' || event.emittedBy === 'expirationYear') { 1274 if (!event.fields.expirationMonth.isValid || !event.fields.expirationYear.isValid) { 1275 return; 1276 } 1277 } else if (event.emittedBy === 'number') { 1278 $('#card-number').next('span').text(''); 1279 } 1280 1281 // Apply styling for a valid field 1282 $(field.container).parents('.form-group').addClass('has-success'); 1283 } else if (field.isPotentiallyValid) { 1284 // Remove styling from potentially valid fields 1285 $(field.container).parents('.form-group').removeClass('has-warning'); 1286 $(field.container).parents('.form-group').removeClass('has-success'); 1287 if (event.emittedBy === 'number') { 1288 $('#card-number').next('span').text(''); 1289 } 1290 } else {
1291 // Add styling to invalid fields 1292 $(field.container).parents('.form-group').addClass('has-warning'); 1293 // Add helper text for an invalid card number 1294 if (event.emittedBy === 'number') { 1295 $('#card-number').next('span').text('Looks like this card number has an error.'); 1296 } 1297 } 1298 }); 1299 1300 hostedFieldsInstance.on('cardTypeChange', function (event) { 1301 // Handle a field's change, such as a change in validity or credit card type 1302 if (event.cards.length === 1) { 1303 $('#card-type').text(event.cards[0].niceType); 1304 } else { 1305 $('#card-type').text('Card'); 1306 } 1307 }); 1308 1309 $('.panel-body').submit(function (event) { 1310 event.preventDefault(); 1311 hostedFieldsInstance.tokenize(function (err, payload) { 1312 if (err) { 1313 console.error(err); 1314 return; 1315 } 1316 1317 var data = { nonce: payload.nonce }; 1318 var method = 'Process'; 1319 1320 if (MUI.Checkout.Payment.Braintree.invoiceKey !== '') { 1321 data.invoiceKey = MUI.Checkout.Payment.Braintree.invoiceKey; 1322 method = 'Retry'; 1323 } 1324 1325 var url = MUI.Settings.Endpoints.braintreeStandardCcSurface + method; 1326 1327 MUI.Checkout.Payment.Braintree.postBraintreeForm(url, data); 1328 }); 1329 }); 1330 }); 1331 }); 1332 } 1333 } 1334 }, 1335 1336 // PayPal payments through Braintree 1337 btpaypal: { 1338 1339 init: function() { 1340 MUI.debugConsole('initializing PayPal OneTime Transaction'); 1341 1342 var token = MUI.Checkout.Payment.Braintree.getBraintreeToken(); 1343 1344 var paypalButton = document.querySelector('.paypal-button'); 1345 1346 // Create a client. 1347 braintree.client.create({ 1348 authorization: token 1349 }, function (clientErr, clientInstance) { 1350 1351 // Stop if there was a problem creating the client. 1352 // This could happen if there is a network error or if the authorization
1353 // is invalid. 1354 if (clientErr) { 1355 console.error('Error creating client:', clientErr); 1356 return; 1357 } 1358 1359 // Create a PayPal component. 1360 braintree.paypal.create({ 1361 client: clientInstance 1362 }, function (paypalErr, paypalInstance) { 1363 1364 // Stop if there was a problem creating PayPal. 1365 // This could happen if there was a network error or if it's incorrectly 1366 // configured. 1367 if (paypalErr) { 1368 console.error('Error creating PayPal:', paypalErr); 1369 return; 1370 } 1371 1372 // Enable the button. 1373 paypalButton.removeAttribute('disabled'); 1374 1375 // When the button is clicked, attempt to tokenize. 1376 paypalButton.addEventListener('click', function (event) { 1377 1378 // Because tokenization opens a popup, this has to be called as a result of 1379 // customer action, like clicking a buttonâyou cannot call this at any time. 1380 paypalInstance.tokenize({ 1381 flow: 'vault' 1382 }, function (tokenizeErr, payload) { 1383 1384 // Stop if there was an error. 1385 if (tokenizeErr) { 1386 if (tokenizeErr.type !== 'CUSTOMER') { 1387 console.error('Error tokenizing:', tokenizeErr); 1388 } 1389 return; 1390 } 1391 1392 // Tokenization succeeded! 1393 paypalButton.setAttribute('disabled', true); 1394 1395 if (MUI.Settings.Endpoints.braintreePayPalSurface !== '') 1396 { 1397 var method = 'Process'; 1398 var data = { nonce: payload.nonce }; 1399 if (MUI.Checkout.Payment.Braintree.invoiceKey !== '') { 1400 method = 'Retry'; 1401 data.invoiceKey = MUI.Checkout.Payment.Braintree.invoiceKey; 1402 } 1403 1404 var url = MUI.Settings.Endpoints.braintreePayPalSurface + method; 1405 MUI.Checkout.Payment.Braintree.postBraintreeForm(url, data); 1406 } 1407 }); 1408 1409 }, false); 1410 }); 1411 1412 }); 1413 } 1414 1415 } 1416 }, 1417 1418 postBraintreeForm: function(url, data) { 1419 MUI.Notify.toggleOverlay(); 1420 $.ajax({
1421 url: url, 1422 type: 'POST', 1423 data: data 1424 }).then(function(result) { 1425 MUI.Checkout.Payment.Braintree.handlePaymentResult(result, 'Braintree.success'); 1426 }, function(err) { 1427 MUI.Checkout.Payment.Braintree.handlePaymentException(err); 1428 }); 1429 }, 1430 1431 handlePaymentResult: function(result, evt) { 1432 MUI.Notify.toggleOverlay(); 1433 if (result.Success) { 1434 MUI.emit('AddItem.added', result); 1435 MUI.emit(evt, result); 1436 MUI.Notify.success(result.PaymentMethodName + ' Success!'); 1437 } else { 1438 console.info(result); 1439 MUI.Notify.warn(result.PaymentMethodName + ' Error!') 1440 } 1441 }, 1442 1443 handlePaymentException: function(err) { 1444 MUI.Notify.toggleOverlay(); 1445 MUI.Notify.error('There was an error'); 1446 MUI.Logger.captureError(err); 1447 }, 1448 1449 getBraintreeToken: function() { 1450 var frm = MUI.Checkout.Payment.Braintree.getBraintreeForm(); 1451 var token = $(frm).find('#Token'); 1452 return $(token).val(); 1453 }, 1454 1455 getBraintreeForm: function() { 1456 return $('[data-muiscript="braintree"]'); 1457 } 1458}; 1459 1460// put prototype methods there 1461 1462// initialize the MUI object 1463MUI.init(); 1464 1465
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.