webgui/www/extras/shop/cart.js
2010-10-28 13:42:55 -07:00

509 lines
17 KiB
JavaScript

/*global _, window, document, YAHOO */
(function () {
function clone(o) {
function F() {}
F.prototype = o;
return new F();
}
function formatCurrency(n) {
return parseFloat(n.toString()).toFixed(2);
}
function validAddress(a) {
return a.label &&
a.firstName &&
a.lastName &&
a.address1 &&
a.city &&
a.state &&
a.code &&
a.country;
}
function addressIdCounts(id) {
return id &&
id !== 'new_address' &&
id !== 'update_address';
}
function fillIn(dom, a) {
_.each(a, function (v, k) {
if (dom[k]) {
dom[k].value = v || '';
}
});
}
var Cart = {
attachAddressBlurHandlers: function (name) {
var els = this.elements[name],
label = els.label,
addr = els.address1,
fields = _.values(els),
handler = this.createAddressBlurHandler(name);
this.event.on(fields, 'focusout', handler);
this.event.on(addr, 'focusout', function () {
if (!label.value) {
label.value = addr.value;
label.blur();
}
});
},
attachAddressSelectHandler: function (name) {
var e = this.elements.dropdowns[name],
handler = this.createAddressFiller(name);
this.event.on(e, 'change', handler);
},
attachPerItemShippingChangeHandler: function (select) {
this.event.on(select, 'change',
_.bind(this.setCartItemShippingId, this, select));
},
// Updates the total fields based on information already contained in
// this.prices
calculateSummary: function () {
var e = this.elements,
prices = this.prices,
shipper = e.shipper,
shipping = shipper && prices.shipping[shipper.value],
shipPrice = (shipping ?
(shipping.hasPrice ?
parseFloat(shipping.price) :
0)
: 0),
tax = parseFloat(prices.tax),
subtotal = parseFloat(prices.subtotal),
beforeCredit = tax + subtotal + shipPrice,
creditAvail = parseFloat(e.credit.available.innerHTML),
creditUsed = Math.min(beforeCredit, creditAvail),
afterCredit = beforeCredit - creditUsed;
e.credit.used.innerHTML = formatCurrency(creditUsed);
e.total.innerHTML = formatCurrency(afterCredit);
},
computePerItemShippingOptions: function () {
var self = this,
shipping = this.elements.dropdowns.shipping,
selectedMain = shipping.value,
validOptions = _.select(shipping.options, function (o) {
var v = o.value;
return addressIdCounts(v) &&
v !== selectedMain;
});
_.each(this.getPerItemShippingDropdowns(), function (d) {
var selected = d.value;
_(d.options).chain().filter(function (o) {
return o.value;
}).each(_.bind(d.removeChild, d));
_.each(validOptions, function (o) {
d.appendChild(o.cloneNode(true));
});
// The idea here is to reselect the option that was selected,
// if it's still valid. If not, we have to tell the backend
// as well.
d.value = selected;
if (d.value !== selected) {
d.value = '';
self.setCartItemShippingId(d);
}
});
},
connect: YAHOO.util.Connect,
copyBilling: function () {
var self = this,
e = this.elements,
d = e.dropdowns;
d.shipping.value = d.billing.value;
this.getSelectAddress(d.billing, function (address) {
fillIn(e.shipping, address);
self.computePerItemShippingOptions();
self.updateSummary();
});
},
create: function (args) {
var self = clone(this);
self.init(args);
},
createAddressBlurHandler: function (name) {
var self = this,
other = name === 'billing' ? 'shipping' : 'billing',
e = this.elements[name],
o = this.elements[other],
c = this.addressCache;
return function () {
var address = {},
label = e.label.value,
copy = o.label && o.label.value === label,
cache = c[label],
dirty;
if (!cache) {
cache = c[label] = {};
}
_.each(e, function (v, k) {
v = v.value || '';
address[k] = v;
if (cache[k] !== v) {
dirty = true;
cache[k] = v;
}
if (copy) {
o[k].value = v;
}
});
if (dirty && validAddress(address)) {
self.saveAddress(address, name);
}
};
},
createAddressFiller: function (name) {
var self = this,
e = this.elements[name],
select = this.elements.dropdowns[name];
return _.bind(this.getSelectAddress, this, select, function (a) {
fillIn(e, a);
if (name === 'billing' && self.sameShipping()) {
self.copyBilling();
}
self.updateSummary();
});
},
dom: YAHOO.util.Dom,
formatAddress: function (a) {
var et = _.template('<a href="mailto:<%= email %>"><%= email %>'),
csz = _.template('<%= city %>, <%= state %> <%= code %>');
return _.compact([
' ',
[a.firstName, a.middleName, a.lastName].join(' '),
a.address1, a.address2, a.address3,
csz(a),
a.country,
a.phone,
a.email && et(a)
]).join('<br />');
},
getPerItemShippingDropdowns: function () {
return this.dom.getElementsByClassName('itemAddressMenu', 'select');
},
getSelectAddress: function (select, callback) {
var self = this,
id = select.value,
label = select.options[select.selectedIndex].text,
c = this.addressCache,
cache = c[label];
if (cache) {
callback(cache);
}
else {
this.request('GET', {
shop : 'address',
method : 'ajaxGetAddress',
addressId : id
},
function (o) {
var address = self.json.parse(o.responseText);
c[address.label] = address;
callback(address);
});
}
},
event: YAHOO.util.Event,
init: function (args) {
// this.elements is our cache of dom objects. We're passed in an
// object with ids, and we want to replace those ids with actual
// dom references.
function getElements(o) {
_.each(o, function (v, k) {
if (typeof v === 'object') {
getElements(v);
}
else {
o[k] = document.getElementById(v);
}
});
}
var self = this,
e = args.elements,
f = document.getElementById('wgCartId'),
checks = f.sameShippingAsBilling,
sameChange;
getElements(e);
this.elements = e;
this.prices = null;
this.addressCache = {};
this.baseUrl = args.baseUrl;
this.attachAddressBlurHandlers('billing');
this.attachAddressSelectHandler('billing');
// if checks is false, we don't have the shipping address form on
// this page (because none of the items in the cart require
// shipping)
if (checks) {
e.same = checks[0];
sameChange = _.bind(this.useSameShippingAddressChange, this);
this.event.on(checks, 'change', sameChange);
sameChange();
this.attachAddressBlurHandlers('shipping');
this.attachAddressSelectHandler('shipping');
_.each(
this.getPerItemShippingDropdowns(),
_.bind(self.attachPerItemShippingChangeHandler, self)
);
this.event.on(e.dropdowns.shipping, 'change', function () {
self.computePerItemShippingOptions();
});
self.computePerItemShippingOptions();
}
else {
delete e.shipping;
}
this.event.on(e.shipper, 'change',
this.calculateSummary, null, this);
this.updateSummary();
},
json: YAHOO.lang.JSON,
saveAddress: function (address, name) {
var self = this,
label = address.label;
this.request('POST', {
shop : 'address',
method : 'ajaxSave',
address : this.json.stringify(address)
},
function (o) {
var id = o.responseText,
d = self.elements.dropdowns;
if (!id.match(/^[A-Za-z0-9_-]{22}$/)) {
alert('Error: bad response trying to save address.');
return;
}
function updateOne(dropdown) {
var opt = _.detect(dropdown.options, function (o) {
return o.text === label;
});
if (!opt) {
opt = document.createElement('option');
opt.innerHTML = label;
dropdown.appendChild(opt);
}
opt.value = id;
}
updateOne(d.billing);
if (d.shipping) {
updateOne(d.shipping);
if (name === 'billing' && self.sameShipping()) {
self.copyBilling();
}
else {
self.computePerItemShippingOptions();
}
}
d[name].value = id;
self.updateSummary();
});
},
// Like calling calculateSummary, except that it will first fetch
// price information from the server. This should be called when the
// address information has changed, and at least once on page load (so
// we have an initial this.prices to work with)
updateSummary: function () {
var self = this,
e = this.elements,
tax = e.tax,
shipper = e.shipper,
d = e.dropdowns,
billing = d.billing.value,
selected, shipping,
params = {
shop: 'cart',
method: 'ajaxPrices'
};
if (addressIdCounts(billing)) {
params.billingId = billing;
}
if (shipper) {
selected = shipper.value; // save so we can restore later
shipping = d.shipping.value;
if (this.sameShipping()) {
params.shippingId = params.billingId;
} else if (addressIdCounts(shipping)) {
params.shippingId = shipping;
}
}
this.request('GET', params, function (o) {
var response = self.json.parse(o.responseText);
if (response.error) {
return;
}
if (shipper) {
// We need to repopulate the shipper options dropdown
// (but only if we have a shipper at all)
_(shipper.options).chain().select(function (o) {
return o.value;
}).each(function (o) {
o.parentNode.removeChild(o);
});
_.each(response.shipping, function (o, id) {
var opt = document.createElement('option'),
label = o.label;
if (o.hasPrice) {
label += ' (' + formatCurrency(o.price) + ')';
}
opt.innerHTML = label;
opt.value = id;
shipper.appendChild(opt);
});
shipper.value = selected;
}
self.prices = response;
tax.innerHTML = formatCurrency(response.tax);
self.calculateSummary();
});
},
// This is a very thin layer on top of YAHOO.util.Connect.asyncRequest.
request: function (method, params, success) {
var url = this.baseUrl,
cb = { success: success },
query = _(params).map(function (v, k) {
return [k, escape(v)].join('=');
}).join('&');
if (method === 'GET') {
this.connect.asyncRequest(method, url + '?' + query, cb);
}
else {
this.connect.asyncRequest(method, url, cb, query);
}
},
sameShipping: function () {
var same = this.elements.same;
return same && same.checked;
},
setCartItemShippingId: function (select) {
var self = this, parent = select.parentNode;
function setText(t) {
parent.innerHTML = t;
parent.insertBefore(select, parent.firstChild);
}
this.request('POST', {
shop : 'cart',
method : 'ajaxSetCartItemShippingId',
itemId : select.id.match(/itemAddress_(.*)_formId/)[1],
addressId : select.value
}, function () {
self.updateSummary();
if (select.value) {
self.getSelectAddress(select, function (address) {
setText(self.formatAddress(address));
});
}
else {
setText('');
}
});
},
useSameShippingAddressChange: function () {
var e = this.elements,
disable = this.sameShipping(),
drops = e.dropdowns;
_.each(e.shipping, function (v, k) {
v.disabled = disable;
});
drops.shipping.disabled = disable;
if (disable && addressIdCounts(drops.billing.value)) {
this.copyBilling();
}
}
},
addressParts = [
'label', 'firstName', 'lastName', 'organization',
'address1', 'address2', 'address3', 'city', 'state',
'code', 'country', 'phoneNumber', 'email'
],
elements = {
shipper : 'shipperId_formId',
tax : 'taxWrap',
total : 'totalPriceWrap',
credit : {
available : 'inShopCreditAvailableWrap',
used : 'inShopCreditDeductionWrap'
},
dropdowns : {
billing : 'billingAddressId_formId',
shipping: 'shippingAddressId_formId'
}
};
function addAddressKind(name) {
var obj = elements[name] = {};
_.each(addressParts, function (key) {
obj[key] = name + '_' + key + '_formId';
});
}
addAddressKind('billing');
addAddressKind('shipping');
Cart.event.onDOMReady(function () {
Cart.create({
baseUrl : window.location.pathname,
elements : elements
});
});
}());