Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variations UX #12566

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
165 changes: 104 additions & 61 deletions assets/js/frontend/add-to-cart-variation.js
Expand Up @@ -15,23 +15,25 @@
$reset_variations = $form.find( '.reset_variations' ),
template = wp.template( 'variation-template' ),
unavailable_template = wp.template( 'unavailable-variation-template' ),
$single_variation_wrap = $form.find( '.single_variation_wrap' );
selection_ux = $form.data( 'selection_ux' ) || 'locking',
$single_variation_wrap = $form.find( '.single_variation_wrap' ),
$attribute_selects = $form.find( '.variations select' );

// Always visible since 2.5.0
$single_variation_wrap.show();

// Unbind any existing events
$form.unbind( 'check_variations update_variation_values found_variation' );
$form.find( '.reset_variations' ).unbind( 'click' );
$form.find( '.variations select' ).unbind( 'change focusin' );
$reset_variations.unbind( 'click' );
$attribute_selects.unbind( 'change focusin' );

// Bind new events to form
$form

// On clicking the reset variation button
.on( 'click', '.reset_variations', function( event ) {
event.preventDefault();
$form.find( '.variations select' ).val( '' ).change();
$attribute_selects.val( '' ).change();
$form.trigger( 'reset_data' );
} )

Expand Down Expand Up @@ -68,10 +70,12 @@
}
} )

// Reload product variations data
// Reload product variations data - allows third-party scripts to modify the available variations data
.on( 'reload_product_variations', function() {
$product_variations = $form.data( 'product_variations' );
$use_ajax = $product_variations === false;

$( this ).wc_variation_form().trigger( 'check_variations' );
} )

// Reset product data
Expand Down Expand Up @@ -102,7 +106,7 @@
var some_attributes_chosen = false;
var data = {};

$form.find( '.variations select' ).each( function() {
$attribute_selects.each( function() {
var attribute_name = $( this ).data( 'attribute_name' ) || $( this ).attr( 'name' );
var value = $( this ).val() || '';

Expand Down Expand Up @@ -157,7 +161,7 @@
}
} else {
$form.trigger( 'woocommerce_variation_select_change' );
$form.trigger( 'check_variations', [ '', false ] );
$form.trigger( 'check_variations' );
$( this ).blur();
}

Expand All @@ -175,7 +179,6 @@

if ( ! $use_ajax ) {
$form.trigger( 'woocommerce_variation_select_focusin' );
$form.trigger( 'check_variations', [ $( this ).data( 'attribute_name' ) || $( this ).attr( 'name' ), true ] );
}
}
} )
Expand Down Expand Up @@ -256,18 +259,19 @@
})

