diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py
index 8e87d0e95f2..c6a84b95274 100644
--- a/addons/website_sale/__manifest__.py
+++ b/addons/website_sale/__manifest__.py
@@ -59,6 +59,8 @@
'website_sale/static/src/js/website_sale_tracking.js',
'website/static/lib/multirange/multirange_custom.js',
'website_sale/static/src/js/website_sale_category_link.js',
+ 'website_sale/static/src/xml/website_sale_image_viewer.xml',
+ 'website_sale/static/src/js/components/website_sale_image_viewer.js',
],
'web._assets_primary_variables': [
'website_sale/static/src/scss/primary_variables.scss',
diff --git a/addons/website_sale/static/src/js/components/website_sale_image_viewer.js b/addons/website_sale/static/src/js/components/website_sale_image_viewer.js
new file mode 100644
index 00000000000..091bcf5215c
--- /dev/null
+++ b/addons/website_sale/static/src/js/components/website_sale_image_viewer.js
@@ -0,0 +1,158 @@
+/** @odoo-module **/
+
+import { useWowlService } from '@web/legacy/utils';
+import { Dialog } from "@web/core/dialog/dialog";
+import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
+
+const { Component, onRendered, useRef, useEffect, useState, xml } = owl;
+
+const ZOOM_STEP = 0.1;
+
+export class ProductImageViewer extends Dialog {
+ setup() {
+ super.setup();
+ this.imageContainerRef = useRef("imageContainer");
+ this.images = [...this.props.images].map(image => {
+ return {
+ src: image.dataset.zoomImage || image.src,
+ thumbnailSrc: image.src.replace('/image_1024/', '/image_128/'),
+ };
+ });
+ this.state = useState({
+ selectedImageIdx: this.props.selectedImageIdx || 0,
+ imageScale: 1,
+ });
+ this.isDragging = false;
+ this.dragStartPos = { x: 0, y: 0 };
+ // Doing a full render for the translate is too slow.
+ this.imageTranslate = { x: 0, y: 0 };
+ useHotkey("arrowleft", this.previousImage.bind(this));
+ useHotkey("arrowright", this.nextImage.bind(this));
+ useHotkey("r", () => {
+ this.imageTranslate = { x: 0, y: 0 };
+ this.isDragging = false;
+ this.state.imageScale = 1;
+ this.updateImage();
+ });
+
+ // Not using a t-on-click on purpose because we want to be able to cancel the drag
+ // when we go outside of the window.
+ useEffect(
+ (document) => {
+ const onGlobalClick = this.onGlobalClick.bind(this);
+ document.addEventListener("click", onGlobalClick);
+ return () => {document.removeEventListener("click", onGlobalClick)};
+ },
+ () => [document],
+ );
+ // For some reason the styling does not always update properly.
+ onRendered(() => {
+ this.updateImage();
+ })
+ }
+
+ get selectedImage() {
+ return this.images[this.state.selectedImageIdx];
+ }
+
+ set selectedImage(image) {
+ this.state.imageScale = 1;
+ this.imageTranslate = { x: 0, y: 0 };
+ this.state.selectedImageIdx = this.images.indexOf(image);
+ }
+
+ get imageStyle() {
+ return `transform:
+ scale3d(${this.state.imageScale}, ${this.state.imageScale}, 1);
+ `;
+ }
+
+ get imageContainerStyle() {
+ return `transform: translate(${this.imageTranslate.x}px, ${this.imageTranslate.y}px);`;
+ }
+
+ previousImage() {
+ this.selectedImage = this.images[(this.state.selectedImageIdx - 1 + this.images.length) % this.images.length];
+ }
+
+ nextImage() {
+ this.selectedImage = this.images[(this.state.selectedImageIdx + 1) % this.images.length];
+ }
+
+ updateImage() {
+ if (!this.imageContainerRef || !this.imageContainerRef.el) {
+ return;
+ }
+ this.imageContainerRef.el.style = this.imageContainerStyle;
+ }
+
+ onGlobalClick(ev) {
+ if (ev.target.tagName === "IMG") {
+ // Only zoom if the image did not move
+ if (this.dragStartPos.clientX === ev.clientX && this.dragStartPos.clientY === ev.clientY) {
+ this.zoomIn(ZOOM_STEP * 3);
+ }
+ }
+ if (ev.target.classList.contains('o_wsale_image_viewer_void') && !this.isDragging) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ this.data.close();
+ } else {
+ this.isDragging = false;
+ }
+ }
+
+ zoomIn(step=undefined) {
+ this.state.imageScale += step || ZOOM_STEP;
+ }
+
+ zoomOut(step=undefined) {
+ this.state.imageScale = Math.max(0.5, this.state.imageScale - (step || ZOOM_STEP));
+ }
+
+ onWheelImage(ev) {
+ if (ev.deltaY > 0) {
+ this.zoomOut();
+ } else {
+ this.zoomIn();
+ }
+ }
+
+ onMousedownImage(ev) {
+ this.isDragging = true;
+ this.dragStartPos = {
+ x: ev.clientX - this.imageTranslate.x,
+ y: ev.clientY - this.imageTranslate.y,
+ clientX: ev.clientX,
+ clientY: ev.clientY,
+ };
+ }
+
+ onGlobalMousemove(ev) {
+ if (!this.isDragging) {
+ return;
+ }
+ this.imageTranslate.x = ev.clientX - this.dragStartPos.x;
+ this.imageTranslate.y = ev.clientY - this.dragStartPos.y;
+ this.updateImage();
+ }
+}
+ProductImageViewer.props = {
+ ...Dialog.props,
+ images: { type: NodeList, required: true },
+ selectedImageIdx: { type: Number, optional: true },
+ close: Function,
+};
+delete ProductImageViewer.props.slots;
+ProductImageViewer.template = "website_sale.ProductImageViewer";
+
+export class ProductImageViewerWrapper extends Component {
+ setup() {
+ this.dialogs = useWowlService('dialog');
+
+ onRendered(() => {
+ this.dialogs.add(ProductImageViewer, this.props);
+ });
+ }
+}
+ProductImageViewerWrapper.template = xml``;
diff --git a/addons/website_sale/static/src/js/website_sale.editor.js b/addons/website_sale/static/src/js/website_sale.editor.js
index d3c3985af6b..481e48543ef 100644
--- a/addons/website_sale/static/src/js/website_sale.editor.js
+++ b/addons/website_sale/static/src/js/website_sale.editor.js
@@ -701,22 +701,44 @@ options.registry.WebsiteSaleProductPage = options.Class.extend({
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector}));
},
- /**
- * @override
- */
- setImageWidth(previewMode, widgetValue, params) {
- this._updateWebsiteConfig({
- product_page_image_width: widgetValue,
+ _getZoomOptionData() {
+ return this._userValueWidgets.find(widget => {
+ return widget.options && widget.options.dataAttributes && widget.options.dataAttributes.name === "o_wsale_zoom_mode";
});
},
/**
* @override
*/
- setImageLayout(previewMode, widgetValue, params) {
- this._updateWebsiteConfig({
- product_page_image_layout: widgetValue,
- });
+ async setImageWidth(previewMode, widgetValue, params) {
+ const zoomOption = this._getZoomOptionData();
+ const updateWidth = this._updateWebsiteConfig.bind(this, { product_page_image_width: widgetValue });
+ if (!zoomOption || widgetValue !== "100_pc") {
+ updateWidth();
+ } else {
+ const defaultZoomOption = "website_sale.product_picture_magnify_click";
+ await this._customizeWebsiteData(defaultZoomOption, { possibleValues: zoomOption._methodsParams.optionsPossibleValues["customizeWebsiteViews"] }, true);
+ updateWidth();
+ }
+ },
+
+ /**
+ * @override
+ */
+ async setImageLayout(previewMode, widgetValue, params) {
+ const zoomOption = this._getZoomOptionData();
+ const updateLayout = this._updateWebsiteConfig.bind(this, { product_page_image_layout: widgetValue });
+ if (!zoomOption) {
+ updateLayout();
+ } else {
+ const imageWidthOption = this.productDetailMain.dataset.image_width;
+ let defaultZoomOption = widgetValue === "grid" ? "website_sale.product_picture_magnify_click" : "website_sale.product_picture_magnify_hover";
+ if (imageWidthOption === "100_pc" && defaultZoomOption === "website_sale.product_picture_magnify_hover") {
+ defaultZoomOption = "website_sale.product_picture_magnify_click";
+ }
+ await this._customizeWebsiteData(defaultZoomOption, { possibleValues: zoomOption._methodsParams.optionsPossibleValues["customizeWebsiteViews"] }, true);
+ updateLayout();
+ }
},
/**
@@ -846,12 +868,25 @@ options.registry.WebsiteSaleProductPage = options.Class.extend({
},
async _computeWidgetVisibility(widgetName, params) {
+ const hasImages = this.productDetailMain.dataset.image_width != 'none';
+ const isFullImage = this.productDetailMain.dataset.image_width == '100_pc';
switch (widgetName) {
case 'o_wsale_thumbnail_pos':
- return Boolean(this.productPageCarousel);
+ return Boolean(this.productPageCarousel) && hasImages;
case 'o_wsale_grid_spacing':
case 'o_wsale_grid_columns':
- return Boolean(this.productPageGrid);
+ return Boolean(this.productPageGrid) && hasImages;
+ case 'o_wsale_image_layout':
+ case 'o_wsale_zoom_click':
+ case 'o_wsale_zoom_none':
+ case 'o_wsale_replace_main_image':
+ case 'o_wsale_add_extra_images':
+ case 'o_wsale_clear_extra_images':
+ case 'o_wsale_zoom_mode':
+ return hasImages;
+ case 'o_wsale_zoom_hover':
+ case 'o_wsale_zoom_both':
+ return hasImages && !isFullImage;
}
return this._super(widgetName, params);
}
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 74c34253cb6..dfdc5b930cf 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -207,6 +207,8 @@ require("web.zoomodoo");
const {extraMenuUpdateCallbacks} = require('website.content.menu');
const dom = require('web.dom');
const { cartesian } = require('@web/core/utils/arrays');
+const { ComponentWrapper } = require('web.OwlCompatibility');
+const { ProductImageViewerWrapper } = require("@website_sale/js/components/website_sale_image_viewer");
publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, cartHandlerMixin, {
selector: '.oe_website_sale',
@@ -280,6 +282,10 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, car
this.getRedirectOption();
return def;
},
+ destroy() {
+ this._super.apply(this, arguments);
+ this._cleanupZoom();
+ },
/**
* The selector is different when using list view of variants.
*
@@ -460,38 +466,97 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, car
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 () {
- // Do not activate image zoom for mobile devices, since it might prevent users from scrolling the page
- if (!config.device.isMobile) {
- var autoZoom = $('.ecom-zoomable').data('ecom-zoom-auto') || false,
- attach = '#o-carousel-product';
- _.each($('.ecom-zoomable img[data-zoom]'), function (el) {
- onImageLoaded(el, function () {
- var $img = $(el);
- $img.zoomOdoo({event: autoZoom ? 'mouseenter' : 'click', attach: attach});
- $img.attr('data-zoom', 1);
- });
- });
+ // Do not activate image zoom on hover for mobile devices
+ const salePage = document.querySelector(".o_wsale_product_page");
+ if (!salePage || config.device.mobile || this._getProductImageWidth() === "none") {
+ return;
}
-
- function onImageLoaded(img, callback) {
- // On Chrome the load event already happened at this point so we
- // have to rely on complete. On Firefox it seems that the event is
- // always triggered after this so we can rely on it.
- //
- // However on the "complete" case we still want to keep listening to
- // the event because if the image is changed later (eg. product
- // configurator) a new load event will be triggered (both browsers).
- $(img).on('load', function () {
- callback();
- });
- if (img.complete) {
- callback();
+ this._cleanupZoom();
+ this.zoomCleanup = [];
+ // Zoom on hover
+ if (salePage.dataset.ecomZoomAuto) {
+ 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();
+ }
+ }
+ const dialog = new ComponentWrapper(this, ProductImageViewerWrapper, {
+ selectedImageIdx: [...images].indexOf(image),
+ images,
+ });
+ dialog.mount(document.body);
+ };
+ 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
@@ -500,14 +565,11 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, car
* @private
*/
_updateProductImage: function ($productContainer, displayImage, productId, productTemplateId, newImages, isCombinationPossible) {
- let $images = $productContainer.find('#o-carousel-product');
- if (!$images.length) {
- $images = $productContainer.find("#o-grid-product");
- }
+ 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 && window.location.search.indexOf('enable_editor') === -1) {
+ if ($images.length && !this._isEditorEnabled()) {
const $newImages = $(newImages);
$images.after($newImages);
$images.remove();
diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss
index adfcb5eb754..6cfc8b1f8b8 100644
--- a/addons/website_sale/static/src/scss/website_sale.scss
+++ b/addons/website_sale/static/src/scss/website_sale.scss
@@ -516,8 +516,6 @@ a.no-decoration {
.carousel-inner {
img {
- height: 100%;
- width: 100%;
object-fit: contain;
}
}
@@ -666,21 +664,20 @@ a.no-decoration {
}
.ecom-zoomable {
- &:not(.ecom-autozoom) {
- img[data-zoom] {
+ &[data-ecom-zoom-click] {
+ img.product_detail_img {
cursor: zoom-in;
}
}
- &.ecom-autozoom {
- img[data-zoom] {
- cursor: crosshair;
- }
+ img[data-zoom] {
+ cursor: zoom-in;
}
.o_editable img[data-zoom] {
cursor: pointer;
}
.zoomodoo-flyout {
box-shadow: 0 0 20px 2px rgba(black, 0.2);
+ z-index: 1050;
}
}
diff --git a/addons/website_sale/static/src/scss/website_sale_frontend.scss b/addons/website_sale/static/src/scss/website_sale_frontend.scss
index 997d3a52f83..8beecf01eb3 100644
--- a/addons/website_sale/static/src/scss/website_sale_frontend.scss
+++ b/addons/website_sale/static/src/scss/website_sale_frontend.scss
@@ -138,3 +138,62 @@ $o-wsale-wizard-label-completed: $success;
}
}
}
+
+.o_wsale_image_viewer {
+ z-index: -1;
+
+ .o_wsale_image_viewer_header {
+ height: 40px;
+ z-index: 1;
+ }
+
+ .o_wsale_image_viewer_image {
+ .o_wsale_image_viewer_void {
+ padding-top: 64px;
+ padding-bottom: 156px;
+ }
+
+ img {
+ cursor: zoom-in;
+ }
+ }
+
+ .o_wsale_image_viewer_carousel {
+ z-index:1;
+
+ ol {
+ list-style: none;
+
+ .o_wsale_image_viewer_thumbnail {
+ width: 128px;
+ height: 128px;
+ }
+
+ li img{
+ border: 1px solid #CED4DA;
+
+ &.active {
+ border-color: #35979c;
+ }
+ }
+ }
+ }
+
+ .o_wsale_image_viewer_control {
+ z-index: 1;
+ width: 40px;
+ height: 40px;
+
+ &:hover {
+ background-color: rgba($gray-400, 0.1);
+ }
+
+ &.o_wsale_image_viewer_previous {
+ margin: 1px 1px 0 0; // not correctly centered for some reasons
+ }
+
+ &.o_wsale_image_viewer_next {
+ margin: 1px 0 0 1px; // not correctly centered for some reasons
+ }
+ }
+}
diff --git a/addons/website_sale/static/src/xml/website_sale_image_viewer.xml b/addons/website_sale/static/src/xml/website_sale_image_viewer.xml
new file mode 100644
index 00000000000..ca9ce6f7b91
--- /dev/null
+++ b/addons/website_sale/static/src/xml/website_sale_image_viewer.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+