well we have heavily customized Magento 2 website. The swatch customizing is done in:
app\code\My_Package\CustomSwatches\view\frontend\web\js\SwatchRenderer.js
by overriding:
vendor\magento\module-swatches\view\frontend\web\js\SwatchRenderer.js
This is the edited SwatchRenderer.js
portion:
$.widget('custom.SwatchRenderer', {
options: {
classes: {
attributeClass: 'swatch-attribute',
attributeLabelClass: 'swatch-attribute-label',
attributeSelectedOptionLabelClass: 'swatch-attribute-selected-option',
attributeOptionsWrapper: 'swatch-attribute-options',
attributeInput: 'swatch-input',
optionClass: 'swatch-option',
selectClass: 'swatch-select',
moreButton: 'swatch-more',
loader: 'swatch-option-loading'
},
jsonConfig: {}, // option's json config
jsonSwatchConfig: {}, // swatch's json config
selectorProduct: '.product-info-main', // selector of parental block of prices and swatches (need to know where to seek for price block)
selectorProductPrice: '[data-role=priceBox]', // selector of price wrapper (need to know where set price)
numberToShow: false, // number of controls to show (false or zero = show all)
onlySwatches: false, // show only swatch controls
enableControlLabel: true, // enable label for control
moreButtonText: 'More', // text for more button
mediaCallback: '', // Callback url for media
mediaGalleryInitial: [{}] // Cache for BaseProduct images. Needed when option unset
},
/**
* Get chosen product
*
* @returns array
*/
getProduct: function() {
return this._CalcProducts().shift();
},
/**
* @private
*/
_init: function() {
if (this.options.jsonConfig != '' && this.options.jsonSwatchConfig != '') {
this._RenderControls();
} else {
console.log('SwatchRenderer: No input data received');
}
},
/**
* @private
*/
_create: function() {
var options = this.options,
gallery = $('[data-gallery-role=gallery-placeholder]', '.column.main'),
isProductViewExist = $('body.catalog-product-view').size() > 0,
$main = isProductViewExist ?
this.element.parents('.column.main') :
this.element.parents('.product-item-info');
if (isProductViewExist) {
gallery.on('gallery:loaded', function() {
var galleryObject = gallery.data('gallery');
options.mediaGalleryInitial = galleryObject.returnCurrentImages();
});
} else {
options.mediaGalleryInitial = [{
'img': $main.find('.product-image-photo').attr('src')
}];
}
},
/**
* Render controls
*
* @private
*/
_RenderControls: function() {
var $widget = this,
container = this.element,
classes = this.options.classes,
chooseText = this.options.jsonConfig.chooseText;
$widget.optionsMap = {};
$.each(this.options.jsonConfig.attributes, function() {
var item = this,
options = $widget._RenderSwatchOptions(item),
select = $widget._RenderSwatchSelect(item, chooseText),
input = $widget._RenderFormInput(item),
fake_tabs = $widget._RenderFakeTabs(item),
label = '';
// Show only swatch controls
if ($widget.options.onlySwatches && !$widget.options.jsonSwatchConfig.hasOwnProperty(item.id)) {
return;
}
if ($widget.options.enableControlLabel) {
if (item.code == "admire_gender") {
label += '';
} else {
label +=
'<span class="' + classes.attributeLabelClass + '">' + item.label + '</span>' +
'<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>';
}
}
// Create new control
if (item.code == "admire_gender") {
container.append('<div class="' + classes.attributeClass + ' ' + item.code + '" attribute-code="' + item.code + '" attribute-id="' + item.id + '">' + label +
'<div class="' + classes.attributeOptionsWrapper + ' clearfix gender-wrapper"><ul class="nav nav-tabs" role="tablist">' + options + select + '</ul>' +
'<div class="tab-content">' + fake_tabs + '</div></div>' + input + '</div>');
} else if (item.code == "admire_color") {
container.append(
'<div class="' + classes.attributeClass + ' ' + item.code + '" attribute-code="' + item.code + '" attribute-id="' + item.id + '">' + label +
'<div class="' + classes.attributeOptionsWrapper + ' clearfix">' + options + select + '<a href="#" class="colors-explained">Colors explained</a></div>' + input + '</div>'
);
} else if (item.code == "admire_size") {
container.append(
'<div class="' + classes.attributeClass + ' ' + item.code + '" attribute-code="' + item.code + '" attribute-id="' + item.id + '">' + label +
'<div class="' + classes.attributeOptionsWrapper + ' clearfix">' + options + select + '<a href="#" class="size-chart">SIZE CHARTS</a></div>' + input + '</div>'
);
} else {
container.append(
'<div class="' + classes.attributeClass + ' ' + item.code + '" attribute-code="' + item.code + '" attribute-id="' + item.id + '">' + label +
'<div class="' + classes.attributeOptionsWrapper + ' clearfix">' + options + select + '</div>' + input + '</div>'
);
}
$widget.optionsMap[item.id] = {};
// Aggregate options array to hash (key => value)
$.each(item.options, function() {
if (this.products.length > 0) {
$widget.optionsMap[item.id][this.id] = {
price: parseInt($widget.options.jsonConfig.optionPrices[this.products[0]].finalPrice.amount, 10),
products: this.products
};
}
});
});
// Connect Tooltip
container
.find('[option-type="1"], [option-type="2"], [option-type="0"], [option-type="3"]')
.SwatchRendererTooltip();
// Hide all elements below more button
$('.' + classes.moreButton).nextAll().hide();
// Handle events like click or change
$widget._EventListener();
// Rewind options
$widget._Rewind(container);
//Emulate click on all swatches from Request
$widget._EmulateSelected();
},
/**
* Render swatch options by part of config
*
* @param config
* @returns {string}
* @private
*/
_RenderSwatchOptions: function(config) {
if (!this.options.jsonSwatchConfig.hasOwnProperty(config.id)) {
return '';
}
var optionConfig = this.options.jsonSwatchConfig[config.id],
optionClass = this.options.classes.optionClass,
moreLimit = this.options.numberToShow,
moreClass = this.options.classes.moreButton,
moreText = this.options.moreButtonText,
countAttributes = 0,
html = '';
var flag = 0;
var activeClass = '';
$.each(config.options, function() {
if (!optionConfig.hasOwnProperty(this.id)) {
return '';
}
// Add more button
if (moreLimit != false && moreLimit == countAttributes++) {
html += '<a href="#" class="' + moreClass + '">' + moreText + '</a>';
}
var id = this.id,
type = optionConfig[id].type,
value = optionConfig[id].hasOwnProperty('value') ? optionConfig[id].value : '',
thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : '',
label = this.label ? this.label : '',
attr =
' option-type="' + type + '"' +
' option-id="' + id + '"' +
' option-label="' + label + '"' +
' option-tooltip-thumb="' + thumb + '"' +
' option-tooltip-value="' + value + '"';
if (!this.hasOwnProperty('products') || this.products.length <= 0) {
attr += ' option-empty="true"';
}
// Text
if (type == 0) {
if (config.code == "admire_gender") {
++flag;
if (flag == 1) {
activeClass = 'active';
} else {
activeClass = '';
}
html += '<li class="' + optionClass + ' ' + activeClass + ' text" ' + attr + ' role="presentation"><a href="#' + value + '" area-controls="' + value + '" role="tab" data-toggle="tab">' + (value ? value : label) + '</a></li>';
} else {
html += '<div class="' + optionClass + ' text" ' + attr + '>' + (value ? value : label) + '</div>';
}
}
// Color
else if (type == 1) {
html += '<div class="' + optionClass + ' color" ' + attr +
'" style="background: ' + thumb + ' no-repeat center; background-size: initial;">' + '' + '</div>';
}
// Image
else if (type == 2) {
html += '<div class="' + optionClass + ' image" ' + attr +
'" style="background: url(' + thumb + ') no-repeat center; background-size: initial;">' + '' + '</div>';
}
// Clear
else if (type == 3) {
html += '<div class="' + optionClass + '" ' + attr + '></div>';
}
// Default
else {
html += '<div class="' + optionClass + '" ' + attr + '>' + label + '</div>';
}
});
return html;
},
/**
* Render select by part of config
*
* @param config
* @param chooseText
* @returns {string}
* @private
*/
_RenderSwatchSelect: function(config, chooseText) {
if (this.options.jsonSwatchConfig.hasOwnProperty(config.id)) {
return '';
}
var html =
'<select class="' + this.options.classes.selectClass + ' ' + config.code + '">' +
'<option value="0" option-id="0">' + chooseText + '</option>';
$.each(config.options, function() {
var label = this.label,
attr = ' value="' + this.id + '" option-id="' + this.id + '"';
if (!this.hasOwnProperty('products') || this.products.length <= 0) {
attr += ' option-empty="true"';
}
html += '<option ' + attr + '>' + label + '</option>';
});
html += '</select>';
return html;
},
/**
* Input for submit form.
* This control shouldn't have "type=hidden", "display: none" for validation work :(
*
* @param config
* @private
*/
_RenderFormInput: function(config) {
return '<input class="' + this.options.classes.attributeInput + '" ' +
'name="super_attribute[' + config.id + ']" ' +
'value="" ' +
'data-validate="{required:true}" ' +
'aria-required="true" ' +
'aria-invalid="true" ' +
'style="visibility: hidden; position:absolute; left:-1000px">';
},
/**
* Input for fake tabs.
* This control shouldn't have "type=hidden", "display: none" for validation work :(
*
* @param config
* @private
*/
_RenderFakeTabs: function(config) {
var tabs = '';
var configOptions = this.options.jsonSwatchConfig[config.id];
var flag = 0;
var tab_image = '';
var activeClass = '';
$.each(config.options, function() {
var id = this.id;
var value = configOptions[id].hasOwnProperty('value') ? configOptions[id].value : '';
if (config.code == 'admire_gender') {
++flag;
if (flag == 1) {
activeClass = 'active in';
} else {
activeClass = '';
}
if (value == "Men") {
tab_image = gender_image_MC;
} else if (value == "Women") {
tab_image = gender_image_W;
} else if (value = "Children") {
tab_image = gender_image_MC;
}
tabs += '<div role="tabpanel" class="tab-pane fade ' + activeClass + '" id="' + value + '"><a href="#"><img src="' + tab_image + '" alt=""/></a></div>';
}
});
return tabs;
},
/**
* Event listener
*
* @private
*/
_EventListener: function() {
var $widget = this;
$widget.element.on('click', '.' + this.options.classes.optionClass, function() {
return $widget._OnClick($(this), $widget);
});
$widget.element.on('change', '.' + this.options.classes.selectClass, function() {
return $widget._OnChange($(this), $widget);
});
$widget.element.on('click', '.' + this.options.classes.moreButton, function(e) {
e.preventDefault();
return $widget._OnMoreClick($(this));
});
},
/**
* Event for swatch options
*
* @param $this
* @param $widget
* @private
*/
_OnClick: function($this, $widget) {
var muaw_color, admire_gender, muaw_size = '';
if ($this.parent().parent().hasClass('admire_color')) {
muaw_color = $this.attr('option-label');
$('.product-selection-color').text(muaw_color + ' t-shirt, ');
} else if ($this.parent().parent().hasClass('admire_size')) {
muaw_size = $this.attr('option-label');
$('.product-selection-size').text('size ' + muaw_size);
} else if ($this.parent().parent().parent().hasClass('admire_gender')) {
if ($this.attr('option-label') == 'Men') {
admire_gender = 'short sleeve'
} else if ($this.attr('option-label') == 'Women') {
admire_gender = 'long sleeve'
} else if ($this.attr('option-label') == 'Children') {
admire_gender = 'short sleeve'
}
$('.product-selection-gender').text(admire_gender + ', ');
}
var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
$label = $parent.find('.' + $widget.options.classes.attributeSelectedOptionLabelClass),
$input = $parent.find('.' + $widget.options.classes.attributeInput);
if ($this.hasClass('disabled')) {
return;
}
if ($this.hasClass('selected')) {
$parent.removeAttr('option-selected').find('.selected').removeClass('selected');
$input.val('');
$label.text('');
} else {
$parent.attr('option-selected', $this.attr('option-id')).find('.selected').removeClass('selected');
$label.text($this.attr('option-label'));
$input.val($this.attr('option-id'));
$this.addClass('selected');
}
$widget._Rebuild();
if ($widget.element.parents($widget.options.selectorProduct)
.find(this.options.selectorProductPrice).is(':data(mage-priceBox)')
) {
$widget._UpdatePrice();
}
$widget._LoadProductMedia();
$('.product-page-left .product-image-design').css('display', 'block');
},
/**
* Event for select
*
* @param $this
* @param $widget
* @private
*/
_OnChange: function($this, $widget) {
var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
$input = $parent.find('.' + $widget.options.classes.attributeInput);
if ($this.val() > 0) {
$parent.attr('option-selected', $this.val());
$input.val($this.val());
} else {
$parent.removeAttr('option-selected');
$input.val('');
}
$widget._Rebuild();
$widget._UpdatePrice();
$widget._LoadProductMedia();
},
/**
* Event for more switcher
*
* @param $this
* @private
*/
_OnMoreClick: function($this) {
$this.nextAll().show();
$this.blur().remove();
},
/**
* Rewind options for controls
*
* @private
*/
_Rewind: function(controls) {
controls.find('div[option-id], option[option-id]').removeClass('disabled').removeAttr('disabled');
controls.find('li[option-id], option[option-id]').removeClass('disabled').removeAttr('disabled');
controls.find('div[option-empty], option[option-empty]').attr('disabled', true).addClass('disabled');
controls.find('li[option-empty], option[option-empty]').attr('disabled', true).addClass('disabled');
},
/**
* Rebuild container
*
* @private
*/
_Rebuild: function() {
var $widget = this,
controls = $widget.element.find('.' + $widget.options.classes.attributeClass + '[attribute-id]'),
selected = controls.filter('[option-selected]');
// Enable all options
$widget._Rewind(controls);
// done if nothing selected
if (selected.size() <= 0) {
return;
}
// Disable not available options
controls.each(function() {
var $this = $(this),
id = $this.attr('attribute-id'),
products = $widget._CalcProducts(id);
if (selected.size() == 1 && selected.first().attr('attribute-id') == id) {
return;
}
$this.find('[option-id]').each(function() {
var $this = $(this),
option = $this.attr('option-id');
if (!$widget.optionsMap.hasOwnProperty(id) || !$widget.optionsMap[id].hasOwnProperty(option) ||
$this.hasClass('selected') ||
$this.is(':selected')) {
return;
}
if (_.intersection(products, $widget.optionsMap[id][option].products).length <= 0) {
$this.attr('disabled', true).addClass('disabled');
}
});
});
},
/**
* Get selected product list
*
* @returns {Array}
* @private
*/
_CalcProducts: function($skipAttributeId) {
var $widget = this,
products = [];
// Generate intersection of products
$widget.element.find('.' + $widget.options.classes.attributeClass + '[option-selected]').each(function() {
var id = $(this).attr('attribute-id');
var option = $(this).attr('option-selected');
if ($skipAttributeId != undefined && $skipAttributeId == id) {
return;
}
if (!$widget.optionsMap.hasOwnProperty(id) || !$widget.optionsMap[id].hasOwnProperty(option)) {
return;
}
if (products.length == 0) {
products = $widget.optionsMap[id][option].products;
} else {
products = _.intersection(products, $widget.optionsMap[id][option].products);
}
});
return products;
},
/**
* Update total price
*
* @private
*/
_UpdatePrice: function() {
var $widget = this,
$product = $widget.element.parents($widget.options.selectorProduct),
$productPrice = $product.find(this.options.selectorProductPrice),
options = _.object(_.keys($widget.optionsMap), {}),
result;
$widget.element.find('.' + $widget.options.classes.attributeClass + '[option-selected]').each(function() {
var attributeId = $(this).attr('attribute-id'),
selectedOptionId = $(this).attr('option-selected');
options[attributeId] = selectedOptionId;
});
result = $widget.options.jsonConfig.optionPrices[_.findKey($widget.options.jsonConfig.index, options)];
$productPrice.trigger(
'updatePrice', {
'prices': $widget._getPrices(result, $productPrice.priceBox('option').prices)
}
);
},
/**
* Get prices
* @param {Object} newPrices
* @returns {Object}
* @private
*/
_getPrices: function(newPrices, displayPrices) {
var $widget = this;
if (_.isEmpty(newPrices)) {
newPrices = $widget.options.jsonConfig.prices;
}
_.each(displayPrices, function(price, code) {
if (newPrices[code]) {
displayPrices[code].amount = newPrices[code].amount - displayPrices[code].amount;
}
});
return displayPrices;
},
/**
* Gets all product media and change current to the needed one
*
* @private
*/
_LoadProductMedia: function() {
var $widget = this,
$this = $widget.element,
attributes = {},
productId = 0;
if (!$widget.options.mediaCallback) {
return;
}
$this.find('[option-selected]').each(function() {
var $selected = $(this);
attributes[$selected.attr('attribute-code')] = $selected.attr('option-selected');
});
if ($('body.catalog-product-view').size() > 0) {
//Product Page
productId = document.getElementsByName('product')[0].value;
} else {
//Category View
productId = $this.parents('.product.details.product-item-details')
.find('.price-box.price-final_price').attr('data-product-id');
}
var additional = $.parseParams(window.location.search.substring(1));
$widget._XhrKiller();
$widget._EnableProductMediaLoader($this);
$widget.xhr = $.post(
$widget.options.mediaCallback, {
product_id: productId,
attributes: attributes,
isAjax: true,
additional: additional
},
function(data) {
$widget._ProductMediaCallback($this, data);
$widget._DisableProductMediaLoader($this);
},
'json'
).done(function() {
$widget._XhrKiller();
});
},
/**
* Enable loader
*
* @param $this
* @private
*/
_EnableProductMediaLoader: function($this) {
var $widget = this;
if ($('body.catalog-product-view').size() > 0) {
$this.parents('.column.main').find('.photo.image')
.addClass($widget.options.classes.loader);
} else {
//Category View
$this.parents('.product-item-info').find('.product-image-photo')
.addClass($widget.options.classes.loader);
}
},
/**
* Disable loader
*
* @param $this
* @private
*/
_DisableProductMediaLoader: function($this) {
var $widget = this;
if ($('body.catalog-product-view').size() > 0) {
$this.parents('.column.main').find('.photo.image')
.removeClass($widget.options.classes.loader);
} else {
//Category View
$this.parents('.product-item-info').find('.product-image-photo')
.removeClass($widget.options.classes.loader);
}
},
/**
* Callback for product media
*
* @param $this
* @param response
* @private
*/
_ProductMediaCallback: function($this, response) {
var isProductViewExist = $('body.catalog-product-view').size() > 0,
$main = isProductViewExist ? $this.parents('.column.main') : $this.parents('.product-item-info'),
$widget = this,
images = [],
support = function(e) {
return e.hasOwnProperty('large') && e.hasOwnProperty('medium') && e.hasOwnProperty('small');
};
if ($widget._ObjectLength(response) < 1) {
this.updateBaseImage(this.options.mediaGalleryInitial, $main, isProductViewExist);
return;
}
if (support(response)) {
images.push({
full: response.large,
img: response.medium,
thumb: response.small
});
if (response.hasOwnProperty('gallery')) {
$.each(response.gallery, function() {
if (!support(this) || response.large === this.large) {
return;
}
images.push({
full: this.large,
img: this.medium,
thumb: this.small
});
});
}
}
this.updateBaseImage(images, $main, isProductViewExist);
},
/**
* Update [gallery-placeholder] or [product-image-photo]
* @param {Array} images
* @param {jQuery} context
* @param {Boolean} isProductViewExist
*/
updateBaseImage: function(images, context, isProductViewExist) {
var justAnImage = images[0];
if (isProductViewExist) {
context
.find('[data-gallery-role=gallery-placeholder]')
.data('gallery')
.updateData(images);
} else if (justAnImage && justAnImage.img) {
context.find('.product-image-photo').attr('src', justAnImage.img);
}
},
/**
* Kill doubled AJAX requests
*
* @private
*/
_XhrKiller: function() {
var $widget = this;
if ($widget.xhr !== undefined && $widget.xhr !== null) {
$widget.xhr.abort();
$widget.xhr = null;
}
},
/**
* Emulate mouse click on all swatches that should be selected
*
* @private
*/
_EmulateSelected: function() {
var $widget = this,
$this = $widget.element,
request = $.parseParams(window.location.search.substring(1));
$.each(request, function(key, value) {
$this.find('.' + $widget.options.classes.attributeClass + '[attribute-code="' + key + '"] [option-id="' + value + '"]').trigger('click');
});
},
/**
* Returns an array/object's length
* @param obj
* @returns {number}
* @private
*/
_ObjectLength: function(obj) {
var size = 0,
key;
for (key in obj) {
if (obj.hasOwnProperty(key)) size++;
}
return size;
}
});
My problems are:
Server reached "pm.max_children". Which is very strange for a server with one on it.
Error in browser console vendor/magento/module-customer/view/frontend/web/js/customer-data.js
:
getFromServer: function (sectionNames, updateSectionId) {
sectionNames = sectionConfig.filterClientSideSections(sectionNames);
var parameters = _.isArray(sectionNames) ? {sections: sectionNames.join(',')} : [];
parameters['update_section_id'] = updateSectionId;
return $.getJSON(options.sectionLoadUrl, parameters).fail(function(jqXHR) {
throw new Error(jqXHR);
});
}
I get around this by increasing the pm.max_children
and clearing cookies, cache in my browser. Then it works fine but it is not a solution.
I know this is pretty messed up and vague question. But suggestions or any help are appreciated very much. Thanks in advance.