Skip to content

Instantly share code, notes, and snippets.

@brianjmiller
Created April 6, 2011 18:13
Show Gist options
  • Save brianjmiller/906188 to your computer and use it in GitHub Desktop.
Save brianjmiller/906188 to your computer and use it in GitHub Desktop.
YUI3 Cart Builder
YUI.add(
"custom-manage-cart_builder",
function(Y) {
var base_new_line_config = {
qty: 0,
subtotal: 0,
display_sku: '',
product_desc: '',
variant_id: null,
variant_price: 0,
variant_desc: '',
price_adj_amt: 0,
is_combinable: null,
delivery_ls_id: null,
delivery_price: 0,
delivery_price_adj_amt: 0,
delivery_price_combined_multiplier: 0,
delivery_price_uncombined_multiplier: 0
};
var calculate_record_subtotal = function (record) {
var subtotal = 0;
var record_data = record.get("data");
subtotal += record_data.qty * record_data.variant_price;
subtotal += record_data.qty * record_data.delivery_price;
subtotal += record_data.price_adj_amt;
subtotal += record_data.delivery_price_adj_amt;
return subtotal;
};
//
// leverage a record set object to store our like items,
// need ability to add/remove records, need a record to
// be displayed with a template and need auto complete
// to function on the SKU identifier
//
var Clazz = Y.namespace("Bikes.Manage").CartBuilder = Y.Base.create(
"custom_manage_cart_builder",
Y.IC.RendererBase,
[],
{
_recordset: null,
_table_node: null,
_table_header_node: null,
_table_delivery_combinations_body_node: null,
_table_total_body_node: null,
_subtotal_node: null,
_button_add: null,
_button_clear: null,
initializer: function (config) {
Y.log(Clazz.NAME + "::initializer");
this._table_node = Y.Node.create('<table style="width: 98%;"></table>');
this._table_header_node = Y.Node.create('<thead><tr><th></th><th>Product Description</th><th>Variants</th><th>SKU</th><th>Qty</th><th>Price</th><th>Subtotal</th></thead>');
this._subtotal_node = Y.Node.create('<td class="emphasized ledger_value"></td>');
var total_row_node = Y.Node.create('<tr><td colspan="6" class="emphasized" style="text-align: right; border-width: 0px;">Subtotal: </td></tr>');
total_row_node.append(this._subtotal_node);
this._table_total_body_node = Y.Node.create('<tbody class="cart_builder_totals"></tbody>');
this._table_total_body_node.append(total_row_node);
//
// setting the content to an empty row here is basically a hack because FF
// removes the borders on the table when it has an empty tbody
//
this._table_delivery_combinations_body_node = Y.Node.create('<tbody class="cart_builder_delivery_combinations"><tr></tr></tbody>');
this._recordset = new Y.Recordset ();
},
destructor: function () {
Y.log(Clazz.NAME + "::destructor");
this._table_node = null;
this._recordset = null;
},
renderUI: function () {
Y.log(Clazz.NAME + "::renderUI");
Clazz.superclass.renderUI.apply(this, arguments);
this._button_add = Y.Node.create('<button>Add Line</button>');
this._button_add.on(
"click",
function (e) {
e.preventDefault();
this.addLine();
},
this
);
this.get("contentBox").append(this._button_add);
this._button_clear = Y.Node.create('<button>Clear</button>');
this._button_clear.on(
"click",
function (e) {
e.preventDefault();
this.clearLines();
},
this
);
this.get("contentBox").append(this._button_clear);
this.get("contentBox").append(this._table_node);
},
bindUI: function () {
Y.log(Clazz.NAME + "::bindUI");
this.after("subtotalChange", this._afterSubtotalChange, this);
this.after("delivery_combinationsChange", this._afterDeliveryCombinationsChange, this);
this._recordset.after( "add", this._afterAddRecord, this );
this._recordset.after( "empty", this._afterEmptyRecords, this );
// add any initial blank line(s)
if (this.get("minimum_lines") > 0) {
this._table_node.append(this._table_header_node);
this._table_node.append(this._table_delivery_combinations_body_node);
this._table_node.append(this._table_total_body_node);
for (i = 0; i < this.get("minimum_lines"); i++) {
this.addLine();
}
}
},
syncUI: function () {
Y.log(Clazz.NAME + "::syncUI");
var subtotal = this.get("subtotal");
this._subtotal_node.setContent(subtotal);
this._subtotal_node.setNumberSignClass(subtotal);
},
addLine: function (line_config) {
Y.log(Clazz.NAME + "::addLine");
Y.log(Clazz.NAME + "::addLine - base_new_line_config: " + Y.dump(base_new_line_config));
Y.log(Clazz.NAME + "::addLine - line_config: " + Y.dump(line_config));
var complete_config = Y.merge(base_new_line_config, line_config);
Y.log(Clazz.NAME + "::addLine - complete_config: " + Y.dump(complete_config));
if (this._recordset.getLength() === 0 && (! this.get("minimum_lines") > 0)) {
this._table_node.append(this._table_header_node);
this._table_node.append(this._table_delivery_combinations_body_node);
this._table_node.append(this._table_total_body_node);
}
this._recordset.add(complete_config);
},
clearLines: function () {
Y.log(Clazz.NAME + "::clearLines");
this.set("subtotal", 0);
this._recordset.empty();
if (! this.get("minimum_lines") > 0) {
this._table_header_node.remove();
this._table_delivery_combinations_body_node.remove();
this._table_total_body_node.remove();
}
},
_afterAddRecord: function (e) {
Y.log(Clazz.NAME + "::_afterAddRecord");
var record = e.added[0];
Y.log(Clazz.NAME + "::_afterAddRecord - record id: " + Y.dump(record.get("id")));
Y.log(Clazz.NAME + "::_afterAddRecord - record data: " + Y.dump(record.get("data")));
var record_values = e.added[0].getValue();
var tbody_node = Y.Node.create('<tbody class="cart_builder_item_row_set"></tbody>');
this._table_delivery_combinations_body_node.insert(tbody_node, "before");
var button_cell = Y.Node.create('<td rowspan="4" style="border-right-width: 0px;"></td>');
var prod_cell = Y.Node.create('<td rowspan="4" style="border-left-width: 0px; width: 100%;"></td>');
var variant_cell = Y.Node.create('<td style="white-space: nowrap; text-align: center;">' + record_values.variant_desc + '</td>');
var sku_cell = Y.Node.create('<td style="white-space: nowrap; text-align: center;">' + record_values.display_sku + '</td>');
var qty_cell = Y.Node.create('<td style="text-align: center;"></td>');
var price_cell = Y.Node.create('<td class="ledger_value">' + record_values.variant_price + '</td>');
if (record_values.variant_price !== 0) {
price_cell.setNumberSignClass(record_values.variant_price);
}
var subtotal_cell = Y.Node.create('<td rowspan="4" class="ledger_value">' + record_values.subtotal + '</td>');
if (record_values.subtotal !== 0) {
subtotal_cell.setNumberSignClass(record_values.subtotal);
}
var row_node = Y.Node.create('<tr></tr>');
row_node.append(button_cell);
row_node.append(prod_cell);
row_node.append(variant_cell);
row_node.append(sku_cell);
row_node.append(qty_cell);
row_node.append(price_cell);
row_node.append(subtotal_cell);
var price_adj_amt_node = Y.Node.create('<input type="text" name="ol-' + record.get("id") + '-adjustment_amount" class="ledger_value" style="width: 3em;" />');
var price_adj_select_cell = Y.Node.create('<td colspan="3" style="white-space: nowrap; text-align: right;" class="mini">Good Price Adjustment: <select name="ol-' + record.get("id") + '-adjustment_kind_code"><option value=""></option><option value="fee">Fee</option><option value="discount">Discount</option></select></td>');
var price_adj_amt_cell = Y.Node.create('<td class="mini"></td>');
price_adj_amt_cell.append(price_adj_amt_node);
price_adj_amt_node.on(
"valueChange",
function (e) {
var value = price_adj_amt_node.get("value");
if (value === "") {
value = 0;
}
price_adj_amt_node.setNumberSignClass(value);
this.set("data.price_adj_amt", Number(value));
},
record
);
var second_row_node = Y.Node.create('<tr></tr>');
second_row_node.append(price_adj_select_cell);
second_row_node.append(price_adj_amt_cell);
var delivery_span_node = Y.Node.create('<span></span>');
var delivery_price_cell = Y.Node.create('<td class="mini ledger_value"></td>');
var delivery_cell = Y.Node.create('<td style="text-align: right; white-space: nowrap;" colspan="3" class="mini">Delivery Service Selection: </td>');
delivery_cell.append(delivery_span_node);
var third_row_node = Y.Node.create('<tr></tr>');
third_row_node.append(delivery_cell);
third_row_node.append(delivery_price_cell);
var delivery_price_adj_amt_node = Y.Node.create('<input type="text" name="ol-' + record.get("id") + '-delivery-adjustment_amount" class="ledger_value" style="width: 3em;" />');
var delivery_price_adj_select_cell = Y.Node.create('<td colspan="3" style="white-space: nowrap; text-align: right;" class="mini">Delivery Service Price Adjustment: <select name="ol-' + record.get("id") + '-delivery-adjustment_kind_code"><option value=""></option><option value="fee">Fee</option><option value="discount">Discount</option></select></td>');
var delivery_price_adj_amt_cell = Y.Node.create('<td class="mini"></td>');
delivery_price_adj_amt_cell.append(delivery_price_adj_amt_node);
delivery_price_adj_amt_node.on(
"valueChange",
function (e) {
var value = delivery_price_adj_amt_node.get("value");
if (value === "") {
value = 0;
}
delivery_price_adj_amt_node.setNumberSignClass(value);
this.set("data.delivery_price_adj_amt", Number(value));
},
record
);
var fourth_row_node = Y.Node.create('<tr></tr>');
fourth_row_node.append(delivery_price_adj_select_cell);
fourth_row_node.append(delivery_price_adj_amt_cell);
var row_drop_button = Y.Node.create('<button>X</button>');
row_drop_button.on(
"click",
function (e) {
Y.log(Clazz.NAME + "::_afterAddRecord - row drop button click");
e.preventDefault();
Y.log(Clazz.NAME + "::_afterAddRecord - row drop button click - id: " + record.get("id"));
Y.log(Clazz.NAME + "::_afterAddRecord - row drop button click - record set length before remove: " + this._recordset.getLength());
this._recordset.get("records").some(
function (val, i, a) {
if (val.get("id") === record.get("id")) {
this._recordset.remove(i, 1);
return true;
}
},
this
);
tbody_node.remove();
this.checkForCombinations();
this.calculateSubtotal();
Y.log(Clazz.NAME + "::_afterAddRecord - row drop button click - record set length after remove: " + this._recordset.getLength());
if (this._recordset.getLength() < this.get("minimum_lines")) {
for (i = this._recordset.getLength(); i < this.get("minimum_lines"); i++) {
this.addLine();
}
}
else if (this._recordset.getLength() === 0 && ! this.get("minimum_lines") > 0) {
this._table_header_node.remove();
this._table_delivery_combinations_body_node.remove();
this._table_total_body_node.remove();
}
},
this
);
button_cell.append(row_drop_button);
//
// if a variant id has already been set, for instance from an external
// addLine call (i.e. from an addQuantity fire by return_builder) then
// just display the product description, otherwise set up an input field
// for searching for products and ultimately selecting a variant
//
if (Y.Lang.isValue(record_values.variant_id)) {
prod_cell.append(record_values.product_desc);
variant_cell.append('<input type="hidden" name="ol-' + record.get("id") + '-variant_id" value="' + record_values.variant_id + '" />');
this._recordSetupDeliverySelect(record, delivery_span_node);
}
else {
var prod_node = Y.Node.create('<input type="text" name="ol-' + record.get("id") + '-prod_desc" value="' + record_values.product_desc + '" style="width: 100%;" />');
prod_cell.append(prod_node);
prod_node.plug(
Y.Plugin.AutoComplete,
{
scrollIntoView: true,
queryDelay: 300,
minQueryLength: 2,
source: "/manage/Products/ac?_format=json&partial={query}",
resultListLocator: "results",
resultTextLocator: "desc"
}
);
prod_node.ac.on(
"select",
function (selection) {
Y.log(Clazz.NAME + "::_afterAddRecord - AC select");
Y.log(Clazz.NAME + "::_afterAddRecord - AC select - selection.itemNode: " + selection.itemNode);
Y.log(Clazz.NAME + "::_afterAddRecord - AC select - selection.result: " + Y.dump(selection.result));
if (record.get("data.qty") === 0) {
record.set("data.qty", 1);
}
record.set("data.is_combinable", selection.result.raw.is_combinable);
record.set("data.delivery_price_combined_multiplier", selection.result.raw.delivery_price_combined_multiplier);
record.set("data.delivery_price_uncombined_multiplier", selection.result.raw.delivery_price_uncombined_multiplier);
if (selection.result.raw.variants.length == 1) {
record.set("data.display_sku", selection.result.raw.sku);
var variant = selection.result.raw.variants[0];
variant_cell.setContent('<input type="hidden" name="ol-' + record.get("id") + '-variant_id" value="' + variant.id + '" />' + variant.label);
record.set("data.variant_id", variant.id);
record.set("data.variant_price", variant.price);
}
else {
record.set("data.display_sku", "");
record.set("data.variant_price", 0);
var variant_select = Y.Node.create('<select name="ol-' + record.get("id") + '-variant_id"></select>');
variant_select.append('<option value="">Choose One</option>');
Y.each(
selection.result.raw.variants,
function (variant) {
variant_select.append('<option value="' + variant.id + '">' + variant.label + ' (' + variant.price + ')</option>');
}
);
variant_cell.setContent(variant_select);
variant_select.on(
"change",
function (e) {
Y.log(Clazz.NAME + "::_afterAddRecord - variant select change");
Y.log(Clazz.NAME + "::_afterAddRecord - variant select change - value: " + variant_select.get("value"));
record.set("data.variant_id", variant_select.get("value"));
var found = Y.some(
selection.result.raw.variants,
function (variant) {
if (variant.id == variant_select.get("value")) {
record.set("data.display_sku", variant.sku);
record.set("data.variant_price", variant.price);
return true;
}
}
);
if (! found) {
Y.log(Clazz.NAME + "::_afterAddRecord - variant select change - clear price");
record.set("data.variant_price", 0);
record.set("data.display_sku", "");
}
}
);
}
this._recordSetupDeliverySelect(record, delivery_span_node);
},
this
);
prod_node.ac.on(
"query",
function (e) {
Y.log(Clazz.NAME + "::_afterAddRecord - onQuery");
prod_node.addClass("yui3-aclist-input-loading");
}
);
prod_node.ac.after(
"results",
function (e) {
Y.log(Clazz.NAME + "::_afterAddRecord - afterResults");
prod_node.removeClass("yui3-aclist-input-loading");
}
);
delivery_span_node.setContent("(Pending Product Selection)");
}
var qty_node = Y.Node.create('<input type="text" name="ol-' + record.get("id") + '-qty" value="' + record_values.qty + '" class="ledger_value" style="width: 3em;" />');
qty_cell.append(qty_node);
qty_node.on(
"valueChange",
function (e) {
Y.log(Clazz.NAME + "::_afterAddRecord - onValueChange");
Y.log(Clazz.NAME + "::_afterAddRecord - onValueChange - e.newVal: " + e.newVal);
record.set("data.qty", Number(e.newVal));
},
this
);
tbody_node.append(row_node);
tbody_node.append(second_row_node);
tbody_node.append(third_row_node);
tbody_node.append(fourth_row_node);
record.after(
"dataChange",
function (e) {
Y.log(Clazz.NAME + "::_afterAddRecord - record dataChange - e.newVal: " + Y.dump(e.newVal));
Y.log(Clazz.NAME + "::_afterAddRecord - record dataChange - subAttrName: " + Y.dump(e.subAttrName));
if (e.subAttrName === "data.display_sku") {
sku_cell.setContent(e.newVal.display_sku);
}
else if (e.subAttrName === "data.subtotal") {
subtotal_cell.setContent(e.newVal.subtotal);
subtotal_cell.setNumberSignClass(e.newVal.subtotal);
this.calculateSubtotal();
}
else if (e.subAttrName === "data.variant_price") {
price_cell.setContent(e.newVal.variant_price);
price_cell.setNumberSignClass(e.newVal.variant_price);
record.set("data.subtotal", calculate_record_subtotal(record));
}
else if (e.subAttrName === "data.price_adj_amt" || e.subAttrName === "data.delivery_price_adj_amt") {
record.set("data.subtotal", calculate_record_subtotal(record));
}
else if (e.subAttrName === "data.delivery_ls_id") {
this.checkForCombinations();
}
else if (e.subAttrName === "data.delivery_price") {
delivery_price_cell.setContent(e.newVal.delivery_price);
delivery_price_cell.setNumberSignClass(e.newVal.delivery_price);
record.set("data.subtotal", calculate_record_subtotal(record));
}
else if (e.subAttrName === "data.qty") {
qty_node.set("value", e.newVal.qty);
this.checkForCombinations();
record.set("data.subtotal", calculate_record_subtotal(record));
}
else if (e.subAttrName === "data.is_combinable") {
this.checkForCombinations();
}
},
this
);
if (Y.Lang.isValue(record_values.is_combinable)) {
this.checkForCombinations();
}
record.set("data.subtotal", calculate_record_subtotal(record));
this.syncUI();
},
_afterEmptyRecords: function (e) {
Y.log(Clazz.NAME + "::_afterEmptyRecords");
this._table_node.all("tbody.cart_builder_item_row_set").remove();
if (this.get("minimum_lines") > 0) {
for (i = 0; i < this.get("minimum_lines"); i++) {
this.addLine();
}
}
},
_recordSetupDeliverySelect: function (record, span_node) {
var delivery_select = Y.Node.create('<select name="ol-' + record.get("id") + '-delivery"></select>');
span_node.setContent(delivery_select);
delivery_select.on(
"change",
function (e) {
var option_node = e.target.all("option").item( e.target.get("selectedIndex") );
record.set("data.delivery_price", option_node.getData("price"));
record.set("data.delivery_ls_id", e.target.get("value"));
},
this
);
if (Y.Lang.isValue(this.get("delivery_options"))) {
this.loadDeliveryOptions(delivery_select, record);
}
else {
delivery_select.setContent('<option value="">Enter Postal Code to Load Options</option>');
}
this.after(
"delivery_optionsChange",
function (e) {
record.set("data.delivery_price", 0);
record.set("data.delivery_ls_id", null);
this.loadDeliveryOptions(delivery_select, record);
},
this
);
this.after(
"delivery_combinationsChange",
function (e) {
this.loadDeliveryOptions(delivery_select, record);
},
this
);
},
checkForCombinations: function () {
Y.log(Clazz.NAME + "::checkForCombinations");
var services_by_id = {};
Y.each(
this.get("delivery_options"),
function (v, i, a) {
services_by_id[v.value] = {
label: v.label,
price: v.price
};
},
this
);
var combinations_by_ls_id = {};
Y.each(
this._recordset.get("records"),
function (record) {
Y.log(Clazz.NAME + "::checkForCombinations - each - record: " + record);
var record_data = record.get("data");
if (record_data.qty > 0 && Y.Lang.isValue(record_data.delivery_ls_id) && record_data.delivery_ls_id !== "") {
if (! Y.Lang.isValue(combinations_by_ls_id[record_data.delivery_ls_id])) {
var service = services_by_id[record_data.delivery_ls_id];
combinations_by_ls_id[record_data.delivery_ls_id] = {
in_use: false,
allow_zero_multiplier: false,
multiplier: 0,
description: service.label,
price: service.price,
subtotal: null
};
}
var combination = combinations_by_ls_id[record_data.delivery_ls_id];
if (record_data.is_combinable) {
combination.multiplier += (Number(record_data.delivery_price_combined_multiplier) * Number(record_data.qty));
combination.in_use = true;
}
else {
combination.allow_zero_multiplier = true;
}
}
}
);
// post process to set the combination subtotal
Y.each(
combinations_by_ls_id,
function (v, k, o) {
if (v.in_use) {
var multiplier = v.multiplier;
if (multiplier < 1 && ! v.allow_zero_multiplier) {
multiplier = 1;
}
v.subtotal = v.price * multiplier;
}
}
);
Y.log(Clazz.NAME + "::checkForCombinations - combinations_by_ls_id: " + Y.dump(combinations_by_ls_id));
this.set("delivery_combinations", combinations_by_ls_id);
},
calculateSubtotal: function () {
Y.log(Clazz.NAME + "::calculateSubtotal");
var subtotal = 0;
Y.each(
this._recordset.get("records"),
function (val) {
Y.log(Clazz.NAME + "::calculateSubtotal - each record - val: " + val);
subtotal += val.get("data.subtotal");
}
);
Y.each(
this.get("delivery_combinations"),
function (combo) {
if (combo.in_use) {
subtotal += combo.subtotal;
}
}
);
Y.log(Clazz.NAME + "::calculateSubtotal - subtotal: " + subtotal);
this.set("subtotal", subtotal);
},
loadDeliveryOptions: function (select, record) {
Y.log(Clazz.NAME + "::loadDeliveryOptions");
var choose_option_node = Y.Node.create('<option value="">Choose One</option>');
choose_option_node.setData("price", 0);
select.setContent(choose_option_node);
var record_is_combinable = record.get("data.is_combinable");
var combined_multiplier = record.get("data.delivery_price_combined_multiplier");
var uncombined_multiplier = record.get("data.delivery_price_uncombined_multiplier");
var delivery_combinations = this.get("delivery_combinations");
Y.each(
this.get("delivery_options"),
function (delivery_option) {
var option_value = delivery_option.value;
var option_label = delivery_option.label;
var option_price = Number(delivery_option.price);
if (record_is_combinable) {
if (Y.Lang.isValue(delivery_combinations[option_value]) && delivery_combinations[option_value].in_use) {
option_label += ' (combined)';
option_price = delivery_option.price * record.get("data.delivery_price_combined_multiplier");
}
else {
if (Y.Lang.isValue(delivery_combinations[option_value])) {
option_label += ' (combinable)';
}
var multiplier = record.get("data.delivery_price_combined_multiplier");
if (multiplier < 1) {
multiplier = 1;
}
option_price = delivery_option.price * multiplier;
option_label += ' (' + option_price + ')';
}
}
else {
option_price = delivery_option.price * uncombined_multiplier;
option_label += ' (' + option_price + ')';
}
var option_node = Y.Node.create('<option value="' + option_value + '">' + option_label + '</option>');
if (record.get("data.delivery_ls_id") === option_value) {
option_node.set("selected", true);
record.set("data.delivery_price", option_price);
}
option_node.setData("price", option_price );
this.append(option_node);
},
select
);
},
_afterSubtotalChange: function (e) {
Y.log(Clazz.NAME + "::_afterSubtotalChange");
this.syncUI();
},
_afterDeliveryCombinationsChange: function (e) {
Y.log(Clazz.NAME + "::_afterDeliveryCombinationsChange");
//
// setting the content to an empty row here is basically a hack because FF
// removes the borders on the table when it has an empty tbody
//
this._table_delivery_combinations_body_node.setContent('<tr></tr>');
var combinations = this.get("delivery_combinations");
if (Y.Lang.isValue(combinations)) {
Y.each(
combinations,
function (v, k, o) {
if (v.in_use) {
var description_cell = Y.Node.create('<td colspan="6" style="text-align: right;">Combined ' + v.description + '</td>');
var subtotal_cell = Y.Node.create('<td class="ledger_value">' + v.subtotal + '</td>');
subtotal_cell.setNumberSignClass(v.subtotal);
var row_node = Y.Node.create('<tr></tr>');
row_node.append(description_cell);
row_node.append(subtotal_cell);
this._table_delivery_combinations_body_node.append(row_node);
}
},
this
);
}
this.calculateSubtotal();
}
},
{
ATTRS: {
subtotal: {
value: 0.00
},
minimum_lines: {
value: 1
},
delivery_options: {
value: null
},
delivery_combinations: {
value: null
}
}
}
);
Y.IC.Renderer.registerConstructor('Bikes.Manage.CartBuilder', Clazz.prototype.constructor);
},
"@VERSION@",
{
requires: [
"custom-manage-cart_builder-css",
"ic-renderer-base",
"recordset-base",
"autocomplete",
"event-valuechange"
]
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment