//======================================================================||//               NOP Design JavaScript Shopping Cart                    ||//                                                                      ||// For more information on SmartSystems, or how NOPDesign can help you  ||// Please visit us on the WWW at http://www.nopdesign.com               ||//                                                                      ||// Javascript portions of this shopping cart software are available as  ||// freeware from NOP Design.  You must keep this comment unchanged in   ||// your code.  For more information contact FreeCart@NopDesign.com.     ||//                                                                      ||// JavaScript Shop Module, V.4.4.0                                      ||//                                                                      ||// This nopERcart version by Eugene Reimer, Version 2008-08-04,         ||// ------------------------------------------------------------         ||// has                                                                  ||//   quantity-based discount pricing;                                   ||// does                                                                 ||//   shipping-calculations by Size & Weight as well as by Zone,         ||//   alterable for retailer-location and package-deliverer,             ||//   as well as for the items being offered for sale,                   ||//   WITHOUT any programming;                                           ||// detects situations where breaking up a parcel into several smaller   ||//   packages lowers the shipping-cost, which is useful, for example,   ||//   with Canada-Post's pricing;                                        ||// supports 2 kinds of sales-tax, with different exceptions,            ||//   with up to three regions of applicability by customer location,    ||//   the two taxes can be shown on separate lines,                      ||//   taxes can also be shown included in prices, yet itemized;          ||//   everything that's needed in Canada, Australia, Europe, etc;        ||// can handle any currency,                                             ||//   whether the smallest actual-unit is thousandths or thousands, etc  ||//   (although not the old British Pound-Shilling-Pence notation);      ||// supports Checkout via:                                               ||//   PayPal,                                                            ||//   Google-Checkout,                                                   ||//   as well as the 3 methods from the original version;                ||// supports ONE-step checkout,                                          ||//   as well as the TWO-step checkout from the original version;        ||// uses:                                                                ||//   parts of UPS-Shipping-by-Weight by Stefko, Ver1.3 (12-20-03);      ||// for an example, see:                                                 ||//   http://www.nativeorchid.org/shopping.htm;                          ||// for information on this nopERcart version,                           ||//   see http://ereimer.net/nopercart.htm                               ||//   or contact  ereimer@shaw.ca.                                       ||//                                                                      ||// Copyright (c) 2007,2008 NopDesign.com, Stefko, Eugene Reimer;        ||// can be freely used, modified, copied, distributed, sold, etc,        ||// under the terms of either the LGPL or the GPL (your choice);         ||// see http://www.gnu.org/licenses for the details of these terms.      ||//                                                                      ||//======================================================================||//----------------------------------------------------------------------||//                      Shopping-Cart Options                           ||//                      =====================                           ||// You can modify these options to change the way the cart functions.   ||//                                                                      ||// Language Packs                                                       ||// ==============                                                       ||// You may include a language-pack along with nopercart.js in your      ||// HTML pages to change the language.  For example:                     ||//      <SCRIPT SRC="noper-language-fr.js"></SCRIPT>                    ||//      <SCRIPT SRC="nopercart.js"></SCRIPT>                            ||//                                                                      ||// Many different language packs are provided together with this        ||// software. Note: I'm far from fluent in (most of) those languages.    ||// If you construct a new one OR make improvements to one of the        ||// supplied ones, please email the result to  ereimer@shaw.ca           ||// so it can be included with this software; you will be credited by    ||// name, and also email-address, website, etc, if you wish.             ||//                                                                      ||// Options For Everyone:                                                ||// =====================                                                ||// * MoneySymbol: string, the symbol which represents dollars/euros/etc ||//   in your locale.                                                    ||// * DisplayPopupOnAdd: true/false, controls whether the user is        ||//   provided with a popup when adding to the cart.                     ||// * DisplayPopupOnRemove: true/false, controls whether the user is     ||//   provided with a popup when removing from the cart.                 ||// * DisplayChangeQty: true/false; whether user permitted to change     ||//   Qty on the viewcart page.                                          ||// * DisplayWtColumn: true/false, controls whether the viewcart         ||//   page (initially) displays shipping-weight column.                  ||// * DisplaySzColumn: true/false, controls whether the viewcart         ||//   page (initially) displays shipping-size column.                    ||// * DynamicWtSzColumns: number, possible values are:                   ||//     0 = avoid using More-Info, Less-Info buttons;                    ||//     1 = the MoreInfo-state displays the Size-column;                 ||//     2 = the MoreInfo-state displays the Weight-column;               ||//     3 = the MoreInfo-state displays both Weight & Size columns;      ||//   nonzero means there'll be either a More-Info or Less-Info button   ||//   on the PkgAttr row & so requires that DisplayPkgAttrRow be true;   ||//   the Less-Info-state displays neither weight nor size column.       ||// * WTUNITS: string, units for item & package weights, eg: "lb".       ||// * SZUNITS: string, units for item & package sizes, eg "cm".          ||// * WTROUND: number; package-weight will be rounded-up to multiple of  ||//   1/WTROUND;  use 1 for weight in grams, perhaps 100 for pounds.     ||// * SZROUND: number; package-length,width,height will each be rounded  ||//   to multiple of 1/SZROUND.                                          ||// * MoneyPLACES: number, controls rounding & display of money-amounts; ||//   use 2 for US-dollars rounded to cents, shown as dollars and cents; ||//   or -3 if amounts are to be rounded to multiples of 1000, and       ||//   shown without any fractional-part.                                 ||// * DisplayPkgAttrRow: true/false; controls whether the viewcart       ||//   page shows the total size & weight line.                           ||// * DisplayShippingRow: true/false, controls whether the viewcart      ||//   and checkout pages display a shipping-cost line at end of table.   ||// * DisplayTaxRow: true/false, controls whether the viewcart           ||//   and checkout pages display tax-subtotal line(s) - whether one or   ||//   two lines depends the TaxNames option.                             ||// * DisplayTaxIncluded: true/false; if true, then item prices are      ||//   shown with taxes included, and the tax-subtotal lines are shown    ||//   after the Total line, with the phrase "included in total".         ||//   Note: if you use this tax-included pricing, then consider having   ||//   a note on your View-Cart page advising a customer who needs the    ||//   tax details to "print this page before checking out"  (because the ||//   statement from the payment-processor will be inadequate WRT the    ||//   tax details).                                                      ||// * TaxNames: array of string; use this to give names to your taxes    ||//   if you have two kinds, in which case DisplayTaxRow will show them  ||//   on TWO lines;  example: TaxNames = ["PST","GST"];                  ||// * TaxRateRegional: number, the tax-rate for a tax that applies to    ||//   customers in Region#0.  Used when you need to charge a tax to      ||//   those in the same state/province you're in, but not to others.     ||//   A nonzero value may lead to your customers being prompted about    ||//   whether or not they live in your region, however such prompting    ||//   can be reduced or eliminated  - see the RegionFromZone,            ||//   RegionDefault, RegionPrompt options.                               ||// * TaxRate: number, the tax-rate for a tax that applies to customers  ||//   in both Region#0 and Region#1;  use TaxRate=0.075 for a 7.5% tax;  ||//   Note: when TaxRateRegional is zero, then TaxRate applies to all    ||//   customers.                                                         ||//   Note: Both TaxRate and TaxRateRegional will be nonzero for most    ||//   Canadian, Australian, European retailers, at most one will be for  ||//   US-Americans.                                                      ||//   TAX-EXCEPTIONS: the first letter of an item's ID is used to        ||//   indicate taxation-exceptions, as follows:                          ||//     "n" - non-taxable - neither tax applies to this item;            ||//     "p" - only the TaxRateRegional tax applies to this item;         ||//     "g" - only the TaxRate tax applies to this item;                 ||//     OTHER - both taxes apply to this item;                           ||//     -- those letters aren't hardcoded but are controlled by options: ||//     PrefNeitherTax, PrefRegionalOnly, PrefNationalOnly.              ||//   When both tax-rates are applicable, the combined-tax is computed   ||//   as Amount TIMES (TaxRate+TaxRateRegional).  Does anyone need       ||//   two kinds of tax with one going on top of the other?               ||// * (the TaxByRegion option has been discarded in this version;        ||//   a nonzero TaxRateRegional conveys the same information.)           ||// * RegionFromZone: array, mapping from Zone to array of Regions legal ||//   for that Zone, the first being the default for that Zone;          ||//   for example, use [[0,1],[2]] if someone in your Zone#0 is in       ||//   Region #0 or #1, someone in Zone#1 is in Region#2;                 ||//   or use [[1,0],[2]] to make Region#1 the default for Zone#0;        ||//   use [] to disable the validity-checking it offers.                 ||// * RegionDefault: number, controls how sales-tax calculations are     ||//   done when user hasn't yet indicated which Region he/she is in;     ||//   0 means Region#0, 1 means Region#1, 2 means Region#2;              ||//   this option is ignored when RegionFromZone is being used.          ||// * RegionPrompt: string, with which to prompt a user who has not yet  ||//   indicated which Region he/she is in;                               ||//   use the empty-string to suppress such a prompt, in which case      ||//   such user will be charged sales-tax using the defaults.            ||// * RegionTable: replaces the TaxableRegion, NonTaxableRegion options; ||//   example: ["Manitoba Resident", "Other Canadian", "Other Country"]; ||//   use [TaxableRegion, NonTaxableRegion] for compatibility with the   ||//   original NopDesign version;  if TaxRateRegional is zero, this      ||//   option is ignored.  if TaxRateRegional is nonzero, this array can  ||//   have either 2 or 3 entries: use 2 regions if your TaxRate applies  ||//   to all customers; use 3 regions if you need a third region that's  ||//   excempt from both taxes.                                           ||// * MinimumDonation: number, the minimum money-amount you're willing   ||//   to accept as a donation;  example: a Canadian Charity using PayPal ||//   needs a 3.50 minimum to ensure compliance with the 80%-rule.       ||// * MinimumDonationPrompt: string, with which to prompt donor not      ||//   meeting the minimum-donation amount.                               ||// * MinimumOrder: number, the minimum money-amount that must be        ||//   purchased before a user is allowed to checkout.  Set it to 0.01    ||//   to disable this minimum, and yet prevent a completely empty cart   ||//   being sent to your payment-processing.                             ||// * MinimumOrderPrompt: string, with which to prompt user who hasn't   ||//   met the minimum-order amount.                                      ||// * gcCurrency: string, currency-code to be passed to Google-Checkout  ||//   for each item;  ignored when PaymentProcessor is not "gc".         ||//                                                                      ||// Payment Processor Options:                                           ||// ==========================                                           ||// * PaymentProcessor: string, a short code for your payment-processor  ||//   gateway.  Set this option to one of:                               ||//     "an"  (Authorize.net)                                            ||//     "wp"  (Worldpay)                                                 ||//     "lp"  (LinkPoint)                                                ||//     "pp"  (PayPal)                                                   ||//     "ppa" (PayPal all-in-one)                                        ||//     "gc"  (GoogleCheckout)                                           ||//     ""    (custom HTML checkout-page)                                ||//     "cgi" (custom CGI/PHP/ASP checkout-script)                       ||//   When using "cgi", please review your OutputItem settings,          ||//   explained below under Options for Programmers.                     ||//                                                                      ||// * PaymentProcessor2: string, taking the same values as shown for     ||//   PaymentProcessor, but applies to CheckoutCart -- for those         ||//   wanting a two-step checkout-process.                               ||//   (TWO-Step-Checkout involves first an HTML checkout-page; and       ||//   that page invokes CheckoutCart.)                                   ||//                                                                      ||// Options to Control the Shipping-Calculations:                        ||// =============================================                        ||// * ShipTable: array, explained below.                                 ||// * PackTable: array, explained below.                                 ||// * ZoneDefault: integer,  default entry-number in ShipTable;          ||//   usually set to the most expensive case, so that a careless         ||//   customer will pay at least enough.                                 ||// * ZonePrompt: string, with which to prompt a user who has not yet    ||//   indicated which Shipping-Zone he/she is in; use the empty-string   ||//   to suppress such a prompt, in which case such user will be         ||//   charged shipping according to ZoneDefault.                         ||// * ShipTaxRate: number, the tax-rate to be applied to shipping prices ||//   in ShipTable; use 0.05 if Canadian, 0.0 if US-American, or         ||//   whatever "VAT" applies to your shipping price.                     ||// * ShipTaxName: string, for showing the tax included in the shipping  ||//   amount;  use empty-string to not have that tax shown.              ||// * HandlingChargePerOrder: amount, handling-charge for the first      ||//   package in an order.                                               ||// * HandlingChargePerExtraPackage: amount, handling-charge for each    ||//   additional package in an order;  use a huge number if you prefer   ||//   to ship one order as a single package.                             ||//                                                                      ||// Options For Programmers:                                             ||// ========================                                             ||// * OutputItem<..>, OutputOrder<..>: string, the left-side of a pair   ||//   passed at checkouttime, if PaymentProcessorX is "cgi".  Change     ||//   these if your CGI script needs specific field names.               ||//   NOTE: used only when PaymentProcessorX=="cgi".                     ||// * AppendItemNumToOutput: true/false, if set to true, the number of   ||//   each ordered item will be appended to the output string.  For      ||//   example if OutputItemId is "ID_" and this is set to true, the      ||//   output field name will be "ID_1", "ID_2" ... for each item.        ||//   When in doubt use true.  This option must be true for at least     ||//   PaymentProcessorX=="pp"||"gc", likely others, perhaps all others?  ||// * Debug: integer, 255 for maximum debug-info, 256 for minimal,       ||//   zero for none.  (the integer is a bitstring.)                      ||//   NOTE: you no longer need to change this setting between 'test' and ||//   'production, because this cart now distinguishes between those     ||//   based on whether coming from a local hard-drive;  it will always   ||//   provide the basic sanity-test popups when local, and will          ||//   suppress Debug-output entirely when non-local.                     ||//   (because Debug-messages would not be helpful to your customers)    ||// * CartID: string, when running several carts on the same website,    ||//   giving each a distinct string prevents them sharing cookies.       ||//                                                                      ||// (The former HiddenFieldsToCheckout option has been discarded in      ||// this version, replaced by PaymentProcessorX=="cgi".)                 ||//----------------------------------------------------------------------||//                                                                      ||//                      Minification:                                   ||//                      =============                                   ||// You can obtain a slight performance-improvement by minifying the     ||// javascipt source-code; its size will be reduced by about 70%.        ||//                                                                      ||// To Minify, first rename this file to nopercart-readable.js,          ||// then use the following commandline:                                  ||//    jsminify <nopercart-readable.js >nopercart.js  "nopERcart Version 2008-08-04 -- readable copy at http://ereimer.net/nopercart.htm"  "(c) 2007,2008 NopDesign.com, Stefko, Eugene Reimer."//                                                                      ||// jsminify is available from  http://ereimer.net/jsminify  or use      ||// jsmin from  http://www.crockford.com/javascript/jsmin.html           ||//----------------------------------------------------------------------||//------------------------------------------------------------------------// Language Strings// ----------------// These strings will be used if you have not included a language-pack.// If using English, simply modify the strings below;// if using another language, then modify them in the file // noper-language-XX.js -- where XX is the language you are using.// (File noper-language-en.js has been discarded in this version.)//// If you construct a new one OR make improvements to one of the // supplied ones, please email the result to  ereimer@shaw.ca// so it can be included with this software, to help other users.//------------------------------------------------------------------------if(typeof strSorry == "undefined"){                                             //ER: make language-pack optional; was: if(!bLanguageDefined){   strSorry = "I'm Sorry, your cart is full;  please proceed to checkout.";   strAdded = "Added to your shopping cart:";   strAddedQuantity = "Quantity: ";                                             //ER: new;  needed by, but not added by, Stefko mods   strAddedProduct  = "Product:  ";                                             //ER: new;  needed by, but not added by, Stefko mods   strRemove = "Click 'OK' to remove this product from your shopping cart.";   strILabel = "Product ID &nbsp; &nbsp; ";   strDLabel = "Product Name";   strQLabel = "Qty";   strPLabel = "Price";   strWLabel = "Weight";                                                        //ER: new - for displaying Product WEIGHT in ManageCart;  replaces strSLabel   strZLabel = "Size";                                                          //ER: new - for displaying Product SIZE in ManageCart   strALabel = "Amount";                                                        //ER: new - replaces Stefko's "Ext." for displaying Price*Qty in CheckoutCart   strRLabel = "&nbsp;";                                                        //ER: was: "Remove from Cart"  (Netscape-4 needs NBSP rather than EMPTYSTRING)   strRButton= "Remove from Cart";                                              //ER: was: "Remove"   strMButton= "More Info";                                                     //ER: new;  for the DynamicWtSzColumns option   strLButton= "Less Info";                                                     //ER: new;  for the DynamicWtSzColumns option   strSUB = "SUBTOTAL";   strWTSZTOT = "PACKAGE ATTRIBUTES";                                           //ER: new;  Stefko had strWTOT="TOTAL WEIGHT";  but now it's for Weight+Size   strSHIP = "SHIPPING";   strTAX = "TAX";   strTOT = "TOTAL";   strErrQty = "Invalid Quantity.";   strNewQty = "Please enter new quantity:";   strSHIPPINGZONE = "SHIPPING<BR>ZONE";                                        //ER: new;  needed by, but not added by, Stefko mods            COND-DEMO1-NOCI-DEMO2   strTAXABLEREGION = "TAXABLE<BR>REGION";                                      //ER: new;  needed by my rewrite of the sales-tax-by-region code   strEA = "/ea";                                                               //ER: new;  needed by the original NopDesign version   strCartEmpty = "Your cart is empty";                                         //ER: new;  needed by the original NopDesign version   strAsMultiple = "as multiple packages:";                                     //ER: new;  for message from ComputeShipping   strAsSingle = "as-one:";                                                     //ER: new;  for message from ComputeShipping   strBroken="our shipping-calculator is broken; please inform our webmaster";  //ER: new;  message from ComputeShipping   strTotalNaN="Your browser's javascript appears to be broken; another browser may help; a reboot may help; if problem persists, please inform our webmaster";  //ER: new;  message from ValidateCart, when total is not a number   strINCLUDEDINTOTAL = "Included in Total";                                    //ER: new;  for the DisplayTaxIncluded option   Language = "en";}//Options for Programmers:OutputItemId         = "ID_";OutputItemQuantity   = "QUANTITY_";OutputItemPrice      = "PRICE_";OutputItemName       = "NAME_";         //2008-02-07: now includes what was formerly in AddtlInfoOutputItemWeight     = "WEIGHT_";       //Renamed by Stefko: Shipping-->WeightOutputItemLength     = "LENGTH_";       //ER: newOutputItemWidth      = "WIDTH_";        //ER: newOutputItemHeight     = "HEIGHT_";       //ER: new//tputItemAddtlInfo  = "ADDTLINFO_";    //2008-02-07: yanked, see OutputItemNameOutputOrderZone      = "SHIPZONE";      //Added by StefkoOutputOrderRegion    = "TAXREGION";     //ER: newOutputOrderSubtotal  = "SUBTOTAL";OutputOrderShipping  = "SHIPPING";OutputOrderTax       = "TAX";OutputOrderTotal     = "TOTAL";AppendItemNumToOutput= true;            //MUST be true for at least PayPal & Google-Checkout;  ER: suspect no-one ever uses false??  CartID             = "";              //empty-string is fine for the first cart on one website                                                COND-DEMO1-NOCIDebug                = 0;               //suppress DEBUG alertsfunction DEBUG(str)   {if(Debug)   alert(str);}         //any nonzero value produces the DEBUG (sanity-test) alertsfunction DEBUG1(str)  {if(Debug&1) alert(str);}         //the  1-bit controls DEBUG1  alertsfunction DEBUG2(str)  {if(Debug&2) alert(str);}         //the  2-bit controls DEBUG2  alertsfunction DEBUG4(str)  {if(Debug&4) alert(str);}         //the  4-bit controls DEBUG4  alertsfunction DEBUG8(str)  {if(Debug&8) alert(str);}         //the  8-bit controls DEBUG8  alertsfunction DEBUG16(str) {if(Debug&16)alert(str);}         //the 16-bit controls DEBUG16 alertsif(window.location.href.substring(0,5)=="file:") Debug|=256;  else Debug=0;     //2008-02-20: smart (Opera-like) behaviour wrt Debug-alerts...//Options for Everyone:  MoneySymbol          = "$";           //use dollar-symbol                                                                                     COND-DEMO1-NOCI-DEMO2  DisplayPopupOnAdd    = true;          //activate "add to cart" popups                                                                         COND-DEMO2-DEMO3  DisplayPopupOnRemove = false;         //suppress "remove from cart" popups  (not suppressable in NopDesign version)  DisplayChangeQty     = true;          //activate changeable quantity-field                                                                    COND-DEMO2-DEMO3  DisplayWtColumn      = false;         //don't show a Shipping-WEIGHT column in ManageCart                                                     COND-DEMO1-NOCI-DEMO2-DEMO4  DisplaySzColumn      = false;         //don't show a Shipping-SIZE column in ManageCart                                                       COND-DEMO1-NOCI-DEMO2-DEMO4  DynamicWtSzColumns   = 2;             //a clickable "More Info" button, to have Shipping-WEIGHT column shown                                  COND-DEMO2  WTUNITS              = "lb";          //pounds                                                                                                COND-DEMO2  SZUNITS              = "in";          //units for Item & Package SIZEs                                                                        COND-DEMO2  WTROUND              = 100;           //want Package-Weight rounded-up to multiple of 0.01                                                    COND-DEMO2  SZROUND              = 10;            //Package-Length,Width,Height rounded to multiple of 0.1  MoneyPLACES          = 2;             //currency needs two decimal places as in dollars and cents  DisplayPkgAttrRow    = false;          //show the total size & weight  DisplayShippingRow   = true;          //show the shipping-cost-line  DisplayTaxRow        = false;          //show the tax-line                                                                                     COND-DEMO1-DEMO2-DEMO3  DisplayTaxIncluded   = false;         //do not show tax-included prices                                                                       COND-DEMO1-NOCI-DEMO2-DEMO4  TaxNames             = ["PST","GST"]; //names for the two sales-taxes in Canada                                                               COND-DEMO1-NOCI  TaxRate              = 0.00;          //charge 5% sales-tax to those in Regions #0 and #1 (Canadian GST; was 6%)                              COND-DEMO1  TaxRateRegional      = 0.00;          //charge 7% provincial-sales-tax to residents of Region#0                                               COND-DEMO1  RegionTable          = ["Manitoba", "Other Canadian", "Other Country"];      //for 2 taxes in 3 Regions, 3rd being tax-exempt        COND-DEMO1-NOCI  RegionFromZone       = [[0,1],[2],[2]];               //customer in Zone#0 can be in Region#0 or 1;  Zone1 or 2 implies Region2               COND-DEMO1-NOCI  RegionDefault        = 0;                             //default to Region#0;  RegionPrompt         = "Please indicate whether you are a resident of Manitoba for tax purposes, before continuing";    //prompt for MB       COND-DEMO1-NOCI  DefaultDonation      = 0.00;          //default Donation-Amount for donor who provides an invalid amount                                      COND-DEMO1-DEMO3-DEMO4  MinimumDonation      = 0.00;          //minimum Donation-Amount                                                                               COND-DEMO1-DEMO2-DEMO3-DEMO4  MinimumDonationPrompt= "We're sorry but we're unable to accept a donation of less than 0.00"; //                                              COND-DEMO1-DEMO2-DEMO3-DEMO4  MinimumOrder         = 0.01;                                                                  //prevent empty-cart to checkout  MinimumOrderPrompt   = "Your cart is empty; please order something before checking out.";     //minimum-order prompt                          COND-DEMO1-NOCI-DEMO2-DEMO4  PrefDonation         = "nDO";         //donations have IDs beginning with nDO  PrefNeitherTax       = "n";           //products with neither sales-tax have IDs beginning with n  PrefRegionalOnly     = "p";           //products with regional-tax only have IDs beginning with p  PrefNationalOnly     = "g";           //products with national-tax only have IDs beginning with g  gcCurrency           = "USD";                                                                 //currency-code for Google-Checkout             COND-DEMO1-NOCI-DEMO2  ppNotesOnItem        = true;          //shipping-notes onto last item for PayPal (they're also in "custom" but that field is overly private)//Payment Processor Options:  PaymentProcessor     = "pp";          //payment-processor for ManageCart;  can be overridden in shoppingcart.htm                              COND-DEMO1-NOCI-DEMO3-DEMO4  PaymentProcessor2    = "cgi";         //payment-processor for CheckoutCart;  can be overridden in shoppingcheckout.htm//========================================================================// Shipping-Info Table// -------------------// Several examples are provided, to illustrate how you can go about // creating a table that describes shipping from your location, by the // package-deliverer you've chosen.  //// RESTRICTION: in the present version, the Length & Width must be the// same in all pkginfo-entries for one zone.//// NOTE:  "*" in last column (of PkgClass) indicates multiple of these // (or smaller) may cost less than shipping as one package, and the // shipping-calculator is to try it both ways;  this feature works best // when size matters and Packing-Rules are provided.// (!!future enhancement: program to derive the "*" info itself.)//========================================================================// ShipTable = [];                                                               //init array                                                    COND-DEMO2  ShipTable[0] = new ShipEntry("Within USA",       []);     //Zone#0 is within USA                             COND-DEMO2  ShipTable[1] = new ShipEntry("To Canada", []);     //Zone#1 is to Canada                          COND-DEMO2  ShipTable[2] = new ShipEntry("To Europe", []);     //Zone#2 is to Europe                 COND-DEMO2  ShipTable[3] = new ShipEntry("To Australia",               []);     //Zone#2 is to Australia                                   COND-DEMO2  anysize= new Size(9999,9999,9999);                                            //this retailer uses weight-only shipping                       COND-DEMO2  //--entry#0 Within-USA                                                                                                        COND-DEMO2  ShipTable[0].pkginfo[0]=new PkgClass(500, anysize,  5.00, 0.50, 1,  "");      //any size is $6.50 + $0.95 per pound                           COND-DEMO2  //--entry#1 To-Canada                                                                                                    COND-DEMO2  ShipTable[1].pkginfo[0]=new PkgClass(500, anysize, 7.00, 0.50, 1,  "");      //any size is 33.40 + 3.80 per pound                            COND-DEMO2  //--entry#2 To-Europe                                                                                                         COND-DEMO2  ShipTable[2].pkginfo[0]=new PkgClass(500, anysize,  11.00, 0.50, 1,  "");      //up to   4lb is   9.00                                         COND-DEMO2 //--entry#3 To-Australia                                                                                                           COND-DEMO2  ShipTable[3].pkginfo[0]=new PkgClass(500, anysize, 11.00, 0.500, 1,  "");      //up to   4lb is  11.00                                         COND-DEMO2ZoneDefault = 0;                                                              //default is Zone#0                                             COND-DEMO2  ZonePrompt  = "Please indicate your Shipping Zone before continuing";         //prompt for Zone                                               COND-DEMO2  ShipTaxRate = 0.00;                                                           //no added-tax in the USA                                       COND-DEMO2  ShipTaxName = "";                                                             //don't show the tax included in shipping                       COND-DEMO2  HandlingChargePerOrder =       0.00;                                          //no HandlingCharge for the first package                       COND-DEMO2  HandlingChargePerExtraPackage= 0.00;                                          //no HandlingCharge per additional package                      COND-DEMO2//========================================================================// Packing-Rule Info (OPTIONAL)// ----------------------------// The program will work without this Packing-Rule info, but will then// be more prone to overcharging the customer for shipping.// You may want to pre-compute packing rules like these, which will be// feasible provided your list of item-sizes is fairly short.// (And if its really short, you probably won't need them:-)////========================================================================PackTable = [];                 //--LEAVE THIS LINE even when omitting Packing-Rule Info //======================================================================||//----------------------------------------------------------------------||// YOU DO NOT NEED TO MAKE ANY MODIFICATIONS BELOW THIS LINE            ||// If you wish to venture below this line and are new to javascript,    ||// then I recommend:  http://www.crockford.com/javascript/survey.html;  ||// (my code would be better had I read it first:-)                      ||//----------------------------------------------------------------------||//======================================================================||//========================================================================// Objects and Methods related to Package-Size;// by Eugene Reimer 2007-07-01.//// Some notes to myself:// -- don't really need constructors;  eg: ShipEntry(A,B) --> {zone:A, pkginfo:B}// -- possibly avoid size-field & doubly-dotted-selectors??  the SizeXX functions can accept any object having L,W,H fields...// -- could rewrite ShipTable & packBySz initializers using ARRAYOBJECT.push//    also ARRAYOBJECT[ARRAYOBJECT.length]=xxx;  -->  ARRAYOBJECT.push(xxx);// -- was tempted to use for(VAR in ARRAYOBJECT) iteration, but it's illegal/ill-advised for arrays (only for objects);  also doesn't do what's wanted...// -- wondered whether it's possible to redefine/overload the == operator instead of my SizeEQ routine;  found some info in://    http://www.mozilla.org/js/language/js20-2000-07/libraries/operator-overloading.html  -- doesn't sound promising...// -- oldest supported browsers:  Netscape-4.06 and IE4/5 conform to ECMA-262-Edition-1 (Javascript-1.3/JScript-3.0), which has push,unshift,split, but not regexprs!//      note: slice, substr not in ECMA-262-Edition-1, yet both of those browsers had them??  (using only substring to be safe)//      indexOf is nonstd, yet both browsers had it  --see http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:indexOf  if code needed//      NOTE: getAttribute and/or the DOM being used (for new-style attributes) are not supported by Netscape-4.7--!!--//// Future enhancements:// May use parts of the Bin-Packing algorithm by Martello, Pisinger, Vigo,// as published in http://www.diku.dk/~pisinger/3dbpp.c// (see also http://forums.devshed.com/software-design-43/3d-bin-packing-49217.html);//========================================================================var PkgQueue = null;                                                    //an array of Qszwt entries;  describes the items (later the packages) in the current ordervar PkgAsOne = null;                                                    //of type Qszwt;  the resulting package size & weightvar sComputeShippingNote="";                                            //an additional note about shipping to be shown to the customer during View-Cartvar gVat=0;                                                             //amount of VAT-tax included in the shipping-costfunction PrefEQ(A,B) {return A.substring(0,B.length)==B;}               //compare string-prefixfunction NumberZ(s) {var N=Number(s); if(isNaN(N))N=0; return(N);}      //like the javascript Number, except it returns zero instead of NaNfunction Integer(s) {return Math.round(NumberZ(s));}moneyEps= Math.pow(10, -MoneyPLACES);                                   //smallest nonzero monetary-amountMoneyROUND_FRA= Math.pow(10, +MoneyPLACES);                             //replaces 100 in all money-rounding  when PLACES>0MoneyROUND_NOF= Math.pow(10, -MoneyPLACES);                             //for reciprocal-based money-rounding when PLACES<=0function CentsFRA(f) {return  Math.round(f*MoneyROUND_FRA) / MoneyROUND_FRA;}function CentsNOF(f) {return  Math.round(f/MoneyROUND_NOF) * MoneyROUND_NOF;}Cents = (MoneyPLACES>0 ?CentsFRA :CentsNOF);                            //money-rounding function, init according to which rounding-method is neededfunction WtRndUP(x) {return Math.ceil( x*WTROUND)/WTROUND;}             //weight-rounding function that rounds-UPfunction WtRnd(x)   {return Math.round(x*WTROUND)/WTROUND;}             //weight-rounding functionfunction SzRnd(x)   {return Math.round(x*SZROUND)/SZROUND;}             //dimension-rounding functionfunction Element(E,S) {for(var e=S.length;e--;)if(E==S[e])return true; return false;}   //is-an-element-of for set as an array; may switch to bitstring...//function Element(E,S) {return (S&(1<<E))!=0;}                                         //is-an-element-of for bitstring  (set with elements 0..31)////--sanity-checking and initialization related to Options:while(PackTable.length < ShipTable.length) PackTable.push([]);                                                                          //ensure an entry for each Zoneif(RegionFromZone.length && RegionFromZone.length < ShipTable.length) DEBUG("RegionFromZone must have as many entries as ShipTable");X=[];for(Z=ShipTable.length;Z--;)X[Z]=false;for(Z=RegionFromZone.length; Z--;)X[Z]=(RegionFromZone[Z].length==1);  RegionFromZoneOvA=X; //array of Overrides booleansX=0; Z=RegionFromZone.length; if(Z>0) {X=1; while(Z--)X&=RegionFromZoneOvA[Z];}  RegionFromZoneOverrides=X;                             //was an option, now derivedif(RegionFromZone.length) {X=[];for(R=0;R<RegionTable.length;++R)X.push(R); while(RegionFromZone.length < ShipTable.length)RegionFromZone.push(X);}  //one for each zone//------------------------------------------------------------------------// CONSTRUCTOR:  ShipEntry (zone, pkginfo)//------------------------------------------------------------------------function ShipEntry (zone,pkginfo){   this.zone=zone;   this.pkginfo=pkginfo;}//------------------------------------------------------------------------// CONSTRUCTOR:  PkgClass (weight, size, costfixed, costperwtunit, wtunit, flag)//------------------------------------------------------------------------function PkgClass (weight,size,costfixed,costperwtunit,wtunit,flag){   this.weight=weight;   this.size=size;   this.costfixed=costfixed;   this.costperwtunit=costperwtunit;   this.wtunit=wtunit;   this.flag=flag;}//------------------------------------------------------------------------// CONSTRUCTOR:  PackingRule (itmsizeinfo, itmqtyinfo, pkgsize)//------------------------------------------------------------------------function PackingRule (itmsizeinfo,itmqtyinfo,pkgsize){   this.itmsizeinfo=itmsizeinfo;   this.itmqtyinfo=itmqtyinfo;   this.pkgsize=pkgsize;}//------------------------------------------------------------------------// CONSTRUCTOR:  Size (Length, width, height)//------------------------------------------------------------------------function Size (Length,width,height){   this.Length=NumberZ(Length); //calling it this.length could be trouble since objects already have a length attribute?   this.width =NumberZ(width );   this.height=NumberZ(height);}//------------------------------------------------------------------------// CONSTRUCTOR:  Qszwt (qty, size, weighteach)//------------------------------------------------------------------------function Qszwt (qty,size,weighteach){   this.qty   =Integer(qty);   this.size  =size;   this.weight=NumberZ(weighteach) * qty;   this.wt=[];  for(var w=0; w<qty; ++w) this.wt[w]= NumberZ(weighteach);   //the wt array is needed to handle same-size items having different weights}//------------------------------------------------------------------------// FUNCTION:  SizeStr (size) -- convert size to string//------------------------------------------------------------------------function SizeStr (size){   return(size.Length + "x" + size.width + "x" + size.height);}//------------------------------------------------------------------------// FUNCTION:  SizeVolume (size) -- returns the volume==Length*width*height;//------------------------------------------------------------------------function SizeVolume (size){   return(size.Length * size.width * size.height);}//------------------------------------------------------------------------// FUNCTION:  SizeEQ (size1, size2) -- compare two sizes for equality//------------------------------------------------------------------------function SizeEQ (size1,size2){   return(size1.Length==size2.Length && size1.width==size2.width && size1.height==size2.height);}//------------------------------------------------------------------------// FUNCTION:  InitPkgQueue() -- initialize the PkgQueue//------------------------------------------------------------------------function InitPkgQueue(){   PkgQueue = [];       //init to an empty array}//------------------------------------------------------------------------// FUNCTION:  AddPkgQueueEntry (qty, size, weighteach) -- revise the global PkgQueue, an array-of-Qszwt;// if there is an entry for size in PkgQueue, then update its qty & weight;// otherwise add an entry.//------------------------------------------------------------------------function AddPkgQueueEntry (qty,size,weighteach){   for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PkgQueue[i].size, size)){      PkgQueue[i].qty += Integer(qty);      PkgQueue[i].weight += NumberZ(weighteach)*Integer(qty);  for(var w=0; w<qty; ++w) PkgQueue[i].wt.push(NumberZ(weighteach));      return;   }   PkgQueue.push(new Qszwt(qty,size,weighteach));}//------------------------------------------------------------------------// FUNCTION:  RemovePkgQueueEntry (i) -- remove the PkgQueue[i] entry//------------------------------------------------------------------------function RemovePkgQueueEntry (i){   PkgQueue.splice(i, 1);}//------------------------------------------------------------------------// FUNCTION:  ShowPkgQueue -- display the PkgQueue  (convert to string)//------------------------------------------------------------------------function ShowPkgQueue(){   var str="";   for(var i=0; i<PkgQueue.length; ++i){      str+= "qty:"+PkgQueue[i].qty +"; sz:"+SizeStr(PkgQueue[i].size) +"; wt:"+Math.round(PkgQueue[i].weight) +" [";      for(var w=0; w<PkgQueue[i].qty; ++w) str+= Math.round(PkgQueue[i].wt[w]) + " ";      str+= "]\n";   }   return str;}//------------------------------------------------------------------------// FUNCTION:  PickAndApplyPackingRule -- find & apply the best matching PackingRule,// thereby simplifying the global PkgQueue;  returns false if no rule matches;// Algorithm:// if we have any size mentioned in only one PackingRule, then apply that rule;// otherwise, pick the best matching rule (by volumetric efficiency**) and apply it//------------------------------------------------------------------------function PickAndApplyPackingRule(PackingRule){   var SZ=null;  var P=null;  var bestGoodness=0;   for(var i=0; i<PkgQueue.length; ++i){      var sz=PkgQueue[i].size,  p=null,  ct=0;      for(var r=0; r<PackingRule.length; ++r) for(var e=0; e<PackingRule[r].itmsizeinfo.length; ++e) if(SizeEQ(PackingRule[r].itmsizeinfo[e], sz)){         p=r; ++ct; break;      }      if(ct==1) {SZ=sz; P=p; break;}                            //--have found P an ONLY rule for size SZ   }   if(SZ==null){      var minRV=99999999; for(var r=0; r<PackingRule.length; ++r) {var RV= SizeVolume(PackingRule[r].pkgsize); if(RV<minRV) minRV=RV;}      for(var r=0; r<PackingRule.length; ++r){         var MV=0;         for(var e=0; e<PackingRule[r].itmsizeinfo.length; ++e) for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PackingRule[r].itmsizeinfo[e], PkgQueue[i].size)){            MV+= SizeVolume(PkgQueue[i].size) * Math.min(PkgQueue[i].qty, PackingRule[r].itmqtyinfo[e]);         }         var RV=SizeVolume(PackingRule[r].pkgsize),  relRV=RV/minRV;         var VE=MV/RV;                                          //volumetric-efficiency (Matching-Volume over Result-Volume)         var g=VE/relRV;                                        //(**)modified volumetric-efficiency, to favour rule with smaller result-size         if(g>bestGoodness) {P=r; bestGoodness=g;}              //--have found a new BEST rule P      }   }   if(P==null) return false;    //--indicate NO matching rule found   if(SZ!=null)  sRule= "PackingRule[" + P + "] is ONLY rule for sz:" + SizeStr(SZ) + "\n";   else          sRule= "PackingRule[" + P + "] is BEST g:" + Math.round(bestGoodness*1000)/1000 + "\n";   sRules+=sRule;   //now apply rule P; first reducing qty or removing matched PkgQueue-entries, then adding an entry for the resulting pkg-size;   var wei=0;   for(var e=0; e<PackingRule[P].itmsizeinfo.length; ++e) for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PackingRule[P].itmsizeinfo[e], PkgQueue[i].size)){      var Q= Math.min(PkgQueue[i].qty, PackingRule[P].itmqtyinfo[e]);      //wei+= PkgQueue[i].weight * Q;      for(w=0; w<Q; ++w) {wei+= PkgQueue[i].wt[w]; PkgQueue[i].weight-= PkgQueue[i].wt[w];}      PkgQueue[i].qty-= Q;      if(PkgQueue[i].qty==0) RemovePkgQueueEntry(i);      else                   PkgQueue[i].wt.splice(0, Q);   }   AddPkgQueueEntry(1, PackingRule[P].pkgsize, wei);   return true;}sRule="";       //global var showing most recently applied PackingRule, for DEBUG purposes onlysRules="";      //global var showing which PackingRules were applied,   for DEBUG purposes only//------------------------------------------------------------------------// FUNCTION:  ComputePackageSize -- compute sum of item-sizes from global PkgQueue;// such summing, not altogether straightforward, is known as the 3D Bin Packing Problem;// RESULT:  global PkgQueue gets a simplified list of sub-packets;//          global PkgAsOne describes the order as a single package;// Algorithm:// while any packingrule matches the item-sizes in PkgQueue do//    pick and apply the best matching rule (see PickAndApplyPackingRule)// done//------------------------------------------------------------------------function ComputePackageSize (ZoneParam){   DEBUG2(ShowPkgQueue());   var PR=PackTable[ZoneParam];   if(PR.length>0){      //---use Packing-Rules Method;      // Condidered doing this twice with two sets of packing-rules:  BySz to get the smallest packages,  ByWt to get weight-limited packages;      // (2nd would need to start with a copy of PkgQueue from before 1st; see System.arraycopy;  OR: better to construct new list in both steps)      // but convinced myself that such complexity was not needed.  The ByWt rules will sometimes make the single-package bigger than optimal,      // but only for an order of predominantly high-density items;  ergo, such a non-minimal package-size will never force a higher price;      // and that likely holds for any user, any pkg-deliverer, or close enough...      sRules="Zone:"+ShipTable[ZoneParam].zone+"\n";      //==!!possibly add some sanity-checking on the PackingRules??      while((PkgQueue.length>1 || (PkgQueue.length==1 && PkgQueue[0].qty>1)) && PickAndApplyPackingRule(PR)) DEBUG2(sRule+ShowPkgQueue());      //was: {}      DEBUG1(sRules+"Packages:\n"+ShowPkgQueue());   }   //---use Crude Method, both in the absence of, and AFTER using PackingRules (to compute size as single package)   var thk=0,  len=0,  wid=0,  wei=0;   for(var i=0; i<PkgQueue.length; ++i){      if(PkgQueue[i].size.Length > len) len = PkgQueue[i].size.Length;      if(PkgQueue[i].size.width  > wid) wid = PkgQueue[i].size.width ;      thk += PkgQueue[i].size.height * PkgQueue[i].qty;      wei += PkgQueue[i].weight;   }   PkgAsOne = new Qszwt(1, new Size(len,wid,SzRnd(thk)), WtRndUP(wei));         //overall package size and weight (weight rounded up)   //==!!NEEDED: Crude-Method, try placing several small items (LxW) into one layer, guided by package-size defns -- in case PackingRules omitted;   //  OR: supply a script that reads a given set of HTML files, extracting items for sale, and then computes the PackingRules  (and make PackingRules required)}//------------------------------------------------------------------------// FUNCTION: ComputeShipping -- compute the shipping cost for size and weight of package//------------------------------------------------------------------------function ComputeShipping (ZoneParam){   sComputeShippingNote="";   if(PkgAsOne.weight==0 && PkgAsOne.size.height==0) return 0.00;   var Ship= ShipTable[ZoneParam].pkginfo;                      //Ship is array of PkgClass(weight,size,costfixed,costperwtunit,wtunit,flag)   function PricePkg(Ship,weight,height){                       //function to lookup price for one pkg      for(var c=0; c<Ship.length; ++c) if(weight<=Ship[c].weight && height<=Ship[c].size.height)      {  return  Cents(Ship[c].costfixed + Ship[c].costperwtunit * Math.ceil(weight / Ship[c].wtunit)); }       //return shipping-price      return 99999.99;                                                                                          //return illegal-weight-or-size indication   }   var asOne= PricePkg(Ship, PkgAsOne.weight, PkgAsOne.size.height);            //---compute price as a single package      var asMult=99999.99,  FC=null,  iN=0;   for(var c=0; c<Ship.length; ++c) if(Ship[c].flag=="*") FC=c;   if(FC!=null){                                                                //---also price as multiple smaller packages (to handle pricing anomalies...)      var maxHt=Ship[FC].size.height;      var maxWt=Ship[FC].weight;      var accHt=0,  accWt=0, sW="";  function R(f) {return " "+Math.ceil(f)+WTUNITS;}      asMult=0;      for(var i=PkgQueue.length; i--;) for(var j=PkgQueue[i].qty; j--;){        //do in reverse order (doing lightest ones first is best, at least for my examples:-)         var Wt= PkgQueue[i].wt[j];         var Ht= PkgQueue[i].size.height;         if( Wt>maxWt || Ht>maxHt) {asMult=99999.99; break;}                    //having ByWt-packing, now treat this as "separate packages impossible"  !!double-break??         if(accWt+Wt>maxWt || accHt+Ht>maxHt){                                  //handle previously accumulated little ones            asMult+= PricePkg(Ship, accWt, accHt);  ++iN; sW+=R(accWt);            accHt=0; accWt=0;         }         accWt+=Wt;  accHt+=Ht;                                                 //accumulate another little one      }      if(accWt+accHt){asMult+=PricePkg(Ship,accWt,accHt); ++iN; sW+=R(accWt);}  //finish off any non-handled accumulation   }   var asOneVat=  Cents(asOne *ShipTaxRate);   var asMultVat= Cents(asMult*ShipTaxRate);   asOne += asOneVat  + HandlingChargePerOrder;                                         //add HandlingCharges BEFORE comparing   asMult+= asMultVat + HandlingChargePerOrder + (iN-1)*HandlingChargePerExtraPackage;  //add HandlingCharges BEFORE comparing   var cost;   if(asOne<=asMult){                                                                                                                   //as one package      cost=asOne;  gVat=asOneVat;   }else{                                                                                                                               //as multiple packages      cost=asMult; gVat=asMultVat;      if(strAsMultiple)sComputeShippingNote="("+strAsMultiple+sW+(strAsSingle?"; "+strAsSingle+MoneySymbol+moneyFormat(asOne):"")+")";  //sW+asOne can be suppressed...   }   if(cost>=99999) {sComputeShippingNote=strBroken;  return 99999.99;}   return  cost;   //   //!!consider: keep info on the items within each packet, so can produce packing-instructions showing which items go into which size envelope (for the shipping dept)}//------------------------------------------------------------------------// FUNCTION: NewZone -- update the ZoneSelected cookie//------------------------------------------------------------------------function NewZone (ZoneParam){   SetCookie("ZoneSelected", ZoneParam, null, "/");   var RegionCookie= iGetCookie("RegionSelected");   if(RegionCookie!=null && RegionFromZone.length && !Element(RegionCookie, RegionFromZone[ZoneParam]))  DeleteCookie("RegionSelected","/");    //delete cookie if now illegal   location.href=location.href;}//------------------------------------------------------------------------// FUNCTION: NewRegion -- update the RegionSelected cookie//------------------------------------------------------------------------function NewRegion (RegionParam){   SetCookie("RegionSelected", RegionParam, null, "/");   var ZoneCookie= iGetCookie("ZoneSelected");   if(ZoneCookie!=null && RegionFromZone.length && !Element(RegionParam, RegionFromZone[ZoneCookie]))  DeleteCookie("ZoneSelected","/");        //delete cookie if now illegal   location.href=location.href;}//------------------------------------------------------------------------// FUNCTION: MoreLessInfo -- do toggling-update to the MoreState cookie, for DynamicWtSzColumns option//------------------------------------------------------------------------function MoreLessInfo(){   var MoreState=iGetCookie("MoreState");  if(MoreState==null) MoreState= (DisplayWtColumn?1:0)*2 + (DisplaySzColumn?1:0);   MoreState= ((MoreState&DynamicWtSzColumns)==DynamicWtSzColumns ?0 :DynamicWtSzColumns);      //toggle Wt&Sz-state as per DynamicWtSzColumns option   SetCookie("MoreState", MoreState, null, "/");                                                //update cookie   location.href=location.href;                                                                 //redraw page}//========================================================================// The rest still resembles the NopDesign version of nopcart.js (with some Stefko mods).// ER: have revised ALL  parseFloat --> NumberZ (to permit whole or fractional number where fractional was needed)// ER: have revised ALL  parseInt   --> Integer (new function that Rounds to whole-number, and avoids the leading-zero-means-octal gotcha)// ER: introduced a few functions to reduce duplication, thus making future maintenance less tedious / error-prone// ER: my other early revisions are flagged with a comment containing "ER:"  (later ones have a YYYY-MM-DD date instead)// ER: after my Quantity-Discount Pricing mods (2007-08-07), any resemblance to the original is but faint//========================================================================//------------------------------------------------------------------------// FUNCTION: NumberV// PURPOSE: convert string to number, for CKquantity, CKprice routines//------------------------------------------------------------------------function NumberV(checkString) {   var sNewString="", K=0;   for(var i=0; i<checkString.length; ++i){      ch = checkString.substring(i, i+1);      if(ch>="0" && ch<="9")     sNewString += ch;      //keep all digits      else if(ch=="." && ++K==1) sNewString += ch;      //ER: keep only the first dot   }   return(NumberZ(sNewString));}//------------------------------------------------------------------------// FUNCTION: CKquantity// PARAMETERS: Quantity to validate// RETURNS: Quantity as a whole-number, but in a string// PURPOSE: Make sure quantity is a whole-number//------------------------------------------------------------------------function CKquantity(checkString) {   var N=Integer(NumberV(checkString));  if(N==0) N=1;  return(""+N);}//------------------------------------------------------------------------// FUNCTION: CKprice// PARAMETERS: Price to validate// RETURNS: Price as a number, but in a string// ER: introduced this routine to validate an Online-Donation amount;//------------------------------------------------------------------------function CKprice(checkString) {   var N=Cents(NumberV(checkString));   if(N==0) N=DefaultDonation;  else if(N<MinimumDonation) {N=MinimumDonation; alert(MinimumDonationPrompt);}   return(moneyFormat(N));}//------------------------------------------------------------------------// FUNCTION: AddToCart// PARAMETERS: Form Object// RETURNS: false if nothing added because of error-message// PURPOSE: Add a product to the user's shopping cart, by updating Cookies, optionally with a popup prompt//------------------------------------------------------------------------function AddToCart(thisForm) {   var iNumberOrdered = 0;   var bAlreadyInCart = false;   var notice = "";   var ELE, ATR;   ELE=thisForm;                                                                //Handle the old-style name=... value=... way of specifying attributes   sID       = "";      if(ATR= ELE._ID || ELE.ID || ELE.ID_NUM ) sID      =ATR.value;          //2008-02-09 also support old name ID_NUM   sQUANTITY = "1";     if(ATR= ELE._QUANTITY   || ELE.QUANTITY ) sQUANTITY=ATR.value;   sPRICE    = "0.00";  if(ATR= ELE._PRICE      || ELE.PRICE    ) sPRICE   =ATR.value;   sNAME     = "";      if(ATR= ELE._NAME       || ELE.NAME     ) sNAME    =ATR.value;   sWEIGHT   = "0";     if(ATR= ELE._WEIGHT     || ELE.WEIGHT   ) sWEIGHT  =ATR.value;          //ER: was called sSHIPPING   sLENGTH   = "0";     if(ATR= ELE._LENGTH     || ELE.LENGTH   ) sLENGTH  =ATR.value;          //ER: new   sWIDTH    = "0";     if(ATR= ELE._WIDTH      || ELE.WIDTH    ) sWIDTH   =ATR.value;          //ER: new   sHEIGHT   = "0";     if(ATR= ELE._HEIGHT     || ELE.HEIGHT   ) sHEIGHT  =ATR.value;          //ER: new   sPROMPT   = "";                                                              //2008-01-21: a prompt-string from a selector   for(var i=0;i<thisForm.elements.length;++i){                                 //2008-02-13: go thru hidden elements handling attrname=attrvalue      ELE=thisForm.elements[i];      if(ELE.type!="hidden") continue;      if(!ELE.getAttribute)  continue;                                          //2008-03-10 skip if old browser that doesn't support getAttribute      if(     ATR= ELE.getAttribute("_ID")      || ELE.getAttribute("ID_NUM")   ) sID      = ATR;       //avoid attribute named ID      if(     ATR= ELE.getAttribute("_QUANTITY")|| ELE.getAttribute("QUANTITY") ) sQUANTITY= ATR;      if(     ATR= ELE.getAttribute("_PRICE")   || ELE.getAttribute("PRICE")    ) sPRICE   = ATR;      if(     ATR= ELE.getAttribute("_NAME")                                    ) sNAME    = ATR;       //avoid attribute named NAME      if(     ATR= ELE.getAttribute("_WEIGHT")  || ELE.getAttribute("WEIGHT")   ) sWEIGHT  = ATR;      if(     ATR= ELE.getAttribute("_LENGTH")  || ELE.getAttribute("LENGTH")   ) sLENGTH  = ATR;      if(     ATR= ELE.getAttribute("_WIDTH")                                   ) sWIDTH   = ATR;       //avoid attribute named WIDTH      if(     ATR= ELE.getAttribute("_HEIGHT")                                  ) sHEIGHT  = ATR;       //avoid attribute named HEIGHT   }   //sADDTLINFO= "";                                                            //2008-02-07: old ADDITIONALINFOn selectors yanked and replaced with new method below   //if(thisForm.ADDITIONALINFO !=null) sADDTLINFO =         thisForm.ADDITIONALINFO [thisForm.ADDITIONALINFO.selectedIndex].value;   //if(thisForm.ADDITIONALINFO2!=null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO2[thisForm.ADDITIONALINFO2.selectedIndex].value;   //if(thisForm.ADDITIONALINFO3!=null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO3[thisForm.ADDITIONALINFO3.selectedIndex].value;   //if(thisForm.ADDITIONALINFO4!=null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO4[thisForm.ADDITIONALINFO4.selectedIndex].value;   //if(thisForm.USERENTRY      !=null) sADDTLINFO += (sADDTLINFO?"; ":"") + thisForm.USERENTRY.value;  //ER: avoid leading semicolon   //if(sADDTLINFO) sNAME+="; "+sADDTLINFO;//==TEMP==                                                                                //2008-02-07:  new selectors with AddOneOfMany-features;  plus they can be Radio-buttons   for(var N=0;N<=2;++N) for(var n=0;n<=9;++n){                                 //2008-02-07:  and they can supply Replacement OR To-Be-Added values (leading plus-sign)      var selname=["ADDITIONALINFO","USERCHOICE","_USERCHOICE"][N] + (n?n:"");  //support  ADDITIONALINFOn  USERCHOICEn  _USERCHOICEn  for n=emptystring,1..9      var selector=thisForm[selname];      if(selector==null) continue;      if(typeof selector.selectedIndex == "undefined"){                         //for a RADIO-button-selector need a loop testing the CHECKED-attribute         for(var i=0;i<selector.length;++i) if(selector[i].checked) ELE=selector[i];      }else{                                                                    //for a SELECT-box;  could use a loop testing the SELECTED-attribute         ELE=selector[selector.selectedIndex];      }      function NewStr(OLD,NEW) {return    (NEW.substring(0,1)=="+" ?        OLD +        NEW.substring(1,NEW.length)  :NEW);}   //NewStr: plus-sign indicates catenation      function NewNum(OLD,NEW) {return ""+(NEW.substring(0,1)=="+" ?NumberZ(OLD)+NumberZ(NEW.substring(1,NEW.length)) :NEW);}   //NewNum: plus-sign indicates addition      if(!ELE.getAttribute)  if(ATR= ELE.value                                  ) sNAME    +=" "+ATR;                   //old-style here; 2008-03-18 only for old browser      if(!ELE.getAttribute)  continue;                                                                                  //2008-03-10 skip if old browser w/o getAttribute      if(     ATR= ELE.getAttribute("_ID")      || ELE.getAttribute("ID")       ) sID      = NewStr(sID,ATR);           //beware of attribute named ID      else if(ATR=                                 ELE.getAttribute("ID_NUM")   ) sID      = NewStr(sID,ATR);           //support old name      if(     ATR= ELE.getAttribute("_QUANTITY")|| ELE.getAttribute("QUANTITY") ) sQUANTITY= NewNum(sQUANTITY,ATR);     //possibly useful here??      if(     ATR= ELE.getAttribute("_PRICE")   || ELE.getAttribute("PRICE")    ) sPRICE   = NewNum(sPRICE,ATR);      if(     ATR= ELE.getAttribute("_NAME")                                    ) sNAME    = NewStr(sNAME,ATR);       else if(ATR= ELE.value                                                    ) sNAME   +=" "+ATR;                    //2008-03-18 old-style still here for new browser      else if(ELE.type!="radio"  &&  (ATR=         ELE.getAttribute("NAME"))    ) sNAME    = NewStr(sNAME,ATR);         //beware of NAME; 2008-03-18 avoid on radio      if(     ATR= ELE.getAttribute("_WEIGHT")  || ELE.getAttribute("WEIGHT")   ) sWEIGHT  = NewNum(sWEIGHT,ATR);      if(     ATR= ELE.getAttribute("_LENGTH")  || ELE.getAttribute("LENGTH")   ) sLENGTH  = NewNum(sLENGTH,ATR);      if(     ATR= ELE.getAttribute("_WIDTH")   || ELE.getAttribute("WIDTH")    ) sWIDTH   = NewNum(sWIDTH,ATR);        //beware of attribute named WIDTH      if(     ATR= ELE.getAttribute("_HEIGHT")  || ELE.getAttribute("HEIGHT")   ) sHEIGHT  = NewNum(sHEIGHT,ATR);       //beware of attribute named HEIGHT      if(     ATR= ELE.getAttribute("_PROMPT")  || ELE.getAttribute("PROMPT")   ) sPROMPT  += (sPROMPT?"; ":"")+ATR;      //--note: for INPUT-TYPE=RADIO/HIDDEN the names NAME, ID, WIDTH, HEIGHT are not available, and that's why I went to _ID _NAME etc (briefly used PRODID PRODNAME)      //but they work in SELECT-OPTION, so am leaving support for them;      //2008-03-18: skip NAME on radio to avoid getting NAME=USERCHOICE;  also skip VALUE in new browser when _NAME present  (may need rethink??)   }   if(sID+sNAME=="" && sPROMPT=="") sPrompt="Please select an option";          //2008-01-21: (AddOneOfManyToCart uses sNAME=="selected")   if(sPROMPT!="")  {alert(sPROMPT);  return false;}                            //2008-01-21: prompt, and avoid adding (null) product   if(PrefEQ(sID,PrefDonation))  sPRICE=CKprice(sPRICE);                        //2008-02-04: validation for donation-amount (default & minimum)   if(     ATR=thisForm._USERTEXT||thisForm.USERTEXT) {if(ATR.value)sNAME+= "; " + ATR.value;}          //2008-02-07: now directly onto sNAME  (was onto sADDTLINFO)   else if(ATR=thisForm.USERENTRY)                    {if(ATR.value)sNAME+= "; " + ATR.value;}          //support old name   //If this product already in the cart, then combine them instead of adding another.   iNumberOrdered= iGetCookie("NumberOrdered",0);   for(var i=1; i<=iNumberOrdered; ++i){      GetRow(i);                                                //ER: get fields for row-i      if(fields[0] == sID    &&         fields[3] == sNAME  &&                                                 //2008-02-07 removed:  fields[8] == sADDTLINFO &&        (fields[2] == sPRICE || PrefEQ(sID,PrefDonation))                       //2008-02-04: donations can be combined even when amount different      ){                                                        //---already in cart, so combine;  ER: a match on all but PRICE deserves a DEBUG-alert?         bAlreadyInCart = true;         if(PrefEQ(sID,PrefDonation)){                          //donations are combined by summing amounts (new 2008-02-04)            dbUpdatedOrder = sID + "|" +               sQUANTITY + "|" +               (Number(sPRICE)+Number(fields[2])) + "|" +               sNAME     + "|" +               sWEIGHT   + "|" +               sLENGTH   + "|" +               sWIDTH    + "|" +               sHEIGHT;                                         //2008-02-07 removed:   + "|" +  sADDTLINFO;         }else{                                                 //non-donations are combined by summing quantities            dbUpdatedOrder = sID + "|" +               (Integer(sQUANTITY)+Integer(fields[1])) + "|" +               sPRICE    + "|" +               sNAME     + "|" +               sWEIGHT   + "|" +               sLENGTH   + "|" +               sWIDTH    + "|" +               sHEIGHT;                                         //2008-02-07 removed:   + "|" + sADDTLINFO;         }         sNewOrder = "Order." + i;         DeleteCookie(sNewOrder, "/");         SetCookie(sNewOrder, dbUpdatedOrder, null, "/");         notice = strAdded + "\n-------------------------------------\n" + strAddedQuantity + sQUANTITY + "\n" + strAddedProduct + sNAME;         break;      }   }   if(!bAlreadyInCart){                                         //---not in cart, so add it      iNumberOrdered++;      if(iNumberOrdered > 15) alert(strSorry);                  //limit nbr-rows in cart to 15;  ER: was 12  (the limit is 20 cookies for one domain)      else {         dbUpdatedOrder = sID + "|" +            sQUANTITY + "|" +            sPRICE    + "|" +            sNAME     + "|" +            sWEIGHT   + "|" +            sLENGTH   + "|" +            sWIDTH    + "|" +            sHEIGHT;                                            //2008-02-07 removed:   + "|" + sADDTLINFO;         sNewOrder = "Order." + iNumberOrdered;         SetCookie(sNewOrder,       dbUpdatedOrder, null, "/");         SetCookie("NumberOrdered", iNumberOrdered, null, "/");         notice = strAdded + "\n-------------------------------------\n" + strAddedQuantity + sQUANTITY + "\n" + strAddedProduct + sNAME;      }   }   if(DisplayPopupOnAdd && notice!="")  alert(notice);   return true;}//------------------------------------------------------------------------// FUNCTION: moneyFormat// PARAMETERS: Number to be formatted// RETURNS: Formatted Number// PURPOSE: convert float to #.## string// ER: I first rewrote this to something I could understand, since supporting MoneyPLACES option was unthinkable otherwise;//------------------------------------------------------------------------function moneyFormatFRA(input) {        //for any currency that uses a fraction, example US-dollars   var cents= "" + Math.round(input * MoneyROUND_FRA);   while(cents.length < MoneyPLACES+1)  cents="0"+cents;   return  cents.substring(0, cents.length-MoneyPLACES) + "." + cents.substring(cents.length-MoneyPLACES, cents.length);   //   ////--ER: was:   //var dollars= Math.floor(input);   //var tmp= new String(input);   //for(var decimalAt=0; decimalAt < tmp.length; decimalAt++){   //   if(tmp.charAt(decimalAt)==".") break;   //}   //var cents= "" + Math.round(input * 100);   //cents= cents.substring(cents.length-2, cents.length)   //dollars += ((tmp.charAt(decimalAt+2)=="9")&&(cents=="00"))? 1 : 0;   //if(cents=="0") cents = "00";   //return  dollars + "." + cents;}function moneyFormatNOF(input) {        //ER: for a currency that uses no fraction (and may need rounding to a multiple of 1000 etc)   return ""+Cents(input);}moneyFormat = (MoneyPLACES>0 ?moneyFormatFRA :moneyFormatNOF);  //ER: init function according to whether a fraction is needed  (MoneyPLACES indicates that)//------------------------------------------------------------------------// FUNCTION: SetCookie// PARAMETERS: name, value, expiration date, path, domain, security// RETURNS: Null// PURPOSE: Store a cookie in the users browser//------------------------------------------------------------------------function SetCookie (name,value,expires,path,domain,secure) {   document.cookie = CartID + name + "=" + escape (value) +   ((expires) ? "; expires=" + expires.toGMTString() : "") +   ((path) ? "; path=" + path : "") +   ((domain) ? "; domain=" + domain : "") +   ((secure) ? "; secure" : "");}//------------------------------------------------------------------------// FUNCTION: DeleteCookie// PARAMETERS: Cookie name, path, domain// RETURNS: null// PURPOSE: Remove a cookie from users browser.//------------------------------------------------------------------------function DeleteCookie (name,path,domain) {   if(GetCookie(name)){      document.cookie = CartID + name + "=" +      ((path) ? "; path=" + path : "") +      ((domain) ? "; domain=" + domain : "") +      "; expires=Thu, 01-Jan-70 00:00:01 GMT";   }}//------------------------------------------------------------------------// FUNCTION: getCookieVal// PARAMETERS: offset// RETURNS: URL unescaped Cookie Value// PURPOSE: Get a specific value from a cookie//------------------------------------------------------------------------function getCookieVal (offset) {   var endstr = document.cookie.indexOf (";", offset);   if(endstr == -1)  endstr= document.cookie.length;   return unescape(document.cookie.substring(offset, endstr));}//------------------------------------------------------------------------// FUNCTION: GetCookie// PARAMETERS: Name// PURPOSE: Retrieve cookie from users browser// RETURNS: Value in Cookie as a string,  or null if no such cookie exists//------------------------------------------------------------------------function GetCookie (name) {   var arg = CartID + name + "=";   var alen = arg.length;   var clen = document.cookie.length;   var i = 0;   while(i < clen){      var j = i + alen;      if(document.cookie.substring(i, j)==arg)  return(getCookieVal(j));      i = document.cookie.indexOf(" ", i) + 1;      if(i == 0) break;   }   return(null);}//------------------------------------------------------------------------// FUNCTION: iGetCookie// PARAMETERS: Name, DEF// PURPOSE: Retrieve an INTEGER cookie from users browser// RETURNS: Value in Cookie as an Integer,  or DEF if no such cookie exists//------------------------------------------------------------------------function iGetCookie (name, DEF) {  if(DEF==null)DEF=null;  //if DEF is omitted, use null as DEF   var r= GetCookie(name);   return (r==null ?DEF :Integer(r));}//------------------------------------------------------------------------// FUNCTION: FixCookieDate// PARAMETERS: date// RETURNS: date// PURPOSE: Fix cookie date, store back in date//------------------------------------------------------------------------//function FixCookieDate (date) {//   var base= new Date(0);//   var skew= base.getTime();//   date.setTime(date.getTime() - skew);//}//------------------------------------------------------------------------// FUNCTION: GetRow// PURPOSE: read one cart-row from the cookie-database// RETURNS: global array fields, an array containing the fields// NOTE: in the database-format, fields are separated by "|"// ER: made this a function to improve maintainability.//------------------------------------------------------------------------function GetRow(i){   RowKey = "Order." + i;   dbrow = "";   dbrow = GetCookie(RowKey);   Token0 = dbrow.indexOf("|", 0);   Token1 = dbrow.indexOf("|", Token0+1);   Token2 = dbrow.indexOf("|", Token1+1);   Token3 = dbrow.indexOf("|", Token2+1);   Token4 = dbrow.indexOf("|", Token3+1);   Token5 = dbrow.indexOf("|", Token4+1);   Token6 = dbrow.indexOf("|", Token5+1);   //ken7 = dbrow.indexOf("|", Token6+1);               //scrapped 2008-02-07   fields = [];   fields[0] = dbrow.substring(0,        Token0);       //Product-ID   fields[1] = dbrow.substring(Token0+1, Token1);       //Quantity   fields[2] = dbrow.substring(Token1+1, Token2);       //Price   fields[3] = dbrow.substring(Token2+1, Token3);       //Product-Name   fields[4] = dbrow.substring(Token3+1, Token4);       //Weight   fields[5] = dbrow.substring(Token4+1, Token5);       //Length;       ER: new   fields[6] = dbrow.substring(Token5+1, Token6);       //Width;        ER: new   fields[7] = dbrow.substring(Token6+1, dbrow.length); //Height;       ER: new;                                2008-02-07 revised Token7-->dbrow.length   //elds[8] = dbrow.substring(Token7+1, dbrow.length); //Addtl-Info;   ER: was fields[5] in NopDesign version  2008-02-07 yanked}//------------------------------------------------------------------------// FUNCTION: RemoveFromCart// PARAMETERS: Row Number to Remove// RETURNS: Null// PURPOSE: Remove an item from a users shopping cart//------------------------------------------------------------------------function RemoveFromCart(RemOrder) {   if( (DisplayPopupOnRemove ? confirm(strRemove) : true) ){    //ER: suppress the confirm when DisplayPopupOnRemove==false      NumberOrdered = iGetCookie("NumberOrdered",0);      for(var i=RemOrder; i < NumberOrdered; ++i){         NewOrder1 = "Order." + (i+1);         NewOrder2 = "Order." + (i);         database = GetCookie(NewOrder1);         SetCookie (NewOrder2, database, null, "/");      }      NewOrder = "Order." + NumberOrdered;      SetCookie ("NumberOrdered", (NumberOrdered>0?NumberOrdered-1:0), null, "/");      DeleteCookie(NewOrder, "/");      location.href=location.href;   }}//------------------------------------------------------------------------// FUNCTION: EmptyTheCart// PURPOSE: Remove all items from a users shopping cart.// Intended for a thanks-for-your-purchase page (that your payment-processor is instructed to return to),// since after checkout one expects an empty cart...//------------------------------------------------------------------------function EmptyTheCart(){   NumberOrdered = iGetCookie("NumberOrdered",0);   for(var i=1; i <= NumberOrdered; ++i){      NewOrder = "Order." + i;      DeleteCookie(NewOrder, "/");   }   SetCookie("NumberOrdered", 0, null, "/");}//------------------------------------------------------------------------// FUNCTION: ChangeQuantity// PARAMETERS: Order Number to Change Quantity// RETURNS: Null// PURPOSE: Change quantity of an item in the shopping cart//------------------------------------------------------------------------function ChangeQuantity(OrderItem,NewQuantity) {   if(isNaN(NewQuantity)){      alert(strErrQty);   }else{      GetRow(OrderItem);        //ER: get fields for row-OrderItem      dbUpdatedOrder = fields[0] + "|" +         NewQuantity + "|" +         fields[2]   + "|" +         fields[3]   + "|" +         fields[4]   + "|" +         fields[5]   + "|" +         fields[6]   + "|" +         fields[7];             //2008-02-07 removed:  + "|" + fields[8];      sNewOrder = "Order." + OrderItem;      DeleteCookie(sNewOrder, "/");      SetCookie(sNewOrder, dbUpdatedOrder, null, "/");      location.href=location.href;   }}////------------------------------------------------------------------------//// FUNCTION:  RadioChecked//// PARAMETERS:  Radio button to check//// RETURNS:   True if a radio has been checked//// PURPOSE:   Form validation//// ER: NOT USED////------------------------------------------------------------------------//function RadioChecked( radiobutton ) {//   var bChecked = false;//   var rlen = radiobutton.length;//   for(var i=0; i < rlen; ++i)  if(radiobutton[i].checked)  bChecked = true;//   return bChecked;//}////------------------------------------------------------------------------//// FUNCTION: QueryString//// PARAMETERS: Key to read//// RETURNS: value of key//// PURPOSE: Read data passed in via GET mode//// ER: NOT USED////------------------------------------------------------------------------//QueryString.keys = [];//QueryString.values = [];//function QueryString(key) {//   var value = null;//   for(var i=0;i<QueryString.keys.length;++i){//      if (QueryString.keys[i]==key) {//         value = QueryString.values[i];//         break;//      }//   }//   return value;//}////------------------------------------------------------------------------//// FUNCTION: QueryString_Parse//// PARAMETERS: (URL string)//// PURPOSE: Parse query string data;  must be called before QueryString//// ER: NOT USED////------------------------------------------------------------------------//function QueryString_Parse() {//   var query= window.location.search.substring(1);//   var pairs= query.split("&");  for(var i=0;i>pairs.length;++i){//      var pos = pairs[i].indexOf("=");//      if (pos >= 0) {//         var argname = pairs[i].substring(0,pos);//         var value = pairs[i].substring(pos+1);//         QueryString.keys[QueryString.keys.length] = argname;//         QueryString.values[QueryString.values.length] = value;//      }//   }//}//------------------------------------------------------------------------// FUNCTION: ReadCartComputePrices// PURPOSE:  Read all rows from the cookies// RETURNS:  globals iNumberOrdered, and Cart an array with Cart[i] being row-i;// NOTE THAT://      Cart[i].ID        replaces all subsequent uses of  fields[0];//      Cart[i].QUANTITY  replaces all subsequent uses of  fields[1]  OR  Integer(fields[1]);//      Cart[i].PRICEAVG  replaces all subsequent uses of  fields[2]  OR  NumberZ(fields[2]);//      Cart[i].NAME      replaces all subsequent uses of  fields[3];//      Cart[i].WEIGHT    replaces all subsequent uses of  fields[4]  OR  NumberZ(fields[4]);//      Cart[i].LENGTH    replaces all subsequent uses of  fields[5]  OR  NumberZ(fields[5]);//      Cart[i].WIDTH     replaces all subsequent uses of  fields[6]  OR  NumberZ(fields[6]);//      Cart[i].HEIGHT    replaces all subsequent uses of  fields[7]  OR  NumberZ(fields[7]);//      Cart[i].ADDTLINFO replaces all subsequent uses of  fields[8]  <-- scrapped 2008-02-07//      Cart[i].PRICE     is the entire string from Form.PRICE; but should never be needed outside of this routine;// Form.PRICE now consists of comma-separated terms such as://      3.95                      -- means 3.95 each//      3.95,2:3.00               -- means 3.95 for the first, 2nd and subsequent are 3.00 each;//      3.95,10=2.99              -- means 3.95 each, or exactly 10 for 29.90;//      3.95,10=2.99,10:2.99      -- means 3.95 each, 10 or more are 2.99 each;//      3.95,2:3.00,4:2.75,8:2.50 -- means 2nd...are 3.00, 4th...are 2.75, 8th and subsequent are 2.50;//      3.95,2:-30,GRP01          -- means 2nd and subsequent are 30% off, and this applies across all products in the group GRP01;//      can have any number of ":" or "=" terms, at most one starting with a letter to name a group;//      ":" and "=" terms may be interspersed, or not, but the ":" terms must appear in increasing order by left-side, and "=" terms likewise;//      RESTRICTION: exact-quantity ("=") pricing is only permitted within a group where all products have identical prices;//      anything else would be so bloody hard to explain, it's hard to see anyone wanting it;// ER: All-at-once reading is essential for a reasonably efficient implementation of Quantity-Discount Pricing,//      since several passes are needed to compute prices, and those are needed before the pass that displays & does payment-processing;//      (more than one preliminary pass is needed to support Product-Groups);// ER: also moved the Shipping & Tax calculation code here, from ManageCart/CheckoutCart, setting globals: fTotal, fShipping, fTax, ZoneSelected, RegionSelected,//      to further reduce duplication thus lessening the tedium & error-proneness of making modifications.// Q: To get deterministic n-or-more pricing, independent of the order items added to cart, could sort by price within group?// 2007-12-04: now applying Cents-rounding to PRICEAVG on the assumption that payment-processors can't handle more fractional digits than is the currency norm;// 2008-02-03: now applying Cents-rounding to PRICEAVG earlier (here) so that amounts shown by ViewCart will agree with what PayPal etc will be displaying;//------------------------------------------------------------------------function ReadCartComputePrices(){   var Dig="0123456789", Lwr="abcdefghijklmnopqrstuvwxyz", Upr="ABCDEFGHIJKLMNOPQRSTUVWXYZ", Let=Lwr+Upr;       //constants for Is-testing   function Is(c,pat) {return pat.indexOf(c)!=-1;}   var i, k;   var KK=-1;                                   //row-nbr for Coupon-discount   var C, G, D, X, K;                           //results from the Pparse routine   function Pparse(priceparm){                  //the Pparse PRICE-parsing routine      C=0; G=""; D=[]; X=[]; K=null;      if(priceparm.substring(0,2)==">="){       //a coupon (2008-03-06)         var x=priceparm.substring(2).split(":"), y=x[1].indexOf("%"); if(y==-1)y=x[1].length;         K= {min:NumberZ(x[0]), amt:NumberZ(x[1].substring(0,y)), pct:x[1].substring(y)};         //alert("min:"+K.min+"; amt:"+K.amt+"; pct:"+K.pct+";");      }else for(var price=priceparm.split(","), J=0; J<price.length; ++J){         var T=price[J];         if(     T.indexOf("=")!=-1)            {var x=T.split("="); X.push( {q:Integer(x[0]), p:NumberZ(x[1])} );}         else if(T.indexOf(":")!=-1)            {var x=T.split(":"); D.push( {q:Integer(x[0]), p:NumberZ(x[1])} );}         else if(Is(T.substring(0,1),Let))      G=T;         else                                   C=NumberZ(T);      }   }   //====read all rows from the cookies====   Cart = [];                                           //init global Cart array   iNumberOrdered=iGetCookie("NumberOrdered",0);        //get the nbr-rows-in-cart cookie   for(i=1; i<=iNumberOrdered; ++i){      GetRow(i);                                        //get fields for row-i      Pparse(fields[2]);                                //parse the PRICE string      Cart[i]= {         ID:               fields[0],          QUANTITY: Integer(fields[1]),          PRICE:            fields[2],          NAME:             fields[3],          WEIGHT:   NumberZ(fields[4]),          LENGTH:   NumberZ(fields[5]),          WIDTH:    NumberZ(fields[6]),          HEIGHT:   NumberZ(fields[7]),          //DTLINFO:        fields[8],                   //2008-02-07 scrapped ADDTLINFO aka fields[8]         C:C, G:G, D:D, X:X, K:K, PRICEAVG:null         //the parsed-price fields; consider using ID as G if no group specified(?)      }   }   //====compute prices====   for(i=1; i<=iNumberOrdered; ++i){      if(Cart[i].PRICEAVG!=null) continue;                                      //skip if row already done (due to groups)      C=Cart[i].C;  G=Cart[i].G;  D=Cart[i].D;  X=Cart[i].X;  K=Cart[i].K;      //info from Pparse      function eEQ(A,B) {return A.q==B.q && A.p==B.p;}                                                                                  //compare q-p elements      function aEQ(A,B) {if(A.length!=B.length)return false; for(var k=A.length;k--;)if(!eEQ(A[k],B[k]))return false; return true;}     //compare array of q-p elements      function pEQ(A,B) {return A.C==B.C && aEQ(A.D,B.D) && aEQ(A.X,B.X);}                                                              //compare parsed prices      function str(X) {var s="["; for(var i=0;i<X.length;++i)s+="{"+X[i].q+","+X[i].p+"},"; s+="]"; return s;}  //convert X or D to string      function pp(P)  {return (P>=0 ?P :C*(100+P)/100);}                                                        //convert negative price to a percent-off-wrt-C price      var q=Cart[i].QUANTITY;                                                                           //q is qty of this product      var Q=q;   if(G!="")for(Q=0, k=1; k<=iNumberOrdered; ++k) if(Cart[k].G==G) Q+= Cart[k].QUANTITY;  //Q is qty across all products in this group      var g=[i]; if(G!="")for(g=[],k=1; k<=iNumberOrdered; ++k) if(Cart[k].G==G) g.push(k);             //g is array of indices for rows in this group      var ix=-1; for(k=X.length; k--;) if(X[k].q<=Q) {ix=k; break;}             //find the biggest applicable exact-qty ("=") discount;  requires ordered terms      var id=-1; for(k=D.length; k--;) if(D[k].q<=Q) {id=k; break;}             //find the biggest applicable n-or-more (":") discount;  requires ordered terms      DEBUG4("row:"+i+" itm:"+Cart[i].ID+" PRICE:"+Cart[i].PRICE+" C:"+C+" G:"+G+" X:"+str(X)+" D:"+str(D)+" g:"+g+" ix:"+ix+" id:"+id);      if(X.length>0){       //var m=[];for(k=g.length;k--;)if(Cart[g[k]].PRICE!=Cart[i].PRICE)m.push(g[k]);  //sanity-check prices across group, using simple string-comparisons         var m=[];for(k=g.length;k--;)if(!pEQ(Cart[g[k]], Cart[i]))m.push(g[k]);        //sanity-check prices across group, comparing C,D,X to permit minor differences         if(m.length>0) DEBUG("group:"+G+" has exact-qty discount but PRICE on row:"+i+" conflicts with rows:"+m);      //issue DEBUG alert         if(m.length>0) {for(k=g.length; k--;) Cart[g[k]].PRICEAVG=C;  continue;}       //suppress further such DEBUG alerts for the other rows in group      }      var A, QQ, q2, I;      if(K){                                                                    //---a Coupon-Discount, will be applied later (2008-03-06)---         Cart[i].QUANTITY=1;                                                    //override quantity, forcing it to be one         Cart[i].PRICEAVG=0;                                                    //set price-apiece to zero, until we find out if qualifications are met         KK=i;      }else if(ix!=-1){                                                         //---apply an exact-quantity discount, or both kinds, to all rows in group---         A=0;  QQ=Q;         while(Q!=0){                                                           //until all items in group are priced or no more applicable exact-discounts            q2= Math.floor(Q / X[ix].q) * X[ix].q;                              //q2 is the largest multiple of X[ix].q that's less-than-or-equal-to Q            A += q2*pp(X[ix].p);  Q-=q2;                                        //sell q2 at price X[ix].p, revising Q to reflect remaining qty            DEBUG4("sell "+q2+" at:"+pp(X[ix].p)+" Q:"+Q);            --ix; while(ix>=0 && X[ix].q>Q) --ix;                               //revise ix for the next applicable exact-discount, if any            if(ix==-1)break                                                     //leave loop if none            if(id!=-1 && pp(D[id].p)<pp(X[ix].p))break;                         //2008-03-01 leave if the n-or-more price beats next applicable exact-discount price         }         if(Q>0) A += Q * (id!=-1 ? pp(D[id].p) :C);                            //price the rest using the applicable n-or-more-price, or C if none         var priceavg= Cents(A/QQ);                                             //2008-02-03: was Cents(A) / QQ         for(k=g.length; k--;)  {I=g[k];  Cart[I].PRICEAVG = priceavg;}         //each row in group gets the same price apiece      }else if(id!=-1){                                                         //---apply an n-or-more discount, to all rows in group---         var ID, QD=0;                                                          //QD is the count of items already priced         for(k=0; k<g.length; ++k){  I=g[k];                                    //for each row I in group do            A=0; q=Cart[I].QUANTITY; C=Cart[I].C;  D=Cart[I].D;            if(D.length==0 || D[0].q!=1)  D.unshift( {q:1, p:C} );              //augment D to include a price for the first one(s), to simplify search-loops, etc            while(q>0){               for(ID=0;;++ID) if(ID+1==D.length||QD+1<D[ID+1].q)break;         //now D[ID].p is the price for the QD+1'th (next) item               q2=q;  if(ID+1<D.length) q2=Math.min(q, D[ID+1].q-1-QD);         //q2 is nbr of items to sell at price D[ID].p               A+= q2*pp(D[ID].p);  QD+=q2;  q-=q2;                             //sell q2 items at D[ID].p, revising QD & q               DEBUG4("sell "+q2+" at:"+pp(D[ID].p)+" ID:"+ID+" QD:"+QD);               if(q2<=0) {DEBUG("ReadCartComputePrices is broken"); break;}            }            Cart[I].PRICEAVG = Cents(A / Cart[I].QUANTITY);                     //price apiece;  2008-02-03: was Cents(A) / Cart[I].QUANTITY         }      }else{                                                                    //---apply constant price to one row---         Cart[i].PRICEAVG = Cents(C);                                           //price apiece;  2008-02-03: was C      }   }   if(KK!=-1){                                                                  //---now apply a Coupon-Discount, if any (2008-03-06)---      for(fTotal=0, i=1; i<=iNumberOrdered; ++i) fTotal+=Cart[i].QUANTITY*Cart[i].PRICEAVG;             //need the pre-tax pre-shipping subtotal, to see if applicable      K=Cart[KK].K;      if(fTotal>=K.min) Cart[KK].PRICEAVG= (K.pct ?fTotal*K.amt/100 : -Math.min(-K.amt,fTotal));        //if applicable, compute the (negative) amount of discount      if(PaymentProcessor=="pp")  PaymentProcessor="ppa";                                               //2008-03-09 because Paypal cannot handle negative price      if(PaymentProcessor2=="pp") PaymentProcessor2="ppa";                                              //2008-03-09 because Paypal cannot handle negative price   }   //====total, shipping, tax calculations====   ZoneSelected=  iGetCookie("ZoneSelected");   ZoneChecked=ZoneSelected;                               //get zone cookie   RegionSelected=iGetCookie("RegionSelected"); RegionChecked=RegionSelected;                           //ER: get region cookie   if(ZoneSelected==null) ZoneSelected=ZoneDefault;                                                             //use zone-default if none selected;  ER: default was 8   if(RegionFromZone.length && RegionSelected==null)       RegionSelected=RegionFromZone[ZoneSelected][0];      //ER: use RegionFromZone option to set RegionSelected   if(RegionFromZoneOverrides)                             RegionSelected=RegionFromZone[ZoneSelected][0];   if(ZoneChecked!=null && RegionFromZoneOvA[ZoneChecked]) RegionSelected=RegionFromZone[ZoneChecked] [0];   if(RegionSelected==null)                                RegionSelected=RegionDefault;                //ER: use region-default, when RegionFromZone not being used   if(RegionFromZone.length && !Element(RegionSelected,RegionFromZone[ZoneSelected])){                  //ER: validity-check, the Zone+Region combination is invalid:      if(ZoneChecked!=null || RegionChecked==null){                                                     //ER: if user has picked Zone or neither, then revise Region         RegionSelected=RegionFromZone[ZoneSelected][0];                                                //ER: revise RegionSelected to make it legal for Zone      }else{                                                                                            //ER: otherwise, revise Zone to achieve consistency         for(var Z=RegionFromZone.length;Z--;) if(Element(RegionSelected, RegionFromZone[Z])) break;    //ER: find a valid Zone Z         if(Z>=0) ZoneSelected=Z;  else DEBUG("RegionFromZone option is invalid");                      //ER: revise ZoneSelected to make it legal for Region      }   }   if(RegionChecked!=null) RegionChecked= RegionSelected;                                               //ER: validity-check RegionChecked   if(ZoneChecked  !=null) ZoneChecked  = ZoneSelected;                                                 //ER: validity-check ZoneChecked   if(TaxRateRegional!=0 && RegionPrompt!="" && !RegionFromZoneOverrides && !(ZoneChecked!=null && RegionFromZoneOvA[ZoneChecked])   ) {}                                                                                                 //ER: leave Region unchecked if user will have to pick   else RegionChecked=RegionSelected;                                                                   //ER: show as checked a Region that suffices   if(ShipTable.length>1 && ZonePrompt!="") {}                                                          //ER: leave Zone unchecked if user will have to pick   else ZoneChecked=ZoneSelected;                                                                       //ER: show as checked a Zone that suffices   //--ER: original version of above was ok for customer selecting Zone first, but would seriously harrass customer trying to select Region before Zone;   // have fixed:  (2) revised validity-checking above;  and reordered, so setting Checked by PromptNotNeeded is done last, and for Zone after Region;   // have fixed:  (3) NewZone routine now deletes Region-cookie if illegal;  NewRegion now deletes Zone-cookie if illegal;  (1) was to revise such cookie;   //                  can continue to regard the presence of a Zone or Region cookie as proof the user has selected (and it hasn't been altered since);   InitPkgQueue();                              //ER: initialize the PkgQueue object (used to compute package-size)   fTotal=0; fTaxG=0; fTaxP=0; g_TotalQty=0;    //initialize subtotal and tax-subtotals   var taxrateP=TaxRateRegional, taxrateG=TaxRate;   if(     RegionSelected==0) {}                                                        //ER: init taxrates based on RegionSelected   else if(RegionSelected==1) {taxrateP=0;}   else                       {taxrateP=0; taxrateG=0;}   for(i=1; i<=iNumberOrdered; ++i){      //ER: considered using TaxableTotal etc, so as to multiply just once;  not essential, provided the rounding-to-cents is deferred  (tho for tax-included it isn't)      var ProdID=Cart[i].ID, QP=Cart[i].QUANTITY*Cart[i].PRICEAVG, taxP, taxG;      if(     PrefEQ(ProdID,PrefNeitherTax  ))  {taxP=0;            taxG=0;}            //ER: PrefNeitherTax (was "n") indicates neither tax applies; was ProdID[0]=="n"      else if(PrefEQ(ProdID,PrefRegionalOnly))  {taxP=QP*taxrateP;  taxG=0;}            //ER: PrefRegionalOnly (was "p") indicates only the regional-tax applies      else if(PrefEQ(ProdID,PrefNationalOnly))  {taxP=0;            taxG=QP*taxrateG;}  //ER: PrefNationalOnly (was "g") indicates only the national-tax applies      else                                      {taxP=QP*taxrateP;  taxG=QP*taxrateG;}  //anything else means both taxes apply      if(DisplayTaxIncluded){                                                           //ER: for tax-included pricing:         taxP=Cents(taxP);  taxG=Cents(taxG);                                           //each tax is rounded to cents for each row, and         Cart[i].PRICEAVG += (taxP + taxG) / Cart[i].QUANTITY;                          //the price-apiece is adjusted to include taxes      }      AddPkgQueueEntry(Cart[i].QUANTITY, new Size(Cart[i].LENGTH,Cart[i].WIDTH,Cart[i].HEIGHT), Cart[i].WEIGHT);        //ER: accumulate for package-size      fTotal+=Cart[i].QUANTITY*Cart[i].PRICEAVG;                                        //accumulate fTotal, the subtotal before (tax)+shipping...      fTaxP+=taxP;  fTaxG+=taxG;                                                        //accumulate tax-subtotals      g_TotalQty+=Cart[i].QUANTITY;                                                     //accumulate total-quantity   }   ComputePackageSize(ZoneSelected);                                                    //ER: compute package-size   fShipping = ComputeShipping(ZoneSelected);                                           //ER: removed if-else since function handles weight being zero   ppTotal=fTotal; ppShipping=fShipping; if(ppTotal==0) {ppTotal=moneyEps; ppShipping=Math.max(ppShipping-moneyEps,0);} //2008-02-09 kludge because PayPal chokes on zero   fTaxG=Cents(fTaxG);  fTaxP=Cents(fTaxP);                                             //ER: round to Cents separately   fTax=fTaxG+fTaxP;  if(DisplayTaxIncluded) fTax=0;                                    //ER: fTax is sum of taxes, however with tax-included pricing use zero   g_TotalCost = fTotal + fShipping + fTax;                                             //compute the grand-total}//------------------------------------------------------------------------// FUNCTION: AddPaymentProcessorFieldsForOneRow// RECEIVES: PP is the payment-processor-code;//           i is the row-nbr, Cart[i] is the info about current row// RETURNS: appends to global string vars  sOutPP and sDescAIO// ER: made this a function to improve maintainability.//------------------------------------------------------------------------function AddPaymentProcessorFieldsForOneRow(PP, i){   var sN="";  if(AppendItemNumToOutput) sN=""+i;                                       //ER: convert i to string once   var ProdNAME = Cart[i].NAME;                                                         //2008-02-07 removed:  + (Cart[i].ADDTLINFO ?"; "+Cart[i].ADDTLINFO :"")   if(PP=="an" || PP=="wp" || PP=="lp"){                //===an/wp/lp===      sDescAIO+= Cart[i].ID + ", " + ProdNAME + ", Qty:"+Cart[i].QUANTITY + "\n";                       //format Description as:  ID, NAME, Qty:QUANTITY<newline>   }else if(PP=="ppa"){                                 //===ppa:PayPal-all-in-one (2008-03-09)===      sDescAIO+= Cart[i].ID + ", " + ProdNAME + ", Qty:"+Cart[i].QUANTITY + (i<iNumberOrdered?"; ":""); //format Description as:  ID, NAME, Qty:QUANTITY;   }   if(PP=="pp" || (PP=="ppa"&&i==iNumberOrdered)){      //===pp:PayPal / ppa+last-item===      var ppNAME= (PP=="ppa" ?sDescAIO :ProdNAME) + (i==iNumberOrdered && ppNotesOnItem ?"; "+strSHIP+" "+ShipTable[ZoneSelected].zone+" "+sComputeShippingNote :""); //2008-02-04 add Shipping-Note on last item;  2008-03-09 support all-in-one      var ppNAME1=ppNAME.substring(0,127);                                              //ER: break into 3 strings according to PayPal limits      var ppNAME2=ppNAME.substring(127,327);                                            //ER: break into 3 strings according to PayPal limits      var ppNAME3=ppNAME.substring(327,527);                                            //ER: break into 3 strings according to PayPal limits      var ppID=Cart[i].ID, ppPRICE=Cart[i].PRICEAVG, ppQUANTITY=Cart[i].QUANTITY;      if(PP=="ppa") {ppID="AIO"; ppPRICE=ppTotal; ppQUANTITY=1;}                        //2008-03-09 for PayPal-all-in-one, supply the subtotal as the price      if(PP=="ppa" && AppendItemNumToOutput) sN=""+1;                                   //2008-03-09 for PayPal-all-in-one, use one as the row-number      sOutPP+=             "<input type=hidden name=\"item_number_"+sN+"\" value=\""+             ppID              + "\">";      sOutPP+=             "<input type=hidden name=\"item_name_"  +sN+"\" value=\""+             ppNAME1           + "\">";      sOutPP+=             "<input type=hidden name=\"amount_"     +sN+"\" value=\""+ moneyFormat(ppPRICE)          + "\">";      sOutPP+=             "<input type=hidden name=\"quantity_"   +sN+"\" value=\""+             ppQUANTITY        + "\">";      if(ppNAME2) sOutPP+= "<input type=hidden name=\"on0_"        +sN+"\" value=\""+             "Info2"           + "\">";      if(ppNAME2) sOutPP+= "<input type=hidden name=\"os0_"        +sN+"\" value=\""+             ppNAME2           + "\">";      if(ppNAME3) sOutPP+= "<input type=hidden name=\"on1_"        +sN+"\" value=\""+             "Info3"           + "\">";      if(ppNAME3) sOutPP+= "<input type=hidden name=\"os1_"        +sN+"\" value=\""+             ppNAME3           + "\">";   }else if(PP=="gc"){                                  //===gc:Google-Checkout===      sOutPP+= "<input type=hidden name=\"item_name_"              +sN+"\" value=\""+             Cart[i].ID        + "\">";      sOutPP+= "<input type=hidden name=\"item_description_"       +sN+"\" value=\""+             ProdNAME          + "\">";      sOutPP+= "<input type=hidden name=\"item_price_"             +sN+"\" value=\""+ moneyFormat(Cart[i].PRICEAVG) + "\">";      sOutPP+= "<input type=hidden name=\"item_quantity_"          +sN+"\" value=\""+             Cart[i].QUANTITY  + "\">";      sOutPP+= "<input type=hidden name=\"item_currency_"          +sN+"\" value=\""+             gcCurrency        + "\">";   }else if(PP=="cgi"){                                 //===cgi:CUSTOM===  was if(HiddenFieldsToCheckout)      sOutPP+= "<input type=hidden name=\"" + OutputItemId         +sN+"\" value=\""+             Cart[i].ID        + "\">";      sOutPP+= "<input type=hidden name=\"" + OutputItemQuantity   +sN+"\" value=\""+             Cart[i].QUANTITY  + "\">";      sOutPP+= "<input type=hidden name=\"" + OutputItemPrice      +sN+"\" value=\""+ moneyFormat(Cart[i].PRICEAVG) + "\">";      sOutPP+= "<input type=hidden name=\"" + OutputItemName       +sN+"\" value=\""+             Cart[i].NAME      + "\">";      sOutPP+= "<input type=hidden name=\"" + OutputItemWeight     +sN+"\" value=\""+             Cart[i].WEIGHT    + "\">";      sOutPP+= "<input type=hidden name=\"" + OutputItemLength     +sN+"\" value=\""+             Cart[i].LENGTH    + "\">";      sOutPP+= "<input type=hidden name=\"" + OutputItemWidth      +sN+"\" value=\""+             Cart[i].WIDTH     + "\">";      sOutPP+= "<input type=hidden name=\"" + OutputItemHeight     +sN+"\" value=\""+             Cart[i].HEIGHT    + "\">";      //utPP+= "<input type=hidden name=\"" + OutputItemAddtlInfo  +sN+"\" value=\""+             Cart[i].ADDTLINFO + "\">";    //yanked 2008-02-07   }}//------------------------------------------------------------------------// FUNCTION: AddPaymentProcessorFieldsFinal// RECEIVES: PP is the payment-processor-code;  global var sDescAIO is the all-in-one-description// RETURNS: appends to global string var  sOutPP// ER: made this a function to improve maintainability.//------------------------------------------------------------------------function AddPaymentProcessorFieldsFinal(PP){   if(PP=="an"){                                        //===an:Authorize.net WebConnect===      sOutPP += "<input type=hidden name=\"x_Version\"      value=\"3.0\">";      sOutPP += "<input type=hidden name=\"x_Show_Form\"    value=\"PAYMENT_FORM\">";      sOutPP += "<input type=hidden name=\"x_Description\"  value=\""+ sDescAIO + "\">";      sOutPP += "<input type=hidden name=\"x_Amount\"       value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";   }else if(PP=="wp"){                                  //===wp:WorldPay===      sOutPP += "<input type=hidden name=\"desc\"           value=\""+ sDescAIO + "\">";      sOutPP += "<input type=hidden name=\"amount\"         value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";   }else if(PP=="lp"){                                  //===lp:LinkPoint===      sOutPP += "<input type=hidden name=\"mode\"           value=\"fullpay\">";      sOutPP += "<input type=hidden name=\"chargetotal\"    value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";      sOutPP += "<input type=hidden name=\"tax\"            value=\""+ MoneySymbol + moneyFormat(fTax) + "\">";      sOutPP += "<input type=hidden name=\"subtotal\"       value=\""+ MoneySymbol + moneyFormat(fTotal) + "\">";      sOutPP += "<input type=hidden name=\"shipping\"       value=\""+ MoneySymbol + moneyFormat(fShipping) + "\">";      sOutPP += "<input type=hidden name=\"desc\"           value=\""+ sDescAIO + "\">";   }else if(PP=="pp" || PP=="ppa"){                     //===pp:PayPal===      var ShpTaxNotes="Shipping+Tax-Notes: "+ShipTable[ZoneSelected].zone+" "+sComputeShippingNote+(TaxRateRegional?" "+RegionTable[RegionSelected]:"");      sOutPP += "<input type=hidden name=\"cmd\"            value=\"_cart\">";                          //2008-02-05: new      sOutPP += "<input type=hidden name=\"upload\"         value=\"1\">";                              //2008-02-05: new      sOutPP += "<input type=hidden name=\"custom\"         value=\""+ ShpTaxNotes              +"\">"; //shipping+tax-notes via "custom" => won't show on the emails      sOutPP += "<input type=hidden name=\"tax_cart\"       value=\""+ moneyFormat(fTax)        +"\">";      sOutPP += "<input type=hidden name=\"handling_cart\"  value=\""+ moneyFormat(ppShipping)  +"\">"; //2008-02-09 ppShipping for the zero-subtotal kludge      sOutPP += "<input type=hidden name=\"no_note\"        value=\""+ "1"                      +"\">"; //use NO_NOTE until PayPal fixes NOTE/SpecialInstructions-support   }else if(PP=="gc"){                                  //===gc:GoogleCheckout===      if(fTax!=0){                                                              //Google forces us to supply TAX as an item...        var sN=""+(iNumberOrdered+1);                                           //get next row-number        sOutPP+="<input type=hidden name=\"item_name_"       +sN+"\" value=\""+ "TAX"                                                           + "\">";        sOutPP+="<input type=hidden name=\"item_description_"+sN+"\" value=\""+ "Tax"+(TaxRateRegional?" for "+RegionTable[RegionSelected]:"")  + "\">";        sOutPP+="<input type=hidden name=\"item_price_"      +sN+"\" value=\""+ moneyFormat(fTax)                                               + "\">";        sOutPP+="<input type=hidden name=\"item_quantity_"   +sN+"\" value=\""+ "1"                                                             + "\">";        sOutPP+="<input type=hidden name=\"item_currency_"   +sN+"\" value=\""+ gcCurrency                                                      + "\">";      }      sOutPP += "<input type=hidden name=\"ship_method_price_1\"     value=\""+ moneyFormat(fShipping)                                          + "\">";      sOutPP += "<input type=hidden name=\"ship_method_currency_1\"  value=\""+ gcCurrency                                                      + "\">";      sOutPP += "<input type=hidden name=\"ship_method_name_1\"      value=\""+ ShipTable[ZoneSelected].zone+" "+sComputeShippingNote           + "\">";      sOutPP += "<input type=hidden name=\"_charset_\"/>";                      //Google says this is required, though no alternatives(?)   }else if(PP=="cgi"){                                 //===cgi:CUSTOM===      //was if(HiddenFieldsToCheckout)      sOutPP += "<input type=hidden name=\""+OutputOrderSubtotal+"\" value=\""+ MoneySymbol + moneyFormat(fTotal)                               + "\">";      sOutPP += "<input type=hidden name=\""+OutputOrderShipping+"\" value=\""+ MoneySymbol + moneyFormat(fShipping)                            + "\">";      sOutPP += "<input type=hidden name=\""+OutputOrderTax     +"\" value=\""+ MoneySymbol + moneyFormat(fTax)                                 + "\">";      sOutPP += "<input type=hidden name=\""+OutputOrderTotal   +"\" value=\""+ MoneySymbol + moneyFormat(fTotal + fShipping + fTax)            + "\">";      sOutPP += "<input type=hidden name=\""+OutputOrderZone    +"\" value=\""+ ShipTable[ZoneSelected].zone+" "+sComputeShippingNote           + "\">";      sOutPP += "<input type=hidden name=\""+OutputOrderRegion  +"\" value=\""+ (TaxRateRegional?RegionTable[RegionSelected]:"")                + "\">";  //ER: new   }   DEBUG8(sOutPP);                                      //2008-03-09 separate sOutput and sOutPP makes for nicer debugging}//------------------------------------------------------------------------// FUNCTION: AddTaxSubtotalLines// RECEIVES: INC string - is either empty-string or strINCLUDEDINTOTAL// RETURNS: appends to global var sOutput// ER: made this a function to improve maintainability.//------------------------------------------------------------------------function AddTaxSubtotalLines(INC,COL,BEG,END){  if(COL==null)COL=6;  if(BEG==null)BEG="<B>";  if(END==null)END="</B>";   if(TaxNames.length>=2){      if(fTaxP) sOutput +=          "<TR><TD CLASS=\"noptotal\" COLSPAN="+COL+">"+BEG+strTAX+"-"+TaxNames[0]+INC+"&nbsp; "+(TaxRateRegional?RegionTable[RegionSelected]:"")+END+"</TD>" +         "<TD CLASS=\"noptotal\" ALIGN=RIGHT>"+BEG + MoneySymbol + moneyFormat(fTaxP) +END+"</TD></TR>";      if(fTaxG) sOutput +=          "<TR><TD CLASS=\"noptotal\" COLSPAN="+COL+">"+BEG+strTAX+"-"+TaxNames[1]+INC+"&nbsp; "+(TaxRateRegional?RegionTable[RegionSelected]:"")+END+"</TD>" +         "<TD CLASS=\"noptotal\" ALIGN=RIGHT>"+BEG + MoneySymbol + moneyFormat(fTaxG) +END+"</TD></TR>";   }else{      if(fTaxP+fTaxG) sOutput +=          "<TR><TD CLASS=\"noptotal\" COLSPAN="+COL+">"+BEG+strTAX+INC+"&nbsp; "+(TaxRateRegional?RegionTable[RegionSelected]:"")+END+"</TD>" +         "<TD CLASS=\"noptotal\" ALIGN=RIGHT>"+BEG + MoneySymbol + moneyFormat(fTaxP+fTaxG) +END+"</TD></TR>";   }}//------------------------------------------------------------------------// FUNCTION: ManageCart// PARAMETERS: Null// PURPOSE: Draw current cart product table on HTML page//------------------------------------------------------------------------function ManageCart() {   //ER: replaced  fWeight-->PkgAsOne.weight;  sTotal-->moneyFormat(fTotal);  sTax-->moneyFormat(fTax);  sShipping-->moneyFormat(fShipping);   //ER: iNumberOrdered, ZoneSelected, ZoneChecked, RegionSelected, RegionChecked  are now global vars set by ReadCartComputePrices routine;   //ER: fTotal, fShipping, fTax, fTaxG, fTaxP, g_TotalCost                        are now global vars set by ReadCartComputePrices routine;   //   var MoreState=iGetCookie("MoreState"); if(MoreState==null) MoreState= (DisplayWtColumn?1:0)*2 + (DisplaySzColumn?1:0);       //ER: MoreState, for DynamicWtSzColumns   ReadCartComputePrices();     //ER: new   sDescAIO="";                 //initialize the All-in-one-Description for cart-less payment-processors   sOutPP="";   sOutput = "<TABLE CELLSPACING=0 CELLPADDING=2 BORDER=0 CLASS=\"nopcart\"><TR>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strILabel+"</B></TD>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strDLabel+"</B></TD>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strQLabel+"</B></TD>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strPLabel+"</B></TD>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+(MoreState&2?strWLabel:"&nbsp;")+"</B></TD>"+  //ER: was: (DisplayShippingColumn?"<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strSLabel+"</B></TD>":"") +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+(MoreState&1?strZLabel:"&nbsp;")+"</B></TD>"+  //ER: new      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strRLabel+"</B></TD></TR>";   if(iNumberOrdered==0)sOutput+="<TR><TD COLSPAN=7 CLASS=\"nopentry\"><CENTER><BR><B>"+strCartEmpty+"</B><BR><BR></CENTER></TD></TR>"; //ER: now subject to translation   for(var i=1; i<=iNumberOrdered; ++i){      var sCLASS="nopentry"; if(Math.round(i/2)==(i/2)) sCLASS="nopeven";               //ER: to eliminate duplication of code for even/odd background on rows      sOutput += "<TR><TD CLASS=\""+sCLASS+"\" ALIGN=CENTER>" + Cart[i].ID + "</TD>";      //if(Cart[i].ADDTLINFO)   sOutput += "<TD CLASS=\""+sCLASS+"\">" + Cart[i].NAME + " - <I>"+ Cart[i].ADDTLINFO + "</I></TD>";  else//yanked 2008-02-07                                sOutput += "<TD CLASS=\""+sCLASS+"\">" + Cart[i].NAME + "</TD>";      if(DisplayChangeQty)      sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=CENTER><INPUT TYPE=TEXT NAME=Q SIZE=2 VALUE=\"" + Cart[i].QUANTITY + "\" onChange=\"ChangeQuantity("+i+", this.value);\"></TD>";      else                      sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=CENTER>" + Cart[i].QUANTITY + "</TD>";      sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>"+ MoneySymbol + moneyFormat(Cart[i].PRICEAVG)+strEA+"</TD>";            //ER: "/ea" now subject to translation      if(MoreState&2)           sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>"+ WtRnd(Cart[i].WEIGHT)+WTUNITS+"</TD>";      //ER: display WEIGHT column (was "shipping" column)      else                      sOutput += "<TD CLASS=\""+sCLASS+"\"></TD>";                                                    //ER: N/A->empty-string      if(MoreState&1)           sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>&nbsp; "+ Cart[i].LENGTH+"x"+Cart[i].WIDTH+"x"+Cart[i].HEIGHT+SZUNITS + "</TD>";       //ER: new      else                      sOutput += "<TD CLASS=\""+sCLASS+"\"></TD>";      sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>&nbsp; <input type=button value=\""+strRButton+"\" onClick=\"RemoveFromCart("+i+")\" class=\"nopbutton\"></TD>"; //ER: was align=CENTER      sOutput += "</TR>";      //--ER: ManageCart producing PaymentProcessor-style hidden-fields is new (for ONE-step checkout);  the NopDesign version only offers "cgi" style here      AddPaymentProcessorFieldsForOneRow(PaymentProcessor, i);                          //ER: add payment-processor form-fields for row-i   }   if(fShipping + fTax != 0){                                                           //ER: avoid showing SUBTOTAL when same as TOTAL (2008-02-07)      sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=6><B>"+strSUB+"</B></TD>";      sOutput += "<TD CLASS=\"noptotal\" COLSPAN=1 ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal) + "</B></TD></TR>";   }   if(DisplayPkgAttrRow && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){   //ER: new option controls this line showing + omit if weightless & sizeless      var MoreLessButton=(MoreState==DynamicWtSzColumns?strLButton:strMButton);         //ER: initialize according to MoreState      var bW=MoreState&2, bw=bW^2, sW="&nbsp; " +PkgAsOne.weight+WTUNITS;               //ER: total package WEIGHT will go in WEIGHT-col, or in 1st      var bS=MoreState&1, bs=bS^1, sS="&nbsp; " +SizeStr(PkgAsOne.size)+SZUNITS;        //ER: total package SIZE will go in SIZE-col, or in 1st      if(DynamicWtSzColumns) {bw&=DynamicWtSzColumns; bs&=DynamicWtSzColumns;}          //ER: only show in first-col if ever shown  (Wt-only but no Dynamic-cols??)      sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strWTSZTOT+(bw?sW:"")+(bs?sS:"")+"</B></TD>";       //ER: first column gets attribs when in LessInfo state      sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + (bW?sW:"") +"</B></TD>";                            //ER: package weight column      sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + (bS?sS:"") +"</B></TD>";                            //ER: package size column      if(DynamicWtSzColumns)  sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT>&nbsp; <input type=button value=\""+MoreLessButton+"\" onClick=\"MoreLessInfo()\" class=\"nopbutton\"></TD>";      //ER: new      else                    sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT></TD>";      sOutput += "</TR>";   }   //Display the Shipping-Zone choices;  ER: Zone Defns are now table-driven; see description up front   //ER: note: show no button as checked if user will be prompted for Zone (see the setting of ZoneChecked above)   if(ShipTable.length>1 && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){  //ER: if more than one zone then customer must be able to pick      sOutput += "<TR><TD CLASS=\"nopship\"><B>"+strSHIPPINGZONE+"</B></TD>";           //ER: was:ALIGN=CENTER  was:"UPS<BR>SHIPPING<BR>ZONE" now subject to translation      sOutput += "<TD CLASS=\"nopship\" COLSPAN=6>";      for(var z=0; z<ShipTable.length; z++) sOutput+= "<input type=radio name=\"ZONE\" value=\""+z+"\"" +         (z==ZoneChecked?" checked":"") +                                               //ER: now adding the "checked" attrib here         " onClick=\"NewZone(this.value)\">"+ShipTable[z].zone+"<br>";                  //ER: ComputeShipping-->NewZone  in onClick      sOutput += "</TD></TR>";   }   if(DisplayShippingRow && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){      sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=6><B>" + strSHIP+"&nbsp; " + ShipTable[ZoneSelected].zone+"</B>&nbsp;&nbsp;" + sComputeShippingNote +"</TD>";      sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fShipping) + "</B></TD></TR>";   }   //ER: have overhauled the way the Region-choices are shown & how changes are handled;  now very similar to the Zone-choices...   //ER: note: RegionFromZoneOverrides means Region is always derived from Zone, so no user-choosing needed   //ER: note: show no button as checked if user will be prompted for region (see the setting of RegionChecked above)   if(TaxRateRegional!=0 && !RegionFromZoneOverrides && iNumberOrdered){                //ER: difference iNumberOrdered!=0 vs iNumberOrdered was a mystery...      sOutput += "<TR><TD CLASS=\"nopship\"><B>"+strTAXABLEREGION+"</B></TD>";          //ER: was:ALIGN=CENTER      sOutput += "<TD CLASS=\"nopship\" COLSPAN=6>";      for(var R=0; R<RegionTable.length; R++) sOutput+= "<input type=radio name=\"TAX\" value=\""+R+"\"" +         (R==RegionChecked?" checked":"") +                                             //ER: add the checked attrib         " onClick=\"NewRegion(this.value)\">"+RegionTable[R]+"<br>";      sOutput += "</TD></TR>";   }   if(DisplayTaxRow && iNumberOrdered && !DisplayTaxIncluded){                          //ER: show tax line(s) for NON-tax-included-pricing      AddTaxSubtotalLines("");   }   sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=6><B>"+strTOT+"</B></TD>";            //ER: now show the TOTAL line even when TaxRateRegional!=0   sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal + fShipping + fTax) + "</B></TD></TR>";   if(DisplayTaxRow && iNumberOrdered && DisplayTaxIncluded){                           //ER: show tax line(s) for TAX-INCLUDED-pricing (new)      AddTaxSubtotalLines(" "+strINCLUDEDINTOTAL, 6, "<i>", "</i>");   }   if(DisplayTaxRow && gVat && RegionSelected<=1 && ShipTaxName!="")  sOutput +=        //ER: show tax included in the shipping-charge, to customer in same country      "<TR><TD CLASS=\"noptotal\" COLSPAN=6><i>" + ShipTaxName +"</i></TD>" +       "<TD CLASS=\"noptotal\" ALIGN=RIGHT>" + MoneySymbol+moneyFormat(gVat) +"</TD></TR>";   sOutput += "</TABLE>";   //   //--ER: ManageCart producing PaymentProcessor-style hidden-fields is new (to enable ONE-step checkout);  the NopDesign version only offers "cgi" style here   AddPaymentProcessorFieldsFinal(PaymentProcessor);                                    //ER: add the final (cart-wide) payment-processor fields   document.write(sOutput+sOutPP);   document.close();}//------------------------------------------------------------------------// FUNCTION:    ValidateCart// PARAMETERS:  Form to validate// RETURNS:     true/false// PURPOSE:     Validate the managecart form//------------------------------------------------------------------------function ValidateCart(theForm){   if(isNaN(g_TotalCost)){      alert(strTotalNaN);               //ER: was NoQtyPrompt      return false;   }   if(g_TotalCost < MinimumOrder){      alert(MinimumOrderPrompt);      return false;   }   //ER: because of my defaults, now need to use the presence of cookies to tell whether user has made a Zone or Region selection;   //ER: 1st test was: !RadioChecked(theForm.ZONE)   //ER: 2nd test was: !RadioChecked(theForm.TAX)  -- actually it was: !RadioChecked(eval("theForm."+OutputOrderTax))  before simplifying OutputOrderTax-->"TAX"   var N=iGetCookie("NumberOrdered",0);  if(N==0) return;               //skip the following if cart is empty;  only needed for the perverse use of MinimumOrder==0   var ZoneCookie=   iGetCookie("ZoneSelected");   var RegionCookie= iGetCookie("RegionSelected");   if(ZoneCookie==null && (PkgAsOne.weight+PkgAsOne.size.height) && ShipTable.length>1 && ZonePrompt!=""){      alert(ZonePrompt);      return false;   }   if(RegionCookie==null && TaxRateRegional!=0 && RegionPrompt!="" && !RegionFromZoneOverrides && !(ZoneCookie!=null && RegionFromZoneOvA[ZoneCookie])){      alert(RegionPrompt);      return false;   }   return true;}//------------------------------------------------------------------------// FUNCTION: CheckoutCart// PARAMETERS: Null// PURPOSE: Draw current cart product table on HTML page for checkout;// NOTE: produces a simpler view of the cart, compared to ManageCart, // and one without controls (Remove-from-Cart etc).//------------------------------------------------------------------------function CheckoutCart() {   ReadCartComputePrices();     //ER: new   sDescAIO="";                 //initialize the all-in-one-Description for cart-less payment-processors   sOutPP="";   sOutput = "<TABLE CELLSPACING=0 CELLPADDING=2 BORDER=0 CLASS=\"nopcart\"><TR>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strILabel+"</B></TD>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strDLabel+"</B></TD>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strQLabel+"</B></TD>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strPLabel+"</B></TD>" +      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strALabel+"</B></TD></TR>";            //ER: strALabel now shown unconditionally   for(var i=1; i<=iNumberOrdered; ++i){      var sCLASS="nopentry"; if(Math.round(i/2)==(i/2)) sCLASS="nopeven";               //ER: to eliminate duplication of code for even/odd background on rows      sOutput += "<TR><TD CLASS=\""+sCLASS+"\" ALIGN=CENTER>" + Cart[i].ID + "</TD>";      //if(Cart[i].ADDTLINFO!="") sOutput+= "<TD CLASS=\""+sCLASS+"\">" + Cart[i].NAME + " - <I>"+ Cart[i].ADDTLINFO + "</I></TD>";  else//yanked 2008-02-07      sOutput+="<TD CLASS=\""+sCLASS+"\">" + Cart[i].NAME + "</TD>";      sOutput+="<TD CLASS=\""+sCLASS+"\" ALIGN=CENTER>" + Cart[i].QUANTITY + "</TD>";      sOutput+="<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>"+MoneySymbol+moneyFormat(Cart[i].PRICEAVG)+strEA+"</TD>";                 //ER: "/ea" now subject to translation      sOutput+="<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>"+MoneySymbol+moneyFormat(Cart[i].QUANTITY*Cart[i].PRICEAVG)+"</TD></TR>"; //ER: now shown unconditionally      AddPaymentProcessorFieldsForOneRow(PaymentProcessor2, i);                         //ER: add payment-processor form-fields for row-i   }   sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strSUB+"</B></TD>";   sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal) + "</B></TD></TR>";   if(DisplayShippingRow){      //sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strWTSZTOT+"</B></TD>";      //sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + fWeight + WTUNITS + "</B></TD>";      sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>" + strSHIP+"&nbsp; "+ShipTable[ZoneSelected].zone+"</B></TD>";        //ER: removed "for"      sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fShipping) + "</B></TD></TR>";   }   if(DisplayTaxRow && !DisplayTaxIncluded){                                            //ER: removed ||TaxRateRegional!=0;  now only for NON-tax-included pricing      AddTaxSubtotalLines("", 4);   }   sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strTOT+"</B></TD>";   sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal + fShipping + fTax) + "</B></TD></TR>";   if(DisplayTaxRow && DisplayTaxIncluded){                                             //ER: show tax line(s) for TAX-INCLUDED-pricing (new)      AddTaxSubtotalLines(" "+strINCLUDEDINTOTAL, 4, "<i>", "</i>");   }   if(DisplayTaxRow && gVat && RegionSelected<=1 && ShipTaxName!="")  sOutput +=        //ER: show tax included in the shipping-charge, to customer in same country      "<TR><TD CLASS=\"noptotal\" COLSPAN=4><i>" + ShipTaxName +"</i></TD>" +       "<TD CLASS=\"noptotal\" ALIGN=RIGHT>" + MoneySymbol+moneyFormat(gVat) +"</TD></TR>";   sOutput+= "</TABLE>";   AddPaymentProcessorFieldsFinal(PaymentProcessor2);                                   //ER: add the final (cart-wide) payment-processor fields   document.write(sOutput+sOutPP);   document.close();}//------------------------------------------------------------------------// FUNCTION: Cart_is_empty// RETURNS: true/false//------------------------------------------------------------------------function Cart_is_empty(){   iNumberOrdered=iGetCookie("NumberOrdered",0);                //get the nbr-rows-in-cart cookie   return iNumberOrdered==0;}//------------------------------------------------------------------------// FUNCTION: Print_total// PARAMETERS: none// PURPOSE: Display cost currently racked up by shopper, on the HTML page//------------------------------------------------------------------------function Print_total(){   ReadCartComputePrices();   document.write(moneyFormat(fTotal));}//------------------------------------------------------------------------// FUNCTION: Print_number_items// PARAMETER: true/false - true to get "item"/"items" appended;  use false in any non-English application// PURPOSE: Display number of items currently racked up by shopper, on the HTML page//------------------------------------------------------------------------function Print_number_items(Verbose){   ReadCartComputePrices();   sOutput= "" + g_TotalQty;   if(Verbose) sOutput+= (g_TotalQty==1?" item":" items");   document.write(sOutput);}Print_total_products = Print_number_items;      //support the old name//------------------------------------------------------------------------// FUNCTION: Print_cart_summary// PARAMETERS: strings to follow QTY singular, follow QTY plural, precede AMT;  may be omitted in an English application// PURPOSE: Display number of items in the cart and their total cost, on the HTML page//------------------------------------------------------------------------function Print_cart_summary(B1,B2,C){  if(B1==null)B1=" item"; if(B2==null)B2=" items"; if(C==null)C=", at a cost of ";   ReadCartComputePrices();   sOutput= "" + g_TotalQty + (g_TotalQty==1?B1:B2) + C + MoneySymbol+moneyFormat(fTotal);   document.write(sOutput);}//========================================================================// END NopDesign + ER Shopping-Cart//========================================================================