From 3b87e527043b92b655e5c0db9dc9a5eff855c7dd Mon Sep 17 00:00:00 2001 From: Tung Dang Date: Wed, 11 May 2022 13:52:39 +0700 Subject: [PATCH 1/3] fix catalog filter --- view/frontend/requirejs-config.js | 3 +- view/frontend/web/js/swatch-renderer.js | 1464 +++++++++++++++++++++++ 2 files changed, 1466 insertions(+), 1 deletion(-) create mode 100644 view/frontend/web/js/swatch-renderer.js diff --git a/view/frontend/requirejs-config.js b/view/frontend/requirejs-config.js index eec98c8..c668f32 100644 --- a/view/frontend/requirejs-config.js +++ b/view/frontend/requirejs-config.js @@ -1,7 +1,8 @@ var config = { map: { '*': { - 'fotorama/fotorama':'Scaleflex_Cloudimage/js/fotorama' + 'fotorama/fotorama':'Scaleflex_Cloudimage/js/fotorama', + 'Magento_Swatches/js/swatch-renderer' : 'Scaleflex_Cloudimage/js/swatch-renderer' } } }; diff --git a/view/frontend/web/js/swatch-renderer.js b/view/frontend/web/js/swatch-renderer.js new file mode 100644 index 0000000..16e7bc4 --- /dev/null +++ b/view/frontend/web/js/swatch-renderer.js @@ -0,0 +1,1464 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'mage/template', + 'mage/smart-keyboard-handler', + 'mage/translate', + 'priceUtils', + 'jquery-ui-modules/widget', + 'jquery/jquery.parsequery', + 'mage/validation/validation' +], function ($, _, mageTemplate, keyboardHandler, $t, priceUtils) { + 'use strict'; + + /** + * Extend form validation to support swatch accessibility + */ + $.widget('mage.validation', $.mage.validation, { + /** + * Handle form with swatches validation. Focus on first invalid swatch block. + * + * @param {jQuery.Event} event + * @param {Object} validation + */ + listenFormValidateHandler: function (event, validation) { + var swatchWrapper, firstActive, swatches, swatch, successList, errorList, firstSwatch; + + this._superApply(arguments); + + swatchWrapper = '.swatch-attribute-options'; + swatches = $(event.target).find(swatchWrapper); + + if (!swatches.length) { + return; + } + + swatch = '.swatch-attribute'; + firstActive = $(validation.errorList[0].element || []); + successList = validation.successList; + errorList = validation.errorList; + firstSwatch = $(firstActive).parent(swatch).find(swatchWrapper); + + keyboardHandler.focus(swatches); + + $.each(successList, function (index, item) { + $(item).parent(swatch).find(swatchWrapper).attr('aria-invalid', false); + }); + + $.each(errorList, function (index, item) { + $(item.element).parent(swatch).find(swatchWrapper).attr('aria-invalid', true); + }); + + if (firstSwatch.length) { + $(firstSwatch).trigger('focus'); + } + } + }); + + /** + * Render tooltips by attributes (only to up). + * Required element attributes: + * - data-option-type (integer, 0-3) + * - data-option-label (string) + * - data-option-tooltip-thumb + * - data-option-tooltip-value + * - data-thumb-width + * - data-thumb-height + */ + $.widget('mage.SwatchRendererTooltip', { + options: { + delay: 200, //how much ms before tooltip to show + tooltipClass: 'swatch-option-tooltip' //configurable, but remember about css + }, + + /** + * @private + */ + _init: function () { + var $widget = this, + $this = this.element, + $element = $('.' + $widget.options.tooltipClass), + timer, + type = parseInt($this.data('option-type'), 10), + label = $this.data('option-label'), + thumb = $this.data('option-tooltip-thumb'), + value = $this.data('option-tooltip-value'), + width = $this.data('thumb-width'), + height = $this.data('thumb-height'), + $image, + $title, + $corner; + + if (!$element.length) { + $element = $('
' + ); + $('body').append($element); + } + + $image = $element.find('.image'); + $title = $element.find('.title'); + $corner = $element.find('.corner'); + + $this.on('mouseenter', function () { + if (!$this.hasClass('disabled')) { + timer = setTimeout( + function () { + var leftOpt = null, + leftCorner = 0, + left, + $window; + + if (type === 2) { + // Image + $image.css({ + 'background': 'url("' + thumb + '") no-repeat center', //Background case + 'background-size': 'initial', + 'width': width + 'px', + 'height': height + 'px' + }); + $image.show(); + } else if (type === 1) { + // Color + $image.css({ + background: value + }); + $image.show(); + } else if (type === 0 || type === 3) { + // Default + $image.hide(); + } + + $title.text(label); + + leftOpt = $this.offset().left; + left = leftOpt + $this.width() / 2 - $element.width() / 2; + $window = $(window); + + // the numbers (5 and 5) is magick constants for offset from left or right page + if (left < 0) { + left = 5; + } else if (left + $element.width() > $window.width()) { + left = $window.width() - $element.width() - 5; + } + + // the numbers (6, 3 and 18) is magick constants for offset tooltip + leftCorner = 0; + + if ($element.width() < $this.width()) { + leftCorner = $element.width() / 2 - 3; + } else { + leftCorner = (leftOpt > left ? leftOpt - left : left - leftOpt) + $this.width() / 2 - 6; + } + + $corner.css({ + left: leftCorner + }); + $element.css({ + left: left, + top: $this.offset().top - $element.height() - $corner.height() - 18 + }).show(); + }, + $widget.options.delay + ); + } + }); + + $this.on('mouseleave', function () { + $element.hide(); + clearTimeout(timer); + }); + + $(document).on('tap', function () { + $element.hide(); + clearTimeout(timer); + }); + + $this.on('tap', function (event) { + event.stopPropagation(); + }); + } + }); + + /** + * Render swatch controls with options and use tooltips. + * Required two json: + * - jsonConfig (magento's option config) + * - jsonSwatchConfig (swatch's option config) + * + * Tuning: + * - numberToShow (show "more" button if options are more) + * - onlySwatches (hide selectboxes) + * - moreButtonText (text for "more" button) + * - selectorProduct (selector for product container) + * - selectorProductPrice (selector for change price) + */ + $.widget('mage.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' + }, + // option's json config + jsonConfig: {}, + + // swatch's json config + jsonSwatchConfig: {}, + + // selector of parental block of prices and swatches (need to know where to seek for price block) + selectorProduct: '.product-info-main', + + // selector of price wrapper (need to know where set price) + selectorProductPrice: '[data-role=priceBox]', + + //selector of product images gallery wrapper + mediaGallerySelector: '[data-gallery-role=gallery-placeholder]', + + // selector of category product tile wrapper + selectorProductTile: '.product-item', + + // number of controls to show (false or zero = show all) + numberToShow: false, + + // show only swatch controls + onlySwatches: false, + + // enable label for control + enableControlLabel: true, + + // control label id + controlLabelId: '', + + // text for more button + moreButtonText: $t('More'), + + // Callback url for media + mediaCallback: '', + + // Local media cache + mediaCache: {}, + + // Cache for BaseProduct images. Needed when option unset + mediaGalleryInitial: [{}], + + // Use ajax to get image data + useAjax: false, + + /** + * Defines the mechanism of how images of a gallery should be + * updated when user switches between configurations of a product. + * + * As for now value of this option can be either 'replace' or 'prepend'. + * + * @type {String} + */ + gallerySwitchStrategy: 'replace', + + // whether swatches are rendered in product list or on product page + inProductList: false, + + // sly-old-price block selector + slyOldPriceSelector: '.sly-old-price', + + // tier prise selectors start + tierPriceTemplateSelector: '#tier-prices-template', + tierPriceBlockSelector: '[data-role="tier-price-block"]', + tierPriceTemplate: '', + // tier prise selectors end + + // A price label selector + normalPriceLabelSelector: '.product-info-main .normal-price .price-label' + }, + + /** + * Get chosen product + * + * @returns int|null + */ + getProduct: function () { + var products = this._CalcProducts(); + + return _.isArray(products) ? products[0] : null; + }, + + /** + * Get chosen product id + * + * @returns int|null + */ + getProductId: function () { + var products = this._CalcProducts(); + + return _.isArray(products) && products.length === 1 ? products[0] : null; + }, + + /** + * @private + */ + _init: function () { + // Don't render the same set of swatches twice + if ($(this.element).attr('data-rendered')) { + return; + } + + $(this.element).attr('data-rendered', true); + + if (_.isEmpty(this.options.jsonConfig.images)) { + this.options.useAjax = true; + // creates debounced variant of _LoadProductMedia() + // to use it in events handlers instead of _LoadProductMedia() + this._debouncedLoadProductMedia = _.debounce(this._LoadProductMedia.bind(this), 500); + } + + this.options.tierPriceTemplate = $(this.options.tierPriceTemplateSelector).html(); + + if (this.options.jsonConfig !== '' && this.options.jsonSwatchConfig !== '') { + // store unsorted attributes + this.options.jsonConfig.mappedAttributes = _.clone(this.options.jsonConfig.attributes); + this._sortAttributes(); + this._RenderControls(); + this._setPreSelectedGallery(); + $(this.element).trigger('swatch.initialized'); + } else { + console.log('SwatchRenderer: No input data received'); + } + }, + + /** + * @private + */ + _sortAttributes: function () { + this.options.jsonConfig.attributes = _.sortBy(this.options.jsonConfig.attributes, function (attribute) { + return parseInt(attribute.position, 10); + }); + }, + + /** + * @private + */ + _create: function () { + var options = this.options, + gallery = $('[data-gallery-role=gallery-placeholder]', '.column.main'), + productData = this._determineProductData(), + $main = productData.isInProductView ? + this.element.parents('.column.main') : + this.element.parents('.product-item-info'); + + if (productData.isInProductView) { + gallery.data('gallery') ? + this._onGalleryLoaded(gallery) : + gallery.on('gallery:loaded', this._onGalleryLoaded.bind(this, gallery)); + } else { + options.mediaGalleryInitial = [{ + 'img': $main.find('.product-image-photo').attr('src') + }]; + } + + this.productForm = this.element.parents(this.options.selectorProductTile).find('form:first'); + this.inProductList = this.productForm.length > 0; + }, + + /** + * Determine product id and related data + * + * @returns {{productId: *, isInProductView: bool}} + * @private + */ + _determineProductData: function () { + // Check if product is in a list of products. + var productId, + isInProductView = false; + + productId = this.element.parents('.product-item-details') + .find('.price-box.price-final_price').attr('data-product-id'); + + if (!productId) { + // Check individual product. + productId = $('[name=product]').val(); + isInProductView = productId > 0; + } + + return { + productId: productId, + isInProductView: isInProductView + }; + }, + + /** + * Render controls + * + * @private + */ + _RenderControls: function () { + var $widget = this, + container = this.element, + classes = this.options.classes, + chooseText = this.options.jsonConfig.chooseText, + showTooltip = this.options.showTooltip; + + $widget.optionsMap = {}; + + $.each(this.options.jsonConfig.attributes, function () { + var item = this, + controlLabelId = 'option-label-' + item.code + '-' + item.id, + options = $widget._RenderSwatchOptions(item, controlLabelId), + select = $widget._RenderSwatchSelect(item, chooseText), + input = $widget._RenderFormInput(item), + listLabel = '', + label = ''; + + // Show only swatch controls + if ($widget.options.onlySwatches && !$widget.options.jsonSwatchConfig.hasOwnProperty(item.id)) { + return; + } + + if ($widget.options.enableControlLabel) { + label += + '' + + $('').text(item.label).html() + + '' + + ''; + } + + if ($widget.inProductList) { + $widget.productForm.append(input); + input = ''; + listLabel = 'aria-label="' + $('').text(item.label).html() + '"'; + } else { + listLabel = 'aria-labelledby="' + controlLabelId + '"'; + } + + // Create new control + container.append( + '
' + + label + + '
' + + options + select + + '
' + input + + '
' + ); + + $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 + }; + } + }); + }); + + if (showTooltip === 1) { + // Connect Tooltip + container + .find('[data-option-type="1"], [data-option-type="2"],' + + ' [data-option-type="0"], [data-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($.parseQuery()); + $widget._EmulateSelected($widget._getSelectedAttributes()); + }, + + /** + * Render swatch options by part of config + * + * @param {Object} config + * @param {String} controlId + * @returns {String} + * @private + */ + _RenderSwatchOptions: function (config, controlId) { + var optionConfig = this.options.jsonSwatchConfig[config.id], + optionClass = this.options.classes.optionClass, + sizeConfig = this.options.jsonSwatchImageSizeConfig, + moreLimit = parseInt(this.options.numberToShow, 10), + moreClass = this.options.classes.moreButton, + moreText = this.options.moreButtonText, + countAttributes = 0, + html = ''; + + if (!this.options.jsonSwatchConfig.hasOwnProperty(config.id)) { + return ''; + } + + $.each(config.options, function (index) { + var id, + type, + value, + thumb, + label, + width, + height, + attr, + swatchImageWidth, + swatchImageHeight; + + if (!optionConfig.hasOwnProperty(this.id)) { + return ''; + } + + // Add more button + if (moreLimit === countAttributes++) { + html += '' + moreText + ''; + } + + id = this.id; + type = parseInt(optionConfig[id].type, 10); + value = optionConfig[id].hasOwnProperty('value') ? + $('').text(optionConfig[id].value).html() : ''; + thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : ''; + width = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.width : 110; + height = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.height : 90; + label = this.label ? $('').text(this.label).html() : ''; + attr = + ' id="' + controlId + '-item-' + id + '"' + + ' index="' + index + '"' + + ' aria-checked="false"' + + ' aria-describedby="' + controlId + '"' + + ' tabindex="0"' + + ' data-option-type="' + type + '"' + + ' data-option-id="' + id + '"' + + ' data-option-label="' + label + '"' + + ' aria-label="' + label + '"' + + ' role="option"' + + ' data-thumb-width="' + width + '"' + + ' data-thumb-height="' + height + '"'; + + attr += thumb !== '' ? ' data-option-tooltip-thumb="' + thumb + '"' : ''; + attr += value !== '' ? ' data-option-tooltip-value="' + value + '"' : ''; + + swatchImageWidth = _.has(sizeConfig, 'swatchImage') ? sizeConfig.swatchImage.width : 30; + swatchImageHeight = _.has(sizeConfig, 'swatchImage') ? sizeConfig.swatchImage.height : 20; + + if (!this.hasOwnProperty('products') || this.products.length <= 0) { + attr += ' data-option-empty="true"'; + } + + if (type === 0) { + // Text + html += '
' + (value ? value : label) + + '
'; + } else if (type === 1) { + // Color + html += '
' + '' + + '
'; + } else if (type === 2) { + // Image + html += '
' + '' + + '
'; + } else if (type === 3) { + // Clear + html += '
'; + } else { + // Default + html += '
' + label + '
'; + } + }); + + return html; + }, + + /** + * Render select by part of config + * + * @param {Object} config + * @param {String} chooseText + * @returns {String} + * @private + */ + _RenderSwatchSelect: function (config, chooseText) { + var html; + + if (this.options.jsonSwatchConfig.hasOwnProperty(config.id)) { + return ''; + } + + html = + ''; + + return html; + }, + + /** + * Input for submit form. + * This control shouldn't have "type=hidden", "display: none" for validation work :( + * + * @param {Object} config + * @private + */ + _RenderFormInput: function (config) { + return ''; + }, + + /** + * Event listener + * + * @private + */ + _EventListener: function () { + var $widget = this, + options = this.options.classes, + target; + + $widget.element.on('click', '.' + options.optionClass, function () { + return $widget._OnClick($(this), $widget); + }); + + $widget.element.on('change', '.' + options.selectClass, function () { + return $widget._OnChange($(this), $widget); + }); + + $widget.element.on('click', '.' + options.moreButton, function (e) { + e.preventDefault(); + + return $widget._OnMoreClick($(this)); + }); + + $widget.element.on('keydown', function (e) { + if (e.which === 13) { + target = $(e.target); + + if (target.is('.' + options.optionClass)) { + return $widget._OnClick(target, $widget); + } else if (target.is('.' + options.selectClass)) { + return $widget._OnChange(target, $widget); + } else if (target.is('.' + options.moreButton)) { + e.preventDefault(); + + return $widget._OnMoreClick(target); + } + } + }); + }, + + /** + * Load media gallery using ajax or json config. + * + * @private + */ + _loadMedia: function () { + var $main = this.inProductList ? + this.element.parents('.product-item-info') : + this.element.parents('.column.main'), + images; + + if (this.options.useAjax) { + this._debouncedLoadProductMedia(); + } else { + images = this.options.jsonConfig.images[this.getProduct()]; + + if (!images) { + images = this.options.mediaGalleryInitial; + } + this.updateBaseImage(this._sortImages(images), $main, !this.inProductList); + } + }, + + /** + * Sorting images array + * + * @private + */ + _sortImages: function (images) { + return _.sortBy(images, function (image) { + return parseInt(image.position, 10); + }); + }, + + /** + * Event for swatch options + * + * @param {Object} $this + * @param {Object} $widget + * @private + */ + _OnClick: function ($this, $widget) { + var $parent = $this.parents('.' + $widget.options.classes.attributeClass), + $wrapper = $this.parents('.' + $widget.options.classes.attributeOptionsWrapper), + $label = $parent.find('.' + $widget.options.classes.attributeSelectedOptionLabelClass), + attributeId = $parent.data('attribute-id'), + $input = $parent.find('.' + $widget.options.classes.attributeInput), + checkAdditionalData = JSON.parse(this.options.jsonSwatchConfig[attributeId]['additional_data']), + $priceBox = $widget.element.parents($widget.options.selectorProduct) + .find(this.options.selectorProductPrice); + + if ($widget.inProductList) { + $input = $widget.productForm.find( + '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]' + ); + } + + if ($this.hasClass('disabled')) { + return; + } + + if ($this.hasClass('selected')) { + $parent.removeAttr('data-option-selected').find('.selected').removeClass('selected'); + $input.val(''); + $label.text(''); + $this.attr('aria-checked', false); + } else { + $parent.attr('data-option-selected', $this.data('option-id')).find('.selected').removeClass('selected'); + $label.text($this.data('option-label')); + $input.val($this.data('option-id')); + $input.attr('data-attr-name', this._getAttributeCodeById(attributeId)); + $this.addClass('selected'); + $widget._toggleCheckedAttributes($this, $wrapper); + } + + $widget._Rebuild(); + + if ($priceBox.is(':data(mage-priceBox)')) { + $widget._UpdatePrice(); + } + + $(document).trigger('updateMsrpPriceBlock', + [ + this._getSelectedOptionPriceIndex(), + $widget.options.jsonConfig.optionPrices, + $priceBox + ]); + + if (parseInt(checkAdditionalData['update_product_preview_image'], 10) === 1) { + $widget._loadMedia(); + } + + $input.trigger('change'); + }, + + /** + * Get selected option price index + * + * @return {String|undefined} + * @private + */ + _getSelectedOptionPriceIndex: function () { + var allowedProduct = this._getAllowedProductWithMinPrice(this._CalcProducts()); + + if (_.isEmpty(allowedProduct)) { + return undefined; + } + + return allowedProduct; + }, + + /** + * Get human readable attribute code (eg. size, color) by it ID from configuration + * + * @param {Number} attributeId + * @returns {*} + * @private + */ + _getAttributeCodeById: function (attributeId) { + var attribute = this.options.jsonConfig.mappedAttributes[attributeId]; + + return attribute ? attribute.code : attributeId; + }, + + /** + * Toggle accessibility attributes + * + * @param {Object} $this + * @param {Object} $wrapper + * @private + */ + _toggleCheckedAttributes: function ($this, $wrapper) { + $wrapper.attr('aria-activedescendant', $this.attr('id')) + .find('.' + this.options.classes.optionClass).attr('aria-checked', false); + $this.attr('aria-checked', true); + }, + + /** + * Event for select + * + * @param {Object} $this + * @param {Object} $widget + * @private + */ + _OnChange: function ($this, $widget) { + var $parent = $this.parents('.' + $widget.options.classes.attributeClass), + attributeId = $parent.data('attribute-id'), + $input = $parent.find('.' + $widget.options.classes.attributeInput); + + if ($widget.productForm.length > 0) { + $input = $widget.productForm.find( + '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]' + ); + } + + if ($this.val() > 0) { + $parent.attr('data-option-selected', $this.val()); + $input.val($this.val()); + } else { + $parent.removeAttr('data-option-selected'); + $input.val(''); + } + + $widget._Rebuild(); + $widget._UpdatePrice(); + $widget._loadMedia(); + $input.trigger('change'); + }, + + /** + * Event for more switcher + * + * @param {Object} $this + * @private + */ + _OnMoreClick: function ($this) { + $this.nextAll().show(); + $this.trigger('blur').remove(); + }, + + /** + * Rewind options for controls + * + * @private + */ + _Rewind: function (controls) { + controls.find('div[data-option-id], option[data-option-id]') + .removeClass('disabled') + .prop('disabled', false); + controls.find('div[data-option-empty], option[data-option-empty]') + .attr('disabled', true) + .addClass('disabled') + .attr('tabindex', '-1'); + }, + + /** + * Rebuild container + * + * @private + */ + _Rebuild: function () { + var $widget = this, + controls = $widget.element.find('.' + $widget.options.classes.attributeClass + '[data-attribute-id]'), + selected = controls.filter('[data-option-selected]'); + + // Enable all options + $widget._Rewind(controls); + + // done if nothing selected + if (selected.length <= 0) { + return; + } + + // Disable not available options + controls.each(function () { + var $this = $(this), + id = $this.data('attribute-id'), + products = $widget._CalcProducts(id); + + if (selected.length === 1 && selected.first().data('attribute-id') === id) { + return; + } + + $this.find('[data-option-id]').each(function () { + var $element = $(this), + option = $element.data('option-id'); + + if (!$widget.optionsMap.hasOwnProperty(id) || !$widget.optionsMap[id].hasOwnProperty(option) || + $element.hasClass('selected') || + $element.is(':selected')) { + return; + } + + if (_.intersection(products, $widget.optionsMap[id][option].products).length <= 0) { + $element.attr('disabled', true).addClass('disabled'); + } + }); + }); + }, + + /** + * Get selected product list + * + * @returns {Array} + * @private + */ + _CalcProducts: function ($skipAttributeId) { + var $widget = this, + selectedOptions = '.' + $widget.options.classes.attributeClass + '[data-option-selected]', + products = []; + + // Generate intersection of products + $widget.element.find(selectedOptions).each(function () { + var id = $(this).data('attribute-id'), + option = $(this).attr('data-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), + result = $widget._getNewPrices(), + tierPriceHtml, + isShow; + + $productPrice.trigger( + 'updatePrice', + { + 'prices': $widget._getPrices(result, $productPrice.priceBox('option').prices) + } + ); + + isShow = typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount; + + $productPrice.find('span:first').toggleClass('special-price', isShow); + + $product.find(this.options.slyOldPriceSelector)[isShow ? 'show' : 'hide'](); + + if (typeof result != 'undefined' && result.tierPrices && result.tierPrices.length) { + if (this.options.tierPriceTemplate) { + tierPriceHtml = mageTemplate( + this.options.tierPriceTemplate, + { + 'tierPrices': result.tierPrices, + '$t': $t, + 'currencyFormat': this.options.jsonConfig.currencyFormat, + 'priceUtils': priceUtils + } + ); + $(this.options.tierPriceBlockSelector).html(tierPriceHtml).show(); + } + } else { + $(this.options.tierPriceBlockSelector).hide(); + } + + $(this.options.normalPriceLabelSelector).hide(); + + _.each($('.' + this.options.classes.attributeOptionsWrapper), function (attribute) { + if ($(attribute).find('.' + this.options.classes.optionClass + '.selected').length === 0) { + if ($(attribute).find('.' + this.options.classes.selectClass).length > 0) { + _.each($(attribute).find('.' + this.options.classes.selectClass), function (dropdown) { + if ($(dropdown).val() === '0') { + $(this.options.normalPriceLabelSelector).show(); + } + }.bind(this)); + } else { + $(this.options.normalPriceLabelSelector).show(); + } + } + }.bind(this)); + }, + + /** + * Get new prices for selected options + * + * @returns {*} + * @private + */ + _getNewPrices: function () { + var $widget = this, + newPrices = $widget.options.jsonConfig.prices, + allowedProduct = this._getAllowedProductWithMinPrice(this._CalcProducts()); + + if (!_.isEmpty(allowedProduct)) { + newPrices = this.options.jsonConfig.optionPrices[allowedProduct]; + } + + return newPrices; + }, + + /** + * Get prices + * + * @param {Object} newPrices + * @param {Object} displayPrices + * @returns {*} + * @private + */ + _getPrices: function (newPrices, displayPrices) { + var $widget = this; + + if (_.isEmpty(newPrices)) { + newPrices = $widget._getNewPrices(); + } + _.each(displayPrices, function (price, code) { + + if (newPrices[code]) { + displayPrices[code].amount = newPrices[code].amount - displayPrices[code].amount; + } + }); + + return displayPrices; + }, + + /** + * Get product with minimum price from selected options. + * + * @param {Array} allowedProducts + * @returns {String} + * @private + */ + _getAllowedProductWithMinPrice: function (allowedProducts) { + var optionPrices = this.options.jsonConfig.optionPrices, + product = {}, + optionFinalPrice, optionMinPrice; + + _.each(allowedProducts, function (allowedProduct) { + optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); + + if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { + optionMinPrice = optionFinalPrice; + product = allowedProduct; + } + }, this); + + return product; + }, + + /** + * Gets all product media and change current to the needed one + * + * @private + */ + _LoadProductMedia: function () { + var $widget = this, + $this = $widget.element, + productData = this._determineProductData(), + mediaCallData, + mediaCacheKey, + + /** + * Processes product media data + * + * @param {Object} data + * @returns void + */ + mediaSuccessCallback = function (data) { + if (!(mediaCacheKey in $widget.options.mediaCache)) { + $widget.options.mediaCache[mediaCacheKey] = data; + } + $widget._ProductMediaCallback($this, data, productData.isInProductView); + setTimeout(function () { + $widget._DisableProductMediaLoader($this); + }, 300); + }; + + if (!$widget.options.mediaCallback) { + return; + } + + mediaCallData = { + 'product_id': this.getProduct() + }; + + mediaCacheKey = JSON.stringify(mediaCallData); + + if (mediaCacheKey in $widget.options.mediaCache) { + $widget._XhrKiller(); + $widget._EnableProductMediaLoader($this); + mediaSuccessCallback($widget.options.mediaCache[mediaCacheKey]); + } else { + mediaCallData.isAjax = true; + $widget._XhrKiller(); + $widget._EnableProductMediaLoader($this); + $widget.xhr = $.ajax({ + url: $widget.options.mediaCallback, + cache: true, + type: 'GET', + dataType: 'json', + data: mediaCallData, + success: mediaSuccessCallback + }).done(function () { + $widget._XhrKiller(); + }); + } + }, + + /** + * Enable loader + * + * @param {Object} $this + * @private + */ + _EnableProductMediaLoader: function ($this) { + var $widget = this; + + if ($('body.catalog-product-view').length > 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 {Object} $this + * @private + */ + _DisableProductMediaLoader: function ($this) { + var $widget = this; + + if ($('body.catalog-product-view').length > 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 {Object} $this + * @param {String} response + * @param {Boolean} isInProductView + * @private + */ + _ProductMediaCallback: function ($this, response, isInProductView) { + var $main = isInProductView ? $this.parents('.column.main') : $this.parents('.product-item-info'), + $widget = this, + images = [], + + /** + * Check whether object supported or not + * + * @param {Object} e + * @returns {*|Boolean} + */ + support = function (e) { + return e.hasOwnProperty('large') && e.hasOwnProperty('medium') && e.hasOwnProperty('small'); + }; + + if (_.size($widget) < 1 || !support(response)) { + this.updateBaseImage(this.options.mediaGalleryInitial, $main, isInProductView); + + return; + } + + images.push({ + full: response.large, + img: response.medium, + thumb: response.small, + isMain: true + }); + + 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, isInProductView); + }, + + /** + * Check if images to update are initial and set their type + * @param {Array} images + */ + _setImageType: function (images) { + var initial = this.options.mediaGalleryInitial[0].img; + + if (images[0].img === initial) { + images = $.extend(true, [], this.options.mediaGalleryInitial); + } else { + images.map(function (img) { + if (!img.type) { + img.type = 'image'; + } + }); + } + + return images; + }, + + /** + * Update [gallery-placeholder] or [product-image-photo] + * @param {Array} images + * @param {jQuery} context + * @param {Boolean} isInProductView + */ + updateBaseImage: function (images, context, isInProductView) { + var justAnImage = images[0], + initialImages = this.options.mediaGalleryInitial, + imagesToUpdate, + gallery = context.find(this.options.mediaGallerySelector).data('gallery'), + isInitial; + + if (isInProductView) { + if (_.isUndefined(gallery)) { + context.find(this.options.mediaGallerySelector).on('gallery:loaded', function () { + this.updateBaseImage(images, context, isInProductView); + }.bind(this)); + + return; + } + + imagesToUpdate = images.length ? this._setImageType($.extend(true, [], images)) : []; + isInitial = _.isEqual(imagesToUpdate, initialImages); + + if (this.options.gallerySwitchStrategy === 'prepend' && !isInitial) { + imagesToUpdate = imagesToUpdate.concat(initialImages); + } + + imagesToUpdate = this._setImageIndex(imagesToUpdate); + + gallery.updateData(imagesToUpdate); + this._addFotoramaVideoEvents(isInitial); + } else if (justAnImage && justAnImage.img) { + context.find('.product-image-photo').attr('src', justAnImage.img); + if (window.ciResponsive) { + context.find('.product-image-photo').attr('ci-src', justAnImage.img); + context.find('.product-image-photo').attr('srcset', ''); + setTimeout(function() { + window.ciResponsive.process(); + }, 1000); + } + } + }, + + /** + * Add video events + * + * @param {Boolean} isInitial + * @private + */ + _addFotoramaVideoEvents: function (isInitial) { + if (_.isUndefined($.mage.AddFotoramaVideoEvents)) { + return; + } + + if (isInitial) { + $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); + + return; + } + + $(this.options.mediaGallerySelector).AddFotoramaVideoEvents({ + selectedOption: this.getProduct(), + dataMergeStrategy: this.options.gallerySwitchStrategy + }); + }, + + /** + * Set correct indexes for image set. + * + * @param {Array} images + * @private + */ + _setImageIndex: function (images) { + var length = images.length, + i; + + for (i = 0; length > i; i++) { + images[i].i = i + 1; + } + + return images; + }, + + /** + * 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 + * @param {Object} [selectedAttributes] + * @private + */ + _EmulateSelected: function (selectedAttributes) { + $.each(selectedAttributes, $.proxy(function (attributeCode, optionId) { + var elem = this.element.find('.' + this.options.classes.attributeClass + + '[data-attribute-code="' + attributeCode + '"] [data-option-id="' + optionId + '"]'), + parentInput = elem.parent(); + + if (elem.hasClass('selected')) { + return; + } + + if (parentInput.hasClass(this.options.classes.selectClass)) { + parentInput.val(optionId); + parentInput.trigger('change'); + } else { + elem.trigger('click'); + } + }, this)); + }, + + /** + * Emulate mouse click or selection change on all swatches that should be selected + * @param {Object} [selectedAttributes] + * @private + */ + _EmulateSelectedByAttributeId: function (selectedAttributes) { + $.each(selectedAttributes, $.proxy(function (attributeId, optionId) { + var elem = this.element.find('.' + this.options.classes.attributeClass + + '[data-attribute-id="' + attributeId + '"] [data-option-id="' + optionId + '"]'), + parentInput = elem.parent(); + + if (elem.hasClass('selected')) { + return; + } + + if (parentInput.hasClass(this.options.classes.selectClass)) { + parentInput.val(optionId); + parentInput.trigger('change'); + } else { + elem.trigger('click'); + } + }, this)); + }, + + /** + * Get default options values settings with either URL query parameters + * @private + */ + _getSelectedAttributes: function () { + var hashIndex = window.location.href.indexOf('#'), + selectedAttributes = {}, + params; + + if (hashIndex !== -1) { + params = $.parseQuery(window.location.href.substr(hashIndex + 1)); + + selectedAttributes = _.invert(_.mapObject(_.invert(params), function (attributeId) { + var attribute = this.options.jsonConfig.mappedAttributes[attributeId]; + + return attribute ? attribute.code : attributeId; + }.bind(this))); + } + + return selectedAttributes; + }, + + /** + * Callback which fired after gallery gets initialized. + * + * @param {HTMLElement} element - DOM element associated with a gallery. + */ + _onGalleryLoaded: function (element) { + var galleryObject = element.data('gallery'); + + this.options.mediaGalleryInitial = galleryObject.returnCurrentImages(); + }, + + /** + * Sets mediaCache for cases when jsonConfig contains preSelectedGallery on layered navigation result pages + * + * @private + */ + _setPreSelectedGallery: function () { + var mediaCallData; + + if (this.options.jsonConfig.preSelectedGallery) { + mediaCallData = { + 'product_id': this.getProduct() + }; + + this.options.mediaCache[JSON.stringify(mediaCallData)] = this.options.jsonConfig.preSelectedGallery; + } + } + }); + + return $.mage.SwatchRenderer; +}); From 55b3308e4a033663779a8e40d8d1cf8b63826cd9 Mon Sep 17 00:00:00 2001 From: Tung Dang Date: Thu, 12 May 2022 15:00:09 +0700 Subject: [PATCH 2/3] fix image load in catalog --- view/frontend/web/js/swatch-renderer.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/view/frontend/web/js/swatch-renderer.js b/view/frontend/web/js/swatch-renderer.js index 16e7bc4..ee6656a 100644 --- a/view/frontend/web/js/swatch-renderer.js +++ b/view/frontend/web/js/swatch-renderer.js @@ -1296,13 +1296,14 @@ define([ gallery.updateData(imagesToUpdate); this._addFotoramaVideoEvents(isInitial); } else if (justAnImage && justAnImage.img) { - context.find('.product-image-photo').attr('src', justAnImage.img); if (window.ciResponsive) { context.find('.product-image-photo').attr('ci-src', justAnImage.img); - context.find('.product-image-photo').attr('srcset', ''); - setTimeout(function() { - window.ciResponsive.process(); - }, 1000); + context.find('.product-image-photo').removeClass('ci-image-loaded'); + context.find('.product-image-photo').removeClass('ci-image'); + context.find('.product-image-photo').removeClass('lazyloaded'); + window.ciResponsive.process(); + } else { + context.find('.product-image-photo').attr('src', justAnImage.img); } } }, From ae30192503348118fe61a9977bb369be4be33837 Mon Sep 17 00:00:00 2001 From: Tung Dang Date: Mon, 1 Aug 2022 15:28:49 +0700 Subject: [PATCH 3/3] version 2.0.4 pre release --- Block/Tag.php | 6 +++++- CHANGELOG.md | 5 +++++ composer.json | 2 +- etc/module.xml | 2 +- view/frontend/templates/tag.phtml | 2 +- view/frontend/web/js/swatch-renderer.js | 2 +- 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Block/Tag.php b/Block/Tag.php index 9bd6401..51efd6a 100644 --- a/Block/Tag.php +++ b/Block/Tag.php @@ -72,7 +72,11 @@ public function getConfigToString(): string } $config .= 'devicePixelRatioList: ' . $this->formatRatioList($this->config->getDevicePixelRatio()) . ', '; if ($this->config->isOrgIfSml()) { - $config .= 'params: {org_if_sml: 1}, '; + $config .= "params: 'org_if_sml=1&".$this->config->getLibraryOptions()."', "; + } else { + if ($this->config->getLibraryOptions()) { + $config .= "params: '".$this->config->getLibraryOptions()."', "; + } } $config .= 'token:\'' . $this->config->getToken() . '\' '; return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md index 39e8fbf..3a7007c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## [2.0.4] - 2022-08-01 + +* Support ```params``` options with Library option +* Fix bug related to Swatch renderer, Catalog image not loaded through CI when change option in Catalog page + ## [2.0.3] - 2022-04-27 * Support Multiple store views(websites) diff --git a/composer.json b/composer.json index 0371337..3321760 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "ext-libxml": "*" }, "type": "magento2-module", - "version": "2.0.3", + "version": "2.0.4", "license": "BSD-3-Clause", "authors": [ { diff --git a/etc/module.xml b/etc/module.xml index 41a8b64..d7a468b 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,7 +1,7 @@ - + diff --git a/view/frontend/templates/tag.phtml b/view/frontend/templates/tag.phtml index 066a1f4..b6bf8be 100644 --- a/view/frontend/templates/tag.phtml +++ b/view/frontend/templates/tag.phtml @@ -25,7 +25,7 @@ window.lazySizesConfig.init = false; - +