// Check variations
.on( 'check_variations', function( event, exclude, focus ) {
.on( 'check_variations', function( event ) {
if ( $use_ajax ) {
return;
}

var all_attributes_chosen = true,
some_attributes_chosen = false,
current_settings = {},
$form = $( this ),
$reset_variations = $form.find( '.reset_variations' );
$form = $( this );

$form.trigger( 'update_variation_values' );

$form.find( '.variations select' ).each( function() {
$attribute_selects.each( function() {
var attribute_name = $( this ).data( 'attribute_name' ) || $( this ).attr( 'name' );
var value = $( this ).val() || '';

Expand All @@ -277,46 +281,31 @@
some_attributes_chosen = true;
}

if ( exclude && attribute_name === exclude ) {
all_attributes_chosen = false;
current_settings[ attribute_name ] = '';
} else {
// Add to settings array
current_settings[ attribute_name ] = value;
}
current_settings[ attribute_name ] = value;
});

// Find the variations that match with the attributes chosen so far
var matching_variations = wc_variation_form_matcher.find_matching_variations( $product_variations, current_settings );

if ( all_attributes_chosen ) {

var variation = matching_variations.shift();
var variation = matching_variations.length > 0 ? matching_variations[0] : false;

if ( variation ) {
$form.trigger( 'found_variation', [ variation ] );
} else {
} else if ( 'locking' === selection_ux ) {
// Nothing found - reset fields
$form.find( '.variations select' ).val( '' );

if ( ! focus ) {
$form.trigger( 'reset_data' );
}

$attribute_selects.val( '' );
$form.trigger( 'reset_data' );
window.alert( wc_add_to_cart_variation_params.i18n_no_matching_variations_text );
}

} else {

$form.trigger( 'update_variation_values', [ matching_variations ] );

if ( ! focus ) {
$form.trigger( 'reset_data' );
}

if ( ! exclude ) {
$single_variation.slideUp( 200 ).trigger( 'hide_variation' );
}
$form.trigger( 'reset_data' );
$single_variation.slideUp( 200 ).trigger( 'hide_variation' );
}

if ( some_attributes_chosen ) {
if ( $reset_variations.css( 'visibility' ) === 'hidden' ) {
$reset_variations.css( 'visibility', 'visible' ).hide().fadeIn();
Expand All @@ -327,18 +316,32 @@
} )

// Disable option fields that are unavaiable for current set of attributes
.on( 'update_variation_values', function( event, variations ) {
.on( 'update_variation_values', function( event ) {
if ( $use_ajax ) {
return;
}
// Loop through selects and disable/enable options based on selections
$form.find( '.variations select' ).each( function( index, el ) {

var current_attr_name, current_attr_select = $( el ),
show_option_none = $( el ).data( 'show_option_none' ),
option_gt_filter = 'no' === show_option_none ? '' : ':gt(0)',
new_attr_select = $( '<select/>' ),
selected_attr_val = current_attr_select.val();
// Collect current settings.
var current_settings = {};

$attribute_selects.each( function( setts_index, setts_el ) {
var attribute_name = $( this ).data( 'attribute_name' ) || $( this ).attr( 'name' ),
value = $( this ).val() || '';
current_settings[ attribute_name ] = value;
} );

// Loop through selects and disable/enable options based on selections
$attribute_selects.each( function( index, el ) {

var current_attr_select = $( el ),
current_attr_name = current_attr_select.data( 'attribute_name' ) || current_attr_select.attr( 'name' ),
ux = 'locking' !== selection_ux ? 'non-locking' : 'locking',
show_option_none = $( el ).data( 'show_option_none' ),
option_gt_filter = ':gt(0)',
attached_options_count = 0,
new_attr_select = $( '<select/>' ),
selected_attr_val = current_attr_select.val() || '',
selected_attr_val_valid = true;

// Reference options set
if ( ! current_attr_select.data( 'attribute_options' ) ) {
Expand All @@ -349,14 +352,29 @@

new_attr_select.html( current_attr_select.data( 'attribute_options' ) );

// Get name from data-attribute_name, or from input name if it doesn't exist
if ( typeof( current_attr_select.data( 'attribute_name' ) ) !== 'undefined' ) {
current_attr_name = current_attr_select.data( 'attribute_name' );
// The attribute of this select field should not be taken into account when calculating its matching variations:
// The constraints of this attribute are shaped by the values of the other attributes.
var settings = $.extend( true, {}, current_settings );

// Selections UX is 'non-locking': The constraints of this attribute are shaped by the values of all preceding attributes.
if ( 'non-locking' === ux ) {
$attribute_selects.each( function( inner_index, inner_el ) {
if ( inner_index >= index ) {
var inner_attr_select = $( inner_el ),
inner_attr_name = inner_attr_select.data( 'attribute_name' ) || inner_attr_select.attr( 'name' );

settings[ inner_attr_name ] = '';
}
} );
// Selections UX is 'locking': The constraints of this attribute are shaped by the values of all other attributes.
} else {
current_attr_name = current_attr_select.attr( 'name' );
settings[ current_attr_name ] = '';
}

// Loop through variations
// As a consequence, the globally 'matching_variations' might be fewer than the matches "seen" by this attribute.
var variations = wc_variation_form_matcher.find_matching_variations( $product_variations, settings );

// Loop through variations.
for ( var num in variations ) {

if ( typeof( variations[ num ] ) !== 'undefined' ) {
Expand All @@ -377,39 +395,64 @@

if ( attr_val ) {

// Decode entities
// Decode entities.
attr_val = $( '<div/>' ).html( attr_val ).text();

// Add slashes
// Add slashes.
attr_val = attr_val.replace( /'/g, '\\\'' );
attr_val = attr_val.replace( /"/g, '\\\"' );

// Compare the meerkat
// Attach.
new_attr_select.find( 'option[value="' + attr_val + '"]' ).addClass( 'attached ' + variation_active );

} else {
new_attr_select.find( 'option' + option_gt_filter ).addClass( 'attached ' + variation_active );
// Attach all apart from placeholder.
new_attr_select.find( 'option:gt(0)' ).addClass( 'attached ' + variation_active );
}
}
}
}
}
}

// Detach unattached
// Count available options.
attached_options_count = new_attr_select.find( 'option.attached' ).length;

// Check if current selection is in attached options.
if ( selected_attr_val && ( attached_options_count === 0 || new_attr_select.find( 'option.attached[value="' + selected_attr_val + '"]' ).length === 0 ) ) {
selected_attr_val_valid = false;
}

// Detach the placeholder if:
// - Valid options exist.
// - The current selection is non-empty.
// - The current selection is valid.
// - Placeholders are not set to be permanently visible.
if ( attached_options_count > 0 && selected_attr_val && selected_attr_val_valid && ( 'no' === show_option_none ) ) {
new_attr_select.find( 'option:first' ).remove();
option_gt_filter = '';
}

// Detach unattached.
new_attr_select.find( 'option' + option_gt_filter + ':not(.attached)' ).remove();

// Grey out disabled
// Grey out disabled.
new_attr_select.find( 'option' + option_gt_filter + ':not(.enabled)' ).attr( 'disabled', 'disabled' );

// Choose selected
// Choose selected.
if ( selected_attr_val ) {
new_attr_select.find( 'option[value="' + selected_attr_val + '"]' ).attr( 'selected', 'selected' );
} else {
new_attr_select.find( 'option:eq(0)' ).attr( 'selected', 'selected' );
// If the previously selected value is no longer available, fall back to the placeholder (it's going to be there).
if ( selected_attr_val_valid ) {
new_attr_select.find( 'option[value="' + selected_attr_val + '"]' ).attr( 'selected', 'selected' );
} else {
new_attr_select.find( 'option:eq(0)' ).attr( 'selected', 'selected' );
if ( 'non-locking' === ux ) {
current_settings[ current_attr_name ] = '';
}
}
}

// Copy to DOM
// Copy to DOM.
current_attr_select.html( new_attr_select.html() );

} );
Expand Down Expand Up @@ -543,7 +586,7 @@
$( function() {
if ( typeof wc_add_to_cart_variation_params !== 'undefined' ) {
$( '.variations_form' ).each( function() {
$( this ).wc_variation_form().find('.variations select:eq(0)').change();
$( this ).wc_variation_form().trigger( 'check_variations' );
});
}
});
Expand Down
28 changes: 16 additions & 12 deletions includes/wc-template-functions.php
Expand Up @@ -1001,6 +1001,12 @@ function woocommerce_variable_add_to_cart() {
'available_variations' => $get_variations ? $product->get_available_variations() : false,
'attributes' => $product->get_variation_attributes(),
'selected_attributes' => $product->get_default_attributes(),
/*
* Selection UX:
* - 'locking': Attribute selections in the n-th attribute are constrained by selections in all atributes other than n.
* - 'non-locking': Attribute selections in the n-th attribute are constrained by selections in all atributes before n.
*/
'selection_ux' => apply_filters( 'woocommerce_variation_attributes_selection_ux', 'locking', $product )
) );
}
}
Expand Down Expand Up @@ -2159,27 +2165,25 @@ function wc_dropdown_variation_attribute_options( $args = array() ) {
'name' => '',
'id' => '',
'class' => '',
'show_option_none' => __( 'Choose an option', 'woocommerce' ),
'show_option_none' => __( 'Choose an option', 'woocommerce' )
) );

$options = $args['options'];
$product = $args['product'];
$attribute = $args['attribute'];
$name = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title( $attribute );
$id = $args['id'] ? $args['id'] : sanitize_title( $attribute );
$class = $args['class'];
$show_option_none = $args['show_option_none'] ? true : false;
$options = $args['options'];
$product = $args['product'];
$attribute = $args['attribute'];
$name = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title( $attribute );
$id = $args['id'] ? $args['id'] : sanitize_title( $attribute );
$class = $args['class'];
$show_option_none = $args['show_option_none'] ? true : false;
$show_option_none_text = $args['show_option_none'] ? $args['show_option_none'] : __( 'Choose an option', 'woocommerce' ); // We'll do our best to hide the placeholder, but we'll need to show something when resetting options.

if ( empty( $options ) && ! empty( $product ) && ! empty( $attribute ) ) {
$attributes = $product->get_variation_attributes();
$options = $attributes[ $attribute ];
}

$html = '<select id="' . esc_attr( $id ) . '" class="' . esc_attr( $class ) . '" name="' . esc_attr( $name ) . '" data-attribute_name="attribute_' . esc_attr( sanitize_title( $attribute ) ) . '" data-show_option_none="' . ( $show_option_none ? 'yes' : 'no' ) . '">';

if ( $show_option_none ) {
$html .= '<option value="">' . esc_html( $args['show_option_none'] ) . '</option>';
}
$html .= '<option value="">' . esc_html( $show_option_none_text ) . '</option>';

if ( ! empty( $options ) ) {
if ( $product && taxonomy_exists( $attribute ) ) {
Expand Down
2 changes: 1 addition & 1 deletion templates/single-product/add-to-cart/variable.php
Expand Up @@ -25,7 +25,7 @@

do_action( 'woocommerce_before_add_to_cart_form' ); ?>

<form class="variations_form cart" method="post" enctype='multipart/form-data' data-product_id="<?php echo absint( $product->get_id() ); ?>" data-product_variations="<?php echo htmlspecialchars( wp_json_encode( $available_variations ) ) ?>">
<form class="variations_form cart" method="post" enctype='multipart/form-data' data-product_id="<?php echo absint( $product->get_id() ); ?>" data-product_variations="<?php echo htmlspecialchars( wp_json_encode( $available_variations ) ) ?>" data-selection_ux="<?php echo $selection_ux; ?>">
<?php do_action( 'woocommerce_before_variations_form' ); ?>

<?php if ( empty( $available_variations ) && false !== $available_variations ) : ?>
Expand Down