/** @odoo-module **/ import publicWidget from "@web/legacy/js/public/public_widget"; import VariantMixin from "@website_sale/js/sale_variant_mixin"; import wSaleUtils from "@website_sale/js/website_sale_utils"; const cartHandlerMixin = wSaleUtils.cartHandlerMixin; import "@website/libs/zoomodoo/zoomodoo"; import {extraMenuUpdateCallbacks} from "@website/js/content/menu"; import { ProductImageViewer } from "@website_sale/js/components/website_sale_image_viewer"; import { rpc } from "@web/core/network/rpc"; import { debounce, throttleForAnimation } from "@web/core/utils/timing"; import { listenSizeChange, SIZES, utils as uiUtils } from "@web/core/ui/ui_service"; import { isBrowserFirefox, hasTouch } from "@web/core/browser/feature_detection"; import { Component } from "@odoo/owl"; export const WebsiteSale = publicWidget.Widget.extend(VariantMixin, cartHandlerMixin, { selector: '.oe_website_sale', events: Object.assign({}, VariantMixin.events || {}, { 'change form .js_product:first input[name="add_qty"]': '_onChangeAddQuantity', 'mouseup .js_publish': '_onMouseupPublish', 'touchend .js_publish': '_onMouseupPublish', 'change .oe_cart input.js_quantity[data-product-id]': '_onChangeCartQuantity', 'click .oe_cart a.js_add_suggested_products': '_onClickSuggestedProduct', 'click a.js_add_cart_json': '_onClickAddCartJSON', 'click .a-submit': '_onClickSubmit', 'change form.js_attributes input, form.js_attributes select': '_onChangeAttribute', 'mouseup form.js_add_cart_json label': '_onMouseupAddCartLabel', 'touchend form.js_add_cart_json label': '_onMouseupAddCartLabel', 'submit .o_wsale_products_searchbar_form': '_onSubmitSaleSearch', 'click .toggle_summary': '_onToggleSummary', 'click #add_to_cart, .o_we_buy_now, #products_grid .o_wsale_product_btn .a-submit': 'async _onClickAdd', 'click input.js_product_change': 'onChangeVariant', 'change .js_main_product [data-attribute_exclusions]': 'onChangeVariant', 'click .o_product_page_reviews_link': '_onClickReviewsLink', 'mousedown .o_wsale_filmstip_wrapper': '_onMouseDown', 'mouseleave .o_wsale_filmstip_wrapper': '_onMouseLeave', 'mouseup .o_wsale_filmstip_wrapper': '_onMouseUp', 'mousemove .o_wsale_filmstip_wrapper': '_onMouseMove', 'click .o_wsale_filmstip_wrapper' : '_onClickHandler', 'submit': '_onClickConfirmOrder', }), /** * @constructor */ init: function () { this._super.apply(this, arguments); this._changeCartQuantity = debounce(this._changeCartQuantity.bind(this), 500); this.isWebsite = true; this.filmStripStartX = 0; this.filmStripIsDown = false; this.filmStripScrollLeft = 0; this.filmStripMoved = false; delete this.events['change [data-attribute_exclusions]']; }, /** * @override */ start() { const def = this._super(...arguments); this._applyHash(); // This has to be triggered to compute the "out of stock" feature and the hash variant changes this.triggerVariantChange(this.$el); listenSizeChange(() => { if (uiUtils.getSize() === SIZES.XL) { $('.toggle_summary_div').addClass('d-none d-xl-block'); } }) this._startZoom(); // Triggered when selecting a variant of a product in a carousel element window.addEventListener("hashchange", (ev) => { this._applyHash(); this.triggerVariantChange(this.$el); }); // This allows conditional styling for the filmstrip const filmstripContainer = this.el.querySelector('.o_wsale_filmstip_container'); const filmstripContainerWidth = filmstripContainer ? filmstripContainer.getBoundingClientRect().width : 0; const filmstripWrapper = this.el.querySelector('.o_wsale_filmstip_wrapper'); const filmstripWrapperWidth = filmstripWrapper ? filmstripWrapper.getBoundingClientRect().width : 0; const isFilmstripScrollable = filmstripWrapperWidth < filmstripContainerWidth if (isBrowserFirefox() || hasTouch() || isFilmstripScrollable) { filmstripContainer?.classList.add('o_wsale_filmstip_fancy_disabled'); } this.getRedirectOption(); return def; }, destroy() { this._super.apply(this, arguments); this._cleanupZoom(); }, /** * The selector is different when using list view of variants. * * @override */ getSelectedVariantValues: function ($container) { var combination = $container.find('input.js_product_change:checked') .data('combination'); if (combination) { return combination; } return VariantMixin.getSelectedVariantValues.apply(this, arguments); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- _onMouseDown: function (ev) { this.filmStripIsDown = true; this.filmStripStartX = ev.pageX - ev.currentTarget.offsetLeft; this.filmStripScrollLeft = ev.currentTarget.scrollLeft; this.formerTarget = ev.target; this.filmStripMoved = false; }, _onMouseLeave: function (ev) { if (!this.filmStripIsDown) { return; } ev.currentTarget.classList.remove('activeDrag'); this.filmStripIsDown = false }, _onMouseUp: function (ev) { this.filmStripIsDown = false; ev.currentTarget.classList.remove('activeDrag'); }, _onMouseMove: function (ev) { if (!this.filmStripIsDown) { return; } ev.preventDefault(); ev.currentTarget.classList.add('activeDrag'); this.filmStripMoved = true; const x = ev.pageX - ev.currentTarget.offsetLeft; const walk = (x - this.filmStripStartX) * 2; ev.currentTarget.scrollLeft = this.filmStripScrollLeft - walk; }, _onClickHandler: function(ev) { if(this.filmStripMoved) { ev.stopPropagation(); ev.preventDefault(); } }, _applyHash: function () { const params = new URLSearchParams(window.location.hash.substring(1)); if (params.get("attribute_values")) { const attributeValueIds = params.get("attribute_values").split(','); const inputs = document.querySelectorAll( 'input.js_variant_change, select.js_variant_change option' ); inputs.forEach((element) => { if (attributeValueIds.includes(element.dataset.attributeValueId)) { if (element.tagName === "INPUT") { element.checked = true; } else if (element.tagName === "OPTION") { element.selected = true; } } }); this._changeAttribute(['.css_attribute_color', '.o_variant_pills']); } }, /** * Sets the url hash from the selected product options. * * @private */ _setUrlHash: function ($parent) { const inputs = document.querySelectorAll( 'input.js_variant_change:checked, select.js_variant_change option:checked' ); let attributeIds = []; inputs.forEach((element) => attributeIds.push(element.dataset.attributeValueId)); if (attributeIds.length > 0) { window.location.hash = `attribute_values=${attributeIds.join(',')}`; } }, /** * Set the checked values active. * * @private * @param {Array} valueSelectors Selectors */ _changeAttribute: function (valueSelectors) { valueSelectors.forEach((selector) => { $(selector).removeClass("active").filter(":has(input:checked)").addClass("active"); }); }, /** * @private */ _changeCartQuantity: function ($input, value, $dom_optional, line_id, productIDs) { $($dom_optional).toArray().forEach((elem) => { $(elem).find('.js_quantity').text(value); productIDs.push($(elem).find('span[data-product-id]').data('product-id')); }); $input.data('update_change', true); rpc("/shop/cart/update_json", { line_id: line_id, product_id: parseInt($input.data('product-id'), 10), set_qty: value, display: true, }).then((data) => { $input.data('update_change', false); var check_value = parseInt($input.val() || 0, 10); if (isNaN(check_value)) { check_value = 1; } if (value !== check_value) { $input.trigger('change'); return; } if (!data.cart_quantity) { return window.location = '/shop/cart'; } $input.val(data.quantity); $('.js_quantity[data-line-id='+line_id+']').val(data.quantity).text(data.quantity); wSaleUtils.updateCartNavBar(data); wSaleUtils.showWarning(data.notification_info.warning); // Propagating the change to the express checkout forms Component.env.bus.trigger('cart_amount_changed', [data.amount, data.minor_amount]); }); }, /** * This is overridden to handle the "List View of Variants" of the web shop. * That feature allows directly selecting the variant from a list instead of selecting the * attribute values. * * Since the layout is completely different, we need to fetch the product_id directly * from the selected variant. * * @override */ _getProductId: function ($parent) { if ($parent.find('input.js_product_change').length !== 0) { return parseInt($parent.find('input.js_product_change:checked').val()); } else { return VariantMixin._getProductId.apply(this, arguments); } }, _getProductImageLayout: function () { return document.querySelector("#product_detail_main").dataset.image_layout; }, _getProductImageWidth: function () { return document.querySelector("#product_detail_main").dataset.image_width; }, _getProductImageContainerSelector: function () { return { 'carousel': "#o-carousel-product", 'grid': "#o-grid-product", }[this._getProductImageLayout()]; }, _getProductImageContainer: function () { return document.querySelector(this._getProductImageContainerSelector()); }, _isEditorEnabled() { return document.body.classList.contains("editor_enable"); }, /** * @private */ _startZoom: function () { const salePage = document.querySelector(".o_wsale_product_page"); if (!salePage || this._getProductImageWidth() === "none") { return; } this._cleanupZoom(); this.zoomCleanup = []; // Zoom on hover (except on mobile) if (salePage.dataset.ecomZoomAuto && !uiUtils.isSmall()) { const images = salePage.querySelectorAll("img[data-zoom]"); for (const image of images) { const $image = $(image); const callback = () => { $image.zoomOdoo({ event: "mouseenter", attach: this._getProductImageContainerSelector(), preventClicks: salePage.dataset.ecomZoomClick, attachToTarget: this._getProductImageLayout() === "grid", }); image.dataset.zoom = 1; }; image.addEventListener('load', callback); this.zoomCleanup.push(() => { image.removeEventListener('load', callback); const zoomOdoo = $image.data("zoomOdoo"); if (zoomOdoo) { zoomOdoo.hide(); $image.unbind(); } }); if (image.complete) { callback(); } } } // Zoom on click if (salePage.dataset.ecomZoomClick) { // In this case we want all the images not just the ones that are "zoomables" const images = salePage.querySelectorAll(".product_detail_img"); for (const image of images ) { const handler = () => { if (salePage.dataset.ecomZoomAuto) { // Remove any flyout const flyouts = document.querySelectorAll(".zoomodoo-flyout"); for (const flyout of flyouts) { flyout.remove(); } } this.call("dialog", "add", ProductImageViewer, { selectedImageIdx: [...images].indexOf(image), images, }); }; image.addEventListener("click", handler); this.zoomCleanup.push(() => { image.removeEventListener("click", handler); }); } } }, _cleanupZoom() { if (!this.zoomCleanup || !this.zoomCleanup.length) { return; } for (const cleanup of this.zoomCleanup) { cleanup(); } this.zoomCleanup = undefined; }, /** * On website, we display a carousel instead of only one image * * @override * @private */ _updateProductImage: function ($productContainer, displayImage, productId, productTemplateId, newImages, isCombinationPossible) { let $images = $productContainer.find(this._getProductImageContainerSelector()); // When using the web editor, don't reload this or the images won't // be able to be edited depending on if this is done loading before // or after the editor is ready. if ($images.length && !this._isEditorEnabled()) { const $newImages = $(newImages); $images.after($newImages); $images.remove(); $images = $newImages; // Update the sharable image (only work for Pinterest). const shareImageSrc = $images[0].querySelector('img').src; document.querySelector('meta[property="og:image"]') .setAttribute('content', shareImageSrc); if ($images.attr('id') === 'o-carousel-product') { $images.carousel(0); } this._startZoom(); // fix issue with carousel height this.trigger_up('widgets_start_request', {$target: $images}); } $images.toggleClass('css_not_available', !isCombinationPossible); }, /** * @private * @param {MouseEvent} ev */ _onClickAdd: function (ev) { ev.preventDefault(); var def = () => { this.getCartHandlerOptions(ev); return this._handleAdd($(ev.currentTarget).closest('form')); }; if ($('.js_add_cart_variants').children().length) { return this._getCombinationInfo(ev).then(() => { return !$(ev.target).closest('.js_product').hasClass("css_not_available") ? def() : Promise.resolve(); }); } return def(); }, /** * Initializes the optional products modal * and add handlers to the modal events (confirm, back, ...) * * @private * @param {$.Element} $form the related webshop form */ _handleAdd: function ($form) { var self = this; this.$form = $form; var productSelector = [ 'input[type="hidden"][name="product_id"]', 'input[type="radio"][name="product_id"]:checked' ]; const productTemplateId = parseInt($form.find('input[type="hidden"][name="product_template_id"]').first().val()); var productReady = this.selectOrCreateProduct( $form, parseInt($form.find(productSelector.join(', ')).first().val(), 10), productTemplateId, ); return productReady.then(function (productId) { $form.find(productSelector.join(', ')).val(productId); self._updateRootProduct($form, productId, productTemplateId); return self._onProductReady($form.closest('.o_wsale_product_page').length > 0); }); }, _onProductReady(isOnProductPage = false) { return this._submitForm(); }, /** * Add custom variant values and attribute values that do not generate variants * in the params to submit form if 'stay on page' option is disabled, or call * '_addToCartInPage' otherwise. * * @private * @returns {Promise} */ _submitForm: function () { const params = this.rootProduct; const $product = $('#product_detail'); const productTrackingInfo = $product.data('product-tracking-info'); if (productTrackingInfo) { productTrackingInfo.quantity = params.quantity; $product.trigger('add_to_cart_event', [productTrackingInfo]); } params.add_qty = params.quantity; params.product_custom_attribute_values = JSON.stringify(params.product_custom_attribute_values); params.no_variant_attribute_values = JSON.stringify(params.no_variant_attribute_values); delete params.quantity; return this.addToCart(params); }, /** * @private * @param {MouseEvent} ev */ _onClickAddCartJSON: function (ev) { this.onClickAddCartJSON(ev); }, /** * @private * @param {Event} ev */ _onChangeAddQuantity: function (ev) { this.onChangeAddQuantity(ev); }, /** * @private * @param {Event} ev */ _onMouseupPublish: function (ev) { $(ev.currentTarget).parents('.thumbnail').toggleClass('disabled'); }, /** * @private * @param {Event} ev */ _onChangeCartQuantity: function (ev) { var $input = $(ev.currentTarget); if ($input.data('update_change')) { return; } var value = parseInt($input.val() || 0, 10); if (isNaN(value)) { value = 1; } var $dom = $input.closest('tr'); // var default_price = parseFloat($dom.find('.text-danger > span.oe_currency_value').text()); var $dom_optional = $dom.nextUntil(':not(.optional_product.info)'); var line_id = parseInt($input.data('line-id'), 10); var productIDs = [parseInt($input.data('product-id'), 10)]; this._changeCartQuantity($input, value, $dom_optional, line_id, productIDs); }, /** * @private * @param {Event} ev */ _onClickSuggestedProduct: function (ev) { $(ev.currentTarget).prev('input').val(1).trigger('change'); }, /** * @private * @param {Event} ev */ _onClickSubmit: function (ev) { if ($(ev.currentTarget).is('#add_to_cart, #products_grid .a-submit')) { return; } var $aSubmit = $(ev.currentTarget); if (!ev.defaultPrevented && !$aSubmit.is(".disabled")) { ev.preventDefault(); $aSubmit.closest('form').submit(); } if ($aSubmit.hasClass('a-submit-disable')) { $aSubmit.addClass("disabled"); } if ($aSubmit.hasClass('a-submit-loading')) { var loading = ''; var fa_span = $aSubmit.find('span[class*="fa"]'); if (fa_span.length) { fa_span.replaceWith(loading); } else { $aSubmit.append(loading); } } }, /** * @private * @param {Event} ev */ _onChangeAttribute: function (ev) { if (!ev.defaultPrevented) { ev.preventDefault(); const productGrid = this.el.querySelector(".o_wsale_products_grid_table_wrapper"); if (productGrid) { productGrid.classList.add("opacity-50"); } $(ev.currentTarget).closest("form").submit(); } }, /** * @private * @param {Event} ev */ _onMouseupAddCartLabel: function (ev) { // change price when they are variants var $label = $(ev.currentTarget); var $price = $label.parents("form:first").find(".oe_price .oe_currency_value"); if (!$price.data("price")) { $price.data("price", parseFloat($price.text())); } var value = $price.data("price") + parseFloat($label.find(".badge span").text() || 0); var dec = value % 1; $price.html(value + (dec < 0.01 ? ".00" : (dec < 1 ? "0" : "") )); }, /** * @private * @param {Event} ev */ _onSubmitSaleSearch: function (ev) { if (!this.$('.dropdown_sorty_by').length) { return; } var $this = $(ev.currentTarget); if (!ev.defaultPrevented && !$this.is(".disabled")) { ev.preventDefault(); var oldurl = $this.attr('action'); oldurl += (oldurl.indexOf("?")===-1) ? "?" : ""; if ($this.find('[name=noFuzzy]').val() === "true") { oldurl += '&noFuzzy=true'; } var search = $this.find('input.search-query'); window.location = oldurl + '&' + search.attr('name') + '=' + encodeURIComponent(search.val()); } }, /** * Toggles the add to cart button depending on the possibility of the * current combination. * * @override */ _toggleDisable: function ($parent, isCombinationPossible) { VariantMixin._toggleDisable.apply(this, arguments); $parent.find("#add_to_cart").toggleClass('disabled', !isCombinationPossible); $parent.find(".o_we_buy_now").toggleClass('disabled', !isCombinationPossible); }, /** * Write the properties of the form elements in the DOM to prevent the * current selection from being lost when activating the web editor. * * @override */ onChangeVariant: function (ev) { var $component = $(ev.currentTarget).closest('.js_product'); $component.find('input').each(function () { var $el = $(this); $el.attr('checked', $el.is(':checked')); }); $component.find('select option').each(function () { var $el = $(this); $el.attr('selected', $el.is(':selected')); }); this._setUrlHash($component); return VariantMixin.onChangeVariant.apply(this, arguments); }, /** * @private */ _onToggleSummary: function () { $('.toggle_summary_div').toggleClass('d-none'); $('.toggle_summary_div').removeClass('d-xl-block'); }, /** * @private */ _onClickReviewsLink: function () { $('#o_product_page_reviews_content').collapse('show'); }, /** * Prevent multiclicks on confirm button when the form is submitted * * @private */ _onClickConfirmOrder: function () { // FIXME ANVFE this should only be triggered when we effectively click on that specific button no? // should not impact the address page at least const submitFormButton = $('form[name="o_wsale_confirm_order"]').find('button[type="submit"]'); submitFormButton.attr('disabled', true); setTimeout(() => submitFormButton.attr('disabled', false), 5000); }, // ------------------------------------- // Utils // ------------------------------------- /** * Update the root product during an Add process. * * @private * @param {Object} $form * @param {Number} productId * @param {Number} productTemplateId */ _updateRootProduct($form, productId, productTemplateId) { this.rootProduct = { product_id: productId, product_template_id: productTemplateId, quantity: parseFloat($form.find('input[name="add_qty"]').val() || 1), product_custom_attribute_values: this.getCustomVariantValues($form.find('.js_product')), variant_values: this.getSelectedVariantValues($form.find('.js_product')), no_variant_attribute_values: this.getNoVariantAttributeValues($form.find('.js_product')) }; }, }); publicWidget.registry.WebsiteSale = WebsiteSale publicWidget.registry.WebsiteSaleLayout = publicWidget.Widget.extend({ selector: '.oe_website_sale', disabledInEditableMode: false, events: { 'change .o_wsale_apply_layout input': '_onApplyShopLayoutChange', }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private * @param {Event} ev */ _onApplyShopLayoutChange: function (ev) { const wysiwyg = this.options.wysiwyg; if (wysiwyg) { wysiwyg.odooEditor.observerUnactive('_onApplyShopLayoutChange'); } var clickedValue = $(ev.target).val(); var isList = clickedValue === 'list'; if (!this.editableMode) { rpc('/shop/save_shop_layout_mode', { 'layout_mode': isList ? 'list' : 'grid', }); } const activeClasses = ev.target.parentElement.dataset.activeClasses.split(' '); ev.target.parentElement.querySelectorAll('.btn').forEach((btn) => { activeClasses.map(c => btn.classList.toggle(c)); }); var $grid = this.$('#products_grid'); // Disable transition on all list elements, then switch to the new // layout then reenable all transitions after having forced a redraw // TODO should probably be improved to allow disabling transitions // altogether with a class/option. $grid.find('*').css('transition', 'none'); $grid.toggleClass('o_wsale_layout_list', isList); void $grid[0].offsetWidth; $grid.find('*').css('transition', ''); if (wysiwyg) { wysiwyg.odooEditor.observerActive('_onApplyShopLayoutChange'); } }, }); publicWidget.registry.WebsiteSaleAccordionProduct = publicWidget.Widget.extend({ selector: "#product_accordion", /** * @override */ async start() { await this._super(...arguments); this._updateAccordionActiveItem(); }, /** * Replace the .SCSS styling applied awaiting Js for the default bootstrap classes, * opening the first accordion entry and restoring flush behavior. * * @private */ _updateAccordionActiveItem() { const firstAccordionItemEl = this.el.querySelector('.accordion-item'); if (!firstAccordionItemEl) return; const firstAccordionItemButtonEl = firstAccordionItemEl.querySelector('.accordion-button'); firstAccordionItemButtonEl.classList.remove('collapsed'); firstAccordionItemButtonEl.setAttribute('aria-expanded', 'true'); firstAccordionItemEl.querySelector('.accordion-collapse').classList.add('show'); this.target.classList.remove('o_accordion_not_initialized'); }, }); publicWidget.registry.websiteSaleCarouselProduct = publicWidget.Widget.extend({ selector: '#o-carousel-product', disabledInEditableMode: false, events: { 'wheel .o_carousel_product_indicators': '_onMouseWheel', }, /** * @override */ async start() { await this._super(...arguments); this._updateCarouselPosition(); this.throttleOnResize = throttleForAnimation(this._onSlideCarouselProduct.bind(this)); extraMenuUpdateCallbacks.push(this._updateCarouselPosition.bind(this)); if (this.$el.find('.carousel-indicators').length > 0) { this.$el.on('slide.bs.carousel.carousel_product_slider', this._onSlideCarouselProduct.bind(this)); $(window).on('resize.carousel_product_slider', this.throttleOnResize); this._updateJustifyContent(); } }, /** * @override */ destroy() { this.$el.css('top', ''); this.$el.off('.carousel_product_slider'); if (this.throttleOnResize) { this.throttleOnResize.cancel(); } this._super(...arguments); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @private */ _updateCarouselPosition() { let size = 5; for (const el of document.querySelectorAll('.o_top_fixed_element')) { size += $(el).outerHeight(); } this.$el.css('top', size); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * Center the selected indicator to scroll the indicators list when it * overflows. * * @private * @param {Event} ev */ _onSlideCarouselProduct: function (ev) { const isReversed = this.$el.css('flex-direction') === "column-reverse"; const isLeftIndicators = this.$el.hasClass('o_carousel_product_left_indicators'); const $indicatorsDiv = isLeftIndicators ? this.$el.find('.o_carousel_product_indicators') : this.$el.find('.carousel-indicators'); let indicatorIndex = $(ev.relatedTarget).index(); indicatorIndex = indicatorIndex > -1 ? indicatorIndex : this.$el.find('li.active').index(); const $indicator = $indicatorsDiv.find('[data-bs-slide-to=' + indicatorIndex + ']'); const indicatorsDivSize = isLeftIndicators && !isReversed ? $indicatorsDiv.outerHeight() : $indicatorsDiv.outerWidth(); const indicatorSize = isLeftIndicators && !isReversed ? $indicator.outerHeight() : $indicator.outerWidth(); const indicatorPosition = isLeftIndicators && !isReversed ? $indicator.position().top : $indicator.position().left; const scrollSize = isLeftIndicators && !isReversed ? $indicatorsDiv[0].scrollHeight : $indicatorsDiv[0].scrollWidth; let indicatorsPositionDiff = (indicatorPosition + (indicatorSize/2)) - (indicatorsDivSize/2); indicatorsPositionDiff = Math.min(indicatorsPositionDiff, scrollSize - indicatorsDivSize); this._updateJustifyContent(); const indicatorsPositionX = isLeftIndicators && !isReversed ? '0' : '-' + indicatorsPositionDiff; const indicatorsPositionY = isLeftIndicators && !isReversed ? '-' + indicatorsPositionDiff : '0'; const translate3D = indicatorsPositionDiff > 0 ? "translate3d(" + indicatorsPositionX + "px," + indicatorsPositionY + "px,0)" : ''; $indicatorsDiv.css("transform", translate3D); }, /** * @private */ _updateJustifyContent: function () { const $indicatorsDiv = this.$el.find('.carousel-indicators'); $indicatorsDiv.css('justify-content', 'start'); if (uiUtils.getSize() <= SIZES.MD) { if (($indicatorsDiv.children().last().position().left + this.$el.find('li').outerWidth()) < $indicatorsDiv.outerWidth()) { $indicatorsDiv.css('justify-content', 'center'); } } }, /** * @private * @param {Event} ev */ _onMouseWheel: function (ev) { ev.preventDefault(); if (ev.originalEvent.deltaY > 0) { this.$el.carousel('next'); } else { this.$el.carousel('prev'); } }, }); publicWidget.registry.websiteSaleProductPageReviews = publicWidget.Widget.extend({ selector: '#o_product_page_reviews', disabledInEditableMode: false, /** * @override */ async start() { await this._super(...arguments); this._updateChatterComposerPosition(); extraMenuUpdateCallbacks.push(this._updateChatterComposerPosition.bind(this)); }, /** * @override */ destroy() { this.$el.find('.o_portal_chatter_composer').css('top', ''); this._super(...arguments); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @private */ _updateChatterComposerPosition() { let size = 20; for (const el of document.querySelectorAll('.o_top_fixed_element')) { size += $(el).outerHeight(); } this.$el.find('.o_portal_chatter_composer').css('top', size); }, }); export default { WebsiteSale: publicWidget.registry.WebsiteSale, WebsiteSaleLayout: publicWidget.registry.WebsiteSaleLayout, WebsiteSaleProductPage: publicWidget.registry.WebsiteSaleAccordionProduct, WebsiteSaleCarouselProduct: publicWidget.registry.websiteSaleCarouselProduct, WebsiteSaleProductPageReviews: publicWidget.registry.websiteSaleProductPageReviews, };