Skip to content

Instantly share code, notes, and snippets.

@citrus
Created May 31, 2011 18:21
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save citrus/1001016 to your computer and use it in GitHub Desktop.
Save citrus/1001016 to your computer and use it in GitHub Desktop.
custom variant selection in spree
<% option_count = 0 %>
<%= form_for :order, :url => populate_orders_url do |f| %>
<% if @product.has_variants? %>
<div id="product-variants">
<% if defined?(@options) && !@options.empty? %>
<% @options.each_with_index do |option,index| %>
<% option_count += 1 %>
<% name = option[0].name.downcase %>
<div class="product-options">
<h2>Choose Your <%= name.capitalize %></h2>
<%= link_to "(clear)", "#clear", :class => 'clear' %>
<%= hidden_field_tag "option_type[#{index}][]", option[0].id, :class => 'option_type hidden' %>
<ul id="product-<%= name %>" class="list-<%= index %>">
<%= render :partial => 'product_options', :locals => { :option_type => option[0], :options => option[1] } %>
</ul>
<br class="clear"/>
</div>
<% end %>
<% end %>
</div>
<% end %>
<% if 0 < option_count && @product.has_stock? || Spree::Config[:allow_backorders] %>
<div class="leftie">
<p class="quantity">Quantity</p>
<div class="frameSmall">
<%= text_field_tag (@product.has_variants? ? :quantity : "variants[#{@product.master.id}]"), 1, :class => "numbers", :size => 3 %>
</div>
</div>
<div class="rightie">
<%= hidden_field_tag 'product_permalink', @product.permalink %>
<button type="submit" class="add-to-cart">Add To Cart</button>
</div>
<br class="clear"/>
<% else %>
<h4 class="out-of-stock">> <%= t('out_of_stock') %> <</h4>
<% end %>
<% end %>
<% content_for :head do %>
<%= javascript_include_tag 'product_options' %>
<% end %>
<% product ||= @product %>
<% options ||= nil %>
<% if options.nil? %>
<li class="empty">
<h5>No options..</h5>
</li>
<% elsif options.empty? %>
<li class="empty">
<h5>Please select an option above first..</h5>
</li>
<% else %>
<% options.each_with_index do |option_value,index| %>
<% if @option_value && @variants %>
<%
a = OptionValueVariant.includes(:variant).where(:variant_id => @variants.collect(&:id), :option_value_id => [@option_value.id, option_value.id]).collect(&:variant_id)
b = Hash.new(0)
a.each{ |v| b[v.to_s.strip] += 1 }
var = @variants.find(b.invert.values_at(2)).first rescue nil
%>
<% else %>
<% var = @product.variants.joins(:option_value_variants).where("option_value_variants.option_value_id" => option_value.id).first %>
<% var.count_on_hand = 999 %>
<% end %>
<li>
<% if var %>
<% if var.count_on_hand == 0 %>
<%= image_tag "out_of_stock.png", :alt => "Out of Stock", :class => "out_of_stock" %>
<% end %>
<script type="text/javascript">
//<![CDATA[
Optys.prices[<%= var.id %>] = [<%= var.price %>, <%= var.wholesale_price if var.is_wholesaleable? %>, <%= var.count_on_hand %>];
//]]>
</script>
<% end %>
<label class="<%= var && var.count_on_hand == 0 ? 'out-of-stock' : 'in-stock' %>">
<span class="radio">
<%= hidden_field_tag "variants[][#{var.id}", var.id, :class => 'hidden' if var %>
<%= radio_button_tag "options[#{product.id}][#{option_type.id}]", option_value.id, false %>
<%= link_to (option_value.has_image? ? image_tag(option_value.image.url, :alt => option_value.name) : content_tag(:span, option_value.name.html_safe)), "#select", :class => 'radio' %>
</span>
</label>
</li>
<% end %>
<% end %>
var Optys = {
prices: []
};
;(function($){
function formatted_price(price, label) {
var p = parseFloat(price) || 0.0;
var str = label ? label + ": $" : "$"
str = str + p.toFixed(2);
return str;
}
$.extend(Optys, {
selected_options: {},
init: function(evt) {
$.preloadImages('/images/spinner.gif');
Optys.count = $('#product-variants .product-options').length;
Optys.toggle_button(false);
$('#product-variants a.clear').live('click', function(evt) {
evt.preventDefault();
var p = $(this).hide().parent();
p.find('ul li label').css('opacity', 1);
p.find('span').css('background-position', '0 0');
Optys.clear_after(parseInt((p.find('ul').attr('class').match(/[0-9]+/) || [0])[0]));
});
$('#product-variants a.radio').live('mouseover', function(evt) {
if (!Optys.selection || $(this).data('selected')) return;
$(this).parents('li').css('opacity', 1);
}).live('mouseout', function(evt) {
if (!Optys.selection || $(this).data('selected')) return;
$(this).parents('li').css('opacity', 0.25);
}).live('click', function(evt) {
evt.preventDefault();
if (Optys.loading) return;
var a = $(this).data('selected', true);
var v = a.siblings('input.hidden');
var div = a.parents('div.product-options');
var ul = div.find('ul');
var index = parseInt((ul.attr('class').match(/[0-9]+/) || [0])[0]) + 1;
var vid = v.val();
var prices = Optys.prices[v.val()];
if (vid && prices) {
//if (parseInt(prices[2]) < 1) {
// evt.preventDefault();
//
//
// return false;
//}
var p = $('.price_msrp');
if (!p.length) {
$('#product-title .price').text(formatted_price(prices[0]));
} else {
p.text(formatted_price(prices[0], 'msrp'))
}
$('.price_wholesale').text(formatted_price(prices[1], 'wholesale'));
}
Optys.loading = true;
Optys.selection = this;
var radio = a.siblings('input[type=radio]').attr('checked', true);
var otid = div.find('input[type=hidden]').val();
var ovid = div.find('input[type=radio]:checked').val();
var url = "/shop/"+$('#product_permalink').attr('value')+"/variant";
div.find('a.clear').show();
ul.find('li label').css('opacity', 0.25);
ul.find('span.radio').css('background-position', '0 0');
a.parents('span').css('background-position', '0 bottom');
a.tipsy('hide').parents('label').css('opacity', 1);
Optys.selected_options[otid] = ovid;
Optys.clear_after(index);
var list = $('#product-variants .list-' + index);
if (list.length) {
list.addClass('list-loaded').empty().append('<li><p><img src="/images/spinner.gif" alt="loading.." class="option-loading" /> Loading...</p></li>');
var url = "/shop/"+$('#product_permalink').attr('value')+"/options";
$.get(url, {
p: ul.attr('id'),
n: list.attr('id'),
ntid: list.parent().find('input[type=hidden]').val(),
otid: otid,
ovid: ovid
}, function(result) {
list.html(result);
Optys.index = index;
Optys.loading = false;
});
}
}).tipsy({ gravity: 'e', html: true, live: true, opacity: 1, offset: -3, title: function(evt) {
var img = $(this).find('img');
if (!img.length) return '';
$('.tipsy-inner').css('background', '#fff url(/images/spinner.gif) center center no-repeat');
var image = '<img src="'+img.attr('src').replace('/small/', '/large/')+'" alt="loading" class="option-preview"/>'
var title = '<b>' + img.attr('alt') + '</b>';
var vari = $(this).siblings('input.hidden').val();
return image + title + (Optys.prices[vari][2] == 0 ? '<span>out of stock</span>' : '');
}});
},
toggle_button: function(enabled) {
if (enabled) {
$('.add-to-cart').removeClass('disabled').addClass('enabled').unbind('click');
} else {
$('.add-to-cart').removeClass('enabled').addClass('disabled').click(function(evt) { evt.preventDefault(); });
}
},
clear_after: function(index) {
Optys.index = index;
function get_list(i) {
return $('#product-variants .list-' + (i + 1) + '.list-loaded').empty();
}
var ul = get_list(index);
var i = index;
while(ul.length) {
if (!ul.find('li.empty').length) {
ul.empty();
ul.parent().find('a.clear').hide();
ul.append('<li class="empty">Please select an option above..</li>');
}
ul = get_list(i);
i++;
if (99 < i) break;
}
Optys.reset();
},
reset: function() {
Optys.selection = null;
Optys.loading = false;
Optys.toggle_button(Optys.index == Optys.count);
}
});
$(document).ready(Optys.init);
})(jQuery);
# just the important stuff
def show
@product = Product.find_by_permalink!(params[:id])
return unless @product
get_options
@variants = Variant.active.find_all_by_product_id(@product.id,
:include => [:option_values, :images])
@product_properties = ProductProperty.find_all_by_product_id(@product.id,
:include => [:property])
@selected_variant = @variants.detect { |v| v.available? }
referer = request.env['HTTP_REFERER']
if referer && referer.match(HTTP_REFERER_REGEXP)
@taxon = Taxon.find_by_permalink($1)
end
end
def options
@product = Product.find_by_permalink(params[:id])
if params[:p]
prev_name = params[:p].sub('product-', '')
end
if params[:n]
name = params[:n].sub('product-', '')
end
@index = params[:i].to_i
get_options
render :partial => 'product_options', :locals => { :option_type => @next_option_type, :options => @options[@index][1], :product => @product, :name => name, :last_name => prev_name, :variants => @variants }
end
def get_options
return if @product.nil?
@options = []
@index ||= 0
@values = nil
option_type = @product.option_types.find(params[:otid]) rescue @product.option_types.first
if option_type
@next_option_type = @product.option_types.find(params[:ntid]) rescue nil
if @next_option_type && params[:ovid]
@option_value = option_type.option_values.find(params[:ovid])
@variants = @option_value.variants.select('variants.id').where('deleted_at IS NULL').where(:product_id => @product.id)
else
@next_option_type = option_type
@variants = @product.variants
end
vids = @variants.collect(&:id)
@values = OptionValue.joins(:option_value_variants).where("option_value_variants.product_id = ? AND option_value_variants.variant_id IN (?) AND option_values.option_type_id = ?", @product.id, vids, @next_option_type.id).order("option_values.position ASC").uniq
end
@product.option_types.each_with_index do |op, index|
@values ||= @product.option_values.where(:option_type_id => op.id).uniq
@options << [op, index == @index ? @values : []]
end
@options
end
# just the important stuff
resources :products, :path => 'shop' do
member do
get :options
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment