Skip to content

Commit

Permalink
fix(storefront): BCTHEME-11 hide product options that are out of stoc…
Browse files Browse the repository at this point in the history
…k on cart (bigcommerce#1911)

Co-authored-by: BC-tymurbiedukhin <66319629+BC-tymurbiedukhin@users.noreply.github.com>
  • Loading branch information
2 people authored and sacr3dc0w committed Mar 31, 2021
1 parent e17b5cf commit 2045fa8
Show file tree
Hide file tree
Showing 8 changed files with 587 additions and 399 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Draft
- Fixed an issue with dispaying options that are out of stock for product on Cart. [#1911](https://github.com/bigcommerce/cornerstone/pull/1911)
- Selecting product options doesn't update image on PDP in Internet Explorer. [#1913](https://github.com/bigcommerce/cornerstone/pull/1913)
- HTML Entity displayed as is via system/error message on a Storefront. [#1888](https://github.com/bigcommerce/cornerstone/pull/1888)
- Shoppers are not anchor-linked to reviews on PDPs if product description tabs are enabled. [#1883](https://github.com/bigcommerce/cornerstone/pull/1883)
Expand Down
34 changes: 23 additions & 11 deletions assets/js/theme/cart.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import PageManager from './page-manager';
import _ from 'lodash';
import { bind, debounce } from 'lodash';
import giftCertCheck from './common/gift-certificate-validator';
import utils from '@bigcommerce/stencil-utils';
import ShippingEstimator from './cart/shipping-estimator';
import { defaultModal, modalTypes } from './global/modal';
import swal from './global/sweet-alert';
import CartItemDetails from './common/cart-item-details';

export default class Cart extends PageManager {
onReady() {
this.$modal = null;
this.$cartContent = $('[data-cart-content]');
this.$cartMessages = $('[data-cart-status]');
this.$cartTotals = $('[data-cart-totals]');
Expand Down Expand Up @@ -125,30 +127,40 @@ export default class Cart extends PageManager {
});
}

cartEditOptions(itemId) {
cartEditOptions(itemId, productId) {
const context = { productForChangeId: productId, ...this.context };
const modal = defaultModal();

if (this.$modal === null) {
this.$modal = $('#modal');
}

const options = {
template: 'cart/modals/configure-product',
};

modal.open();
this.$modal.find('.modal-content').addClass('hide-content');

utils.api.productAttributes.configureInCart(itemId, options, (err, response) => {
modal.updateContent(response.content);
const $productOptionsContainer = $('[data-product-attributes-wrapper]', this.$modal);
const modalBodyReservedHeight = $productOptionsContainer.outerHeight();
$productOptionsContainer.css('height', modalBodyReservedHeight);

this.productDetails = new CartItemDetails(this.$modal, context);

this.bindGiftWrappingForm();

modal.setupFocusableElements(modalTypes.CART_CHANGE_PRODUCT);
});

utils.hooks.on('product-option-change', (event, currentTarget) => {
const $changedOption = $(currentTarget);
const $form = $changedOption.parents('form');
const $form = $(currentTarget).find('form');
const $submit = $('input.button', $form);
const $messageBox = $('.alertMessageBox');
const item = $('[name="item_id"]', $form).attr('value');

utils.api.productAttributes.optionChange(item, $form.serialize(), (err, result) => {
utils.api.productAttributes.optionChange(productId, $form.serialize(), (err, result) => {
const data = result.data || {};

if (err) {
Expand Down Expand Up @@ -213,9 +225,9 @@ export default class Cart extends PageManager {

bindCartEvents() {
const debounceTimeout = 400;
const cartUpdate = _.bind(_.debounce(this.cartUpdate, debounceTimeout), this);
const cartUpdateQtyTextChange = _.bind(_.debounce(this.cartUpdateQtyTextChange, debounceTimeout), this);
const cartRemoveItem = _.bind(_.debounce(this.cartRemoveItem, debounceTimeout), this);
const cartUpdate = bind(debounce(this.cartUpdate, debounceTimeout), this);
const cartUpdateQtyTextChange = bind(debounce(this.cartUpdateQtyTextChange, debounceTimeout), this);
const cartRemoveItem = bind(debounce(this.cartRemoveItem, debounceTimeout), this);
let preVal;

// cart update
Expand Down Expand Up @@ -257,10 +269,10 @@ export default class Cart extends PageManager {

$('[data-item-edit]', this.$cartContent).on('click', event => {
const itemId = $(event.currentTarget).data('itemEdit');

const productId = $(event.currentTarget).data('productId');
event.preventDefault();
// edit item in cart
this.cartEditOptions(itemId);
this.cartEditOptions(itemId, productId);
});
}

Expand Down
143 changes: 143 additions & 0 deletions assets/js/theme/common/cart-item-details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import utils from '@bigcommerce/stencil-utils';
import ProductDetailsBase, { optionChangeDecorator } from './product-details-base';
import { isEmpty } from 'lodash';
import { isBrowserIE, convertIntoArray } from './utils/ie-helpers';

export default class CartItemDetails extends ProductDetailsBase {
constructor($scope, context, productAttributesData = {}) {
super($scope, context);

const $form = $('#CartEditProductFieldsForm', this.$scope);
const $productOptionsElement = $('[data-product-attributes-wrapper]', $form);
const hasOptions = $productOptionsElement.html().trim().length;
const hasDefaultOptions = $productOptionsElement.find('[data-default]').length;

$productOptionsElement.on('change', e => {
this.setProductVariant();
});

const optionChangeCallback = optionChangeDecorator.call(this, hasDefaultOptions);

// Update product attributes. Also update the initial view in case items are oos
// or have default variant properties that change the view
if ((isEmpty(productAttributesData) || hasDefaultOptions) && hasOptions) {
const productId = this.context.productForChangeId;

utils.api.productAttributes.optionChange(productId, $form.serialize(), 'products/bulk-discount-rates', optionChangeCallback);
} else {
this.updateProductAttributes(productAttributesData);
}
}

setProductVariant() {
const unsatisfiedRequiredFields = [];
const options = [];

$.each($('[data-product-attribute]'), (index, value) => {
const optionLabel = value.children[0].innerText;
const optionTitle = optionLabel.split(':')[0].trim();
const required = optionLabel.toLowerCase().includes('required');
const type = value.getAttribute('data-product-attribute');

if ((type === 'input-file' || type === 'input-text' || type === 'input-number') && value.querySelector('input').value === '' && required) {
unsatisfiedRequiredFields.push(value);
}

if (type === 'textarea' && value.querySelector('textarea').value === '' && required) {
unsatisfiedRequiredFields.push(value);
}

if (type === 'date') {
const isSatisfied = Array.from(value.querySelectorAll('select')).every((select) => select.selectedIndex !== 0);

if (isSatisfied) {
const dateString = Array.from(value.querySelectorAll('select')).map((x) => x.value).join('-');
options.push(`${optionTitle}:${dateString}`);

return;
}

if (required) {
unsatisfiedRequiredFields.push(value);
}
}

if (type === 'set-select') {
const select = value.querySelector('select');
const selectedIndex = select.selectedIndex;

if (selectedIndex !== 0) {
options.push(`${optionTitle}:${select.options[selectedIndex].innerText}`);

return;
}

if (required) {
unsatisfiedRequiredFields.push(value);
}
}

if (type === 'set-rectangle' || type === 'set-radio' || type === 'swatch' || type === 'input-checkbox' || type === 'product-list') {
const checked = value.querySelector(':checked');
if (checked) {
const getSelectedOptionLabel = () => {
const productVariantslist = convertIntoArray(value.children);
const matchLabelForCheckedInput = inpt => inpt.dataset.productAttributeValue === checked.value;
return productVariantslist.filter(matchLabelForCheckedInput)[0];
};
if (type === 'set-rectangle' || type === 'set-radio' || type === 'product-list') {
const label = isBrowserIE ? getSelectedOptionLabel().innerText.trim() : checked.labels[0].innerText;
if (label) {
options.push(`${optionTitle}:${label}`);
}
}

if (type === 'swatch') {
const label = isBrowserIE ? getSelectedOptionLabel().children[0] : checked.labels[0].children[0];
if (label) {
options.push(`${optionTitle}:${label.title}`);
}
}

if (type === 'input-checkbox') {
options.push(`${optionTitle}:Yes`);
}

return;
}

if (type === 'input-checkbox') {
options.push(`${optionTitle}:No`);
}

if (required) {
unsatisfiedRequiredFields.push(value);
}
}
});

let productVariant = unsatisfiedRequiredFields.length === 0 ? options.sort().join(', ') : 'unsatisfied';
const view = $('.modal-header-title');

if (productVariant) {
productVariant = productVariant === 'unsatisfied' ? '' : productVariant;
if (view.attr('data-event-type')) {
view.attr('data-product-variant', productVariant);
} else {
const productName = view.html().match(/'(.*?)'/)[1];
const card = $(`[data-name="${productName}"]`);
card.attr('data-product-variant', productVariant);
}
}
}

/**
* Hide or mark as unavailable out of stock attributes if enabled
* @param {Object} data Product attribute data
*/
updateProductAttributes(data) {
super.updateProductAttributes(data);

this.$scope.find('.modal-content').removeClass('hide-content');
}
}
Loading

0 comments on commit 2045fa8

Please sign in to comment.