diff --git a/addons/web_editor/static/src/components/media_dialog/media_dialog.js b/addons/web_editor/static/src/components/media_dialog/media_dialog.js index 7892964a065..cb1cc65bfc1 100644 --- a/addons/web_editor/static/src/components/media_dialog/media_dialog.js +++ b/addons/web_editor/static/src/components/media_dialog/media_dialog.js @@ -180,6 +180,18 @@ export class MediaDialog extends Component { if (this.props.media.dataset.shapeRotate) { element.dataset.shapeRotate = this.props.media.dataset.shapeRotate; } + if (this.props.media.dataset.hoverEffect) { + element.dataset.hoverEffect = this.props.media.dataset.hoverEffect; + } + if (this.props.media.dataset.hoverEffectColor) { + element.dataset.hoverEffectColor = this.props.media.dataset.hoverEffectColor; + } + if (this.props.media.dataset.hoverEffectStrokeWidth) { + element.dataset.hoverEffectStrokeWidth = this.props.media.dataset.hoverEffectStrokeWidth; + } + if (this.props.media.dataset.hoverEffectIntensity) { + element.dataset.hoverEffectIntensity = this.props.media.dataset.hoverEffectIntensity; + } } } for (const otherTab of Object.keys(TABS).filter(key => key !== this.state.activeTab)) { diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index c623fae0f5b..7a5857a0abf 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -6115,6 +6115,13 @@ registry.ImageTools = ImageHandlerOption.extend({ async setImgShape(previewMode, widgetValue, params) { const img = this._getImg(); const saveData = previewMode === false; + if (img.dataset.hoverEffect && !widgetValue) { + // When a shape is removed and there is a hover effect on the + // image, we then place the "Square" shape as the default because a + // shape is required for the hover effects to work. + const shapeImgShapeWidget = this._requestUserValueWidgets("shape_img_square_opt")[0]; + widgetValue = shapeImgShapeWidget.getActiveValue("setImgShape"); + } if (widgetValue) { await this._loadShape(widgetValue); if (previewMode === 'reset' && img.dataset.shapeColors) { @@ -6131,9 +6138,18 @@ registry.ImageTools = ImageHandlerOption.extend({ } // When the user selects a shape, we remove the data attributes // that are not compatible with this shape. - if (saveData && !this._isTransformableShape()) { - delete img.dataset.shapeFlip; - delete img.dataset.shapeRotate; + if (saveData) { + if (!this._isTransformableShape()) { + delete img.dataset.shapeFlip; + delete img.dataset.shapeRotate; + } + if (params.animated || this._isDeviceShape()) { + delete img.dataset.hoverEffect; + delete img.dataset.hoverEffectColor; + delete img.dataset.hoverEffectStrokeWidth; + delete img.dataset.hoverEffectIntensity; + img.classList.remove("o_animate_on_hover"); + } } } } else { @@ -6198,6 +6214,111 @@ registry.ImageTools = ImageHandlerOption.extend({ async setImgShapeRotateRight(previewMode, widgetValue, params) { await this._setImgShapeRotate(90); }, + /** + * Sets the hover effects of the image shape. + * + * @see this.selectClass for parameters + */ + async setImgShapeHoverEffect(previewMode, widgetValue, params) { + const imgEl = this._getImg(); + delete imgEl.dataset.hoverEffectColor; + delete imgEl.dataset.hoverEffectIntensity; + delete imgEl.dataset.hoverEffectStrokeWidth; + if (params.name === "hover_effect_overlay_opt") { + imgEl.dataset.hoverEffectColor = this._getCSSColorValue("black-25"); + } else if (params.name === "hover_effect_outline_opt") { + imgEl.dataset.hoverEffectColor = this._getCSSColorValue("primary"); + imgEl.dataset.hoverEffectStrokeWidth = 10; + } else { + imgEl.dataset.hoverEffectIntensity = 20; + if (params.name !== "hover_effect_mirror_blur_opt") { + imgEl.dataset.hoverEffectColor = "rgba(0, 0, 0, 0)"; + } + } + await this._applyOptions(); + }, + /** + * @see this.selectClass for parameters + */ + async selectDataAttribute(previewMode, widgetValue, params) { + await this._super(...arguments); + if (["hoverEffectIntensity", "hoverEffectStrokeWidth"].includes(params.attributeName)) { + await this._applyOptions(); + } + }, + /** + * Sets the color of hover effects. + * + * @see this.selectClass for parameters + */ + async setHoverEffectColor(previewMode, widgetValue, params) { + const img = this._getImg(); + let defaultColor = "rgba(0, 0, 0, 0)"; + if (img.dataset.hoverEffect === "overlay") { + defaultColor = "black-25"; + } else if (img.dataset.hoverEffect === "outline") { + defaultColor = "primary"; + } + img.dataset.hoverEffectColor = this._getCSSColorValue(widgetValue || defaultColor); + await this._applyOptions(); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + notify(name) { + if (name === "enable_hover_effect") { + this.trigger_up("snippet_edition_request", {exec: () => { + // Add the "Overlay" hover effect to the shape. + const hoverEffectOverlayWidget = this._requestUserValueWidgets("hover_effect_overlay_opt")[0]; + hoverEffectOverlayWidget.enable(); + hoverEffectOverlayWidget.getParent().close(); // FIXME remove this ugly hack asap + // Add the "square" shape to the image if it has no shape + // because the "hover effects" need a shape to work. + const imgEl = this._getImg(); + const shapeName = imgEl.dataset.shape?.split("/")[2]; + if (!shapeName) { + const shapeImgSquareWidget = this._requestUserValueWidgets("shape_img_square_opt")[0]; + shapeImgSquareWidget.enable(); + shapeImgSquareWidget.getParent().close(); // FIXME remove this ugly hack asap + } + }}); + } else if (name === "disable_hover_effect") { + this._disableHoverEffect(); + } else { + this._super(...arguments); + } + }, + /** + * @override + */ + async updateUI() { + await this._super(...arguments); + // Adapts the colorpicker label according to the selected "On Hover" + // animation. + const hoverEffectName = this.$target[0].dataset.hoverEffect; + if (hoverEffectName) { + const hoverEffectColorWidget = this.findWidget("hover_effect_color_opt"); + const needToAdaptLabel = ["image_zoom_in", "image_zoom_out", "dolly_zoom"].includes(hoverEffectName); + const labelEl = hoverEffectColorWidget.el.querySelector("we-title"); + if (!this._originalHoverEffectColorLabel) { + this._originalHoverEffectColorLabel = labelEl.textContent; + } + labelEl.textContent = needToAdaptLabel + ? _t("Overlay") + : this._originalHoverEffectColorLabel; + } + // Move the "hover effects" options to the 'websiteAnimate' options. + const hoverEffectsOptionsEl = this.$el[0].querySelector("#o_hover_effects_options"); + const animationEffectWidget = this._requestUserValueWidgets("animation_effect_opt")[0]; + if (hoverEffectsOptionsEl && animationEffectWidget) { + animationEffectWidget.getParent().$el[0].append(hoverEffectsOptionsEl); + } + }, //-------------------------------------------------------------------------- // Private @@ -6285,6 +6406,7 @@ registry.ImageTools = ImageHandlerOption.extend({ async _writeShape(svgText) { const img = this._getImg(); const initialImageWidth = img.naturalWidth; + let needToRefreshPublicWidgets = false; const svg = new DOMParser().parseFromString(svgText, 'image/svg+xml').documentElement; @@ -6307,6 +6429,14 @@ registry.ImageTools = ImageHandlerOption.extend({ svg.querySelector("#filterPath").setAttribute("style", `transform: ${shapeTransformValues.join(" ")}; ${transformOrigin}`); } + // Add shape animations on hover. + if (img.dataset.hoverEffect && !this._isDeviceShape() && !this._isAnimatedShape()) { + this._addImageShapeHoverEffect(svg, img); + // The "ImageShapeHoverEffet" public widget needs to restart + // (e.g. image replacement). + needToRefreshPublicWidgets = true; + } + const svgAspectRatio = parseInt(svg.getAttribute('width')) / parseInt(svg.getAttribute('height')); // We will store the image in base64 inside the SVG. // applyModifications will return a dataURL with the current filters @@ -6319,7 +6449,9 @@ registry.ImageTools = ImageHandlerOption.extend({ }; const imgDataURL = await applyModifications(img, options); svg.removeChild(svg.querySelector('#preview')); - svg.querySelector('image').setAttribute('xlink:href', imgDataURL); + svg.querySelectorAll("image").forEach(image => { + image.setAttribute("xlink:href", imgDataURL); + }); // Force natural width & height (note: loading the original image is // needed for Safari where natural width & height of SVG does not return // the correct values). @@ -6342,7 +6474,11 @@ registry.ImageTools = ImageHandlerOption.extend({ const dataURL = await createDataURL(blob); const imgFilename = (img.dataset.originalSrc.split('/').pop()).split('.')[0]; img.dataset.fileName = `${imgFilename}.svg`; - return loadImage(dataURL, img); + const loadedImg = await loadImage(dataURL, img); + if (needToRefreshPublicWidgets) { + this._refreshPublicWidgets(); + } + return loadedImg; }, /** * @override @@ -6420,6 +6556,33 @@ registry.ImageTools = ImageHandlerOption.extend({ "img_shape_transform_rotate_x_opt", "img_shape_transform_rotate_y_opt"].includes(params.name)) { return this._isTransformableShape(); } + if (widgetName === "hover_effect_none_opt") { + // The hover effects are removed with the "WebsiteAnimate" animation + // selector so this option should not be visible. + return false; + } + if (params.optionsPossibleValues.setImgShapeHoverEffect) { + const imgEl = this._getImg(); + return imgEl.classList.contains("o_animate_on_hover") && !this._isDeviceShape() && !this._isAnimatedShape(); + } + // If "Description" or "Tooltip" options. + if (["alt", "title"].includes(params.attributeName)) { + const imgEl = this._getImg(); + return !imgEl.matches("[data-oe-type='image'] > img, [data-oe-xpath]"); + } + // The "Square" shape is only used for hover effects. It is + // automatically set when there is an hover effect and no shape is + // chosen by the user. This shape is always hidden in the shape select. + if (widgetName === "shape_img_square_opt") { + return false; + } + if (widgetName === "remove_img_shape_opt") { + // Do not show the "remove shape" button when the "square" shape is + // enable. The "square" shape is only enable when there is a hover + // effect and it is always hidden in the shape select. + const shapeImgShareWidget = this._requestUserValueWidgets("shape_img_square_opt")[0]; + return !shapeImgShareWidget.isActive(); + } return this._super(...arguments); }, /** @@ -6461,6 +6624,10 @@ registry.ImageTools = ImageHandlerOption.extend({ const imgEl = this._getImg(); return imgEl.dataset.shapeFlip?.includes("y") || ""; } + case 'setHoverEffectColor': { + const imgEl = this._getImg(); + return imgEl.dataset.hoverEffectColor || ""; + } } return this._super(...arguments); }, @@ -6578,6 +6745,10 @@ registry.ImageTools = ImageHandlerOption.extend({ delete img.dataset.originalMimetype; delete img.dataset.shapeFlip; delete img.dataset.shapeRotate; + delete img.dataset.hoverEffect; + delete img.dataset.hoverEffectColor; + delete img.dataset.hoverEffectStrokeWidth; + delete img.dataset.hoverEffectIntensity; return; } if (img.dataset.mimetype !== "image/svg+xml") { @@ -6670,6 +6841,149 @@ registry.ImageTools = ImageHandlerOption.extend({ const shapeImgWidget = this._requestUserValueWidgets("shape_img_opt")[0]; return (shapeImgWidget && !shapeImgWidget.getMethodsParams().noTransform) && !this._isDeviceShape(); }, + /** + * Checks if the shape is in animated. + * + * @private + * @returns {boolean} + */ + _isAnimatedShape() { + const shapeImgWidget = this._requestUserValueWidgets("shape_img_opt")[0]; + return shapeImgWidget && shapeImgWidget.getMethodsParams().animated; + }, + /** + * Adds hover effect to the SVG. + * + * @private + * @param {HTMLElement} svgEl + * @param {HTMLImageElement} [img] img element + */ + async _addImageShapeHoverEffect(svgEl, img) { + let rgba = null; + let rbg = null; + let opacity = null; + // Add the required parts for the hover effects to the SVG. + const hoverEffectName = img.dataset.hoverEffect; + if (!this.hoverEffectsSvg) { + this.hoverEffectsSvg = await this._getHoverEffects(); + } + const hoverEffectEls = this.hoverEffectsSvg.querySelectorAll(`#${hoverEffectName} > *`); + hoverEffectEls.forEach(hoverEffectEl => { + svgEl.appendChild(hoverEffectEl.cloneNode(true)); + }); + // Modifies the svg according to the chosen hover effect and the value + // of the options. + const animateEl = svgEl.querySelector("animate"); + const animateTransformEls = svgEl.querySelectorAll("animateTransform"); + const animateElValues = animateEl?.getAttribute("values"); + let animateTransformElValues = animateTransformEls[0]?.getAttribute("values"); + if (img.dataset.hoverEffectColor) { + rgba = convertCSSColorToRgba(img.dataset.hoverEffectColor); + rbg = `rgb(${rgba.red},${rgba.green},${rgba.blue})`; + opacity = rgba.opacity / 100; + if (!["outline", "image_mirror_blur"].includes(hoverEffectName)) { + svgEl.querySelector('[fill="hover_effect_color"]').setAttribute("fill", rbg); + animateEl.setAttribute("values", animateElValues.replace("hover_effect_opacity", opacity)); + } + } + switch (hoverEffectName) { + case "outline": { + svgEl.querySelector('[stroke="hover_effect_color"]').setAttribute("stroke", rbg); + svgEl.querySelector('[stroke-opacity="hover_effect_opacity"]').setAttribute("stroke-opacity", opacity); + // The stroke width needs to be multiplied by two because half + // of the stroke is invisible since it is centered on the path. + const strokeWidth = parseInt(img.dataset.hoverEffectStrokeWidth) * 2; + animateEl.setAttribute("values", animateElValues.replace("hover_effect_stroke_width", strokeWidth)); + break; + } + case "image_zoom_in": + case "image_zoom_out": + case "dolly_zoom": { + const imageEl = svgEl.querySelector("image"); + const clipPathEl = svgEl.querySelector("#clip-path"); + imageEl.setAttribute("id", "shapeImage"); + // Modify the SVG so that the clip-path is not zoomed when the + // image is zoomed. + imageEl.setAttribute("style", "transform-origin: center; width: 100%; height: 100%"); + imageEl.setAttribute("preserveAspectRatio", "none"); + svgEl.setAttribute("viewBox", "0 0 1 1"); + svgEl.setAttribute("preserveAspectRatio", "none"); + clipPathEl.setAttribute("clipPathUnits", "userSpaceOnUse"); + const clipPathValue = imageEl.getAttribute("clip-path"); + imageEl.removeAttribute("clip-path"); + const gEl = document.createElementNS("http://www.w3.org/2000/svg", "g"); + gEl.setAttribute("clip-path", clipPathValue); + imageEl.parentNode.replaceChild(gEl, imageEl); + gEl.appendChild(imageEl); + let zoomValue = 1.01 + parseInt(img.dataset.hoverEffectIntensity) / 200; + animateTransformEls[0].setAttribute("values", animateTransformElValues.replace("hover_effect_zoom", zoomValue)); + if (hoverEffectName === "image_zoom_out") { + // Set zoom intensity for the image. + const styleAttr = svgEl.querySelector("style"); + styleAttr.textContent = styleAttr.textContent.replace("hover_effect_zoom", zoomValue); + } + if (hoverEffectName === "dolly_zoom") { + clipPathEl.setAttribute("style", "transform-origin: center;"); + // Set zoom intensity for clip-path and overlay. + zoomValue = 0.99 - parseInt(img.dataset.hoverEffectIntensity) / 2000; + animateTransformEls.forEach((animateTransformEl, index) => { + if (index > 0) { + animateTransformElValues = animateTransformEl.getAttribute("values"); + animateTransformEl.setAttribute("values", animateTransformElValues.replace("hover_effect_zoom", zoomValue)); + } + }); + } + break; + } + case "image_mirror_blur": { + const imageEl = svgEl.querySelector("image"); + imageEl.setAttribute('id', 'shapeImage'); + imageEl.setAttribute('style', 'transform-origin: center;'); + const imageMirrorEl = imageEl.cloneNode(); + imageMirrorEl.setAttribute("id", 'shapeImageMirror'); + imageMirrorEl.setAttribute("filter", "url(#blurFilter)"); + imageEl.insertAdjacentElement("beforebegin", imageMirrorEl); + const zoomValue = 0.99 - parseInt(img.dataset.hoverEffectIntensity) / 200; + animateTransformEls[0].setAttribute("values", animateTransformElValues.replace("hover_effect_zoom", zoomValue)); + break; + } + } + }, + /** + * Gets the hover effects list. + * + * @private + * @returns {HTMLElement} + */ + _getHoverEffects() { + const hoverEffectsURL = "/website/static/src/svg/hover_effects.svg"; + return fetch(hoverEffectsURL) + .then(response => response.text()) + .then(text => { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(text, "text/xml"); + return xmlDoc.getElementsByTagName("svg")[0]; + }); + }, + /** + * Disables the hover effect on the image. + * + * @private + */ + async _disableHoverEffect() { + const imgEl = this._getImg(); + const shapeName = imgEl.dataset.shape?.split("/")[2]; + delete imgEl.dataset.hoverEffect; + delete imgEl.dataset.hoverEffectColor; + delete imgEl.dataset.hoverEffectStrokeWidth; + delete imgEl.dataset.hoverEffectIntensity; + await this._applyOptions(); + // If "Square" shape, remove it, it doesn't make sense to keep it + // without hover effect. + if (shapeName === "geo_square") { + this._requestUserValueWidgets("remove_img_shape_opt")[0].enable(); + } + }, //-------------------------------------------------------------------------- // Handlers diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml index 57eac2043c7..dd72328e416 100644 --- a/addons/web_editor/views/snippets.xml +++ b/addons/web_editor/views/snippets.xml @@ -480,17 +480,6 @@ -
- - -
-
@@ -518,7 +507,7 @@
Geometrics - + @@ -710,7 +699,8 @@
- + @@ -730,7 +720,14 @@ data-no-preview="true" data-dependencies="shape_img_opt" title="Rotate right"/> - + + @@ -744,7 +741,7 @@ - + Default 25% 50% diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index 59e6dc86087..df0bbf2d2ad 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -1681,6 +1681,139 @@ registry.ZoomedBackgroundShape = publicWidget.Widget.extend({ }, }); +registry.ImageShapeHoverEffet = publicWidget.Widget.extend({ + selector: "img[data-hover-effect]", + disabledInEditableMode: false, + events: { + "mouseenter": "_onMouseEnter", + "mouseleave": "_onMouseLeave", + }, + + /** + * @constructor + */ + init() { + this._super(...arguments); + this.lastMouseEvent = Promise.resolve(); + }, + /** + * @override + */ + start() { + this._super(...arguments); + this.originalImgSrc = this.el.src; + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + if (this.originalImgSrc && (this.lastImgSrc === this.el.src)) { + this.el.src = this.originalImgSrc; + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onMouseEnter() { + if (!this.originalImgSrc) { + return; + } + this.lastMouseEvent = this.lastMouseEvent.then(() => new Promise((resolve) => { + if (!this.svgInEl) { + fetch(this.el.src) + .then(response => response.text()) + .then(text => { + const parser = new DOMParser(); + const result = parser.parseFromString(text, "text/xml"); + const svg = result.getElementsByTagName("svg")[0]; + this.svgInEl = svg; + if (!this.svgInEl) { + resolve(); + return; + } + // Start animations. + const animateEls = this.svgInEl.querySelectorAll("#hoverEffects animateTransform, #hoverEffects animate"); + animateEls.forEach(animateTransformEl => { + animateTransformEl.removeAttribute("begin"); + }); + this._setImgSrc(this.svgInEl, resolve); + }); + } else { + this._setImgSrc(this.svgInEl, resolve); + } + })); + }, + /** + * @private + */ + _onMouseLeave() { + this.lastMouseEvent = this.lastMouseEvent.then(() => new Promise((resolve) => { + if (!this.originalImgSrc || !this.svgInEl) { + resolve(); + return; + } + if (!this.svgOutEl) { + // Reverse animations. + this.svgOutEl = this.svgInEl.cloneNode(true); + const animateTransformEls = this.svgOutEl.querySelectorAll("#hoverEffects animateTransform, #hoverEffects animate"); + animateTransformEls.forEach(animateTransformEl => { + let valuesValue = animateTransformEl.getAttribute("values"); + valuesValue = valuesValue.split(";").reverse().join(";"); + animateTransformEl.setAttribute("values", valuesValue); + }); + } + this._setImgSrc(this.svgOutEl, resolve); + })); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Converts the SVG to a data URI and set it as the image source. + * + * @private + * @param {HTMLElement} svg + */ + _setImgSrc(svg, resolve) { + // Add random class to prevent browser from caching image. Otherwise the + // animations do not trigger more than once. + const previousRandomClass = [...svg.classList].find(cl => cl.startsWith("o_shape_anim_random_")); + svg.classList.remove(previousRandomClass); + svg.classList.add("o_shape_anim_random_" + Date.now()); + // Convert the SVG element to a data URI. + const svg64 = btoa(new XMLSerializer().serializeToString(svg)); + // The image is preloaded to avoid a flickering when it is added to the + // DOM. + const preloadedImg = new Image(); + preloadedImg.src = `data:image/svg+xml;base64,${svg64}`; + preloadedImg.onload = () => { + if (this.isDestroyed()) { + // In some cases, it is possible for the "preloadedImg" to + // finish loading while the widget has already been destroyed. + // So, we do not set the image source because that can cause + // unexpected reverse of the animation. + resolve(); + return; + } + this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerUnactive("setImgHoverEffectSrc"); + this.el.src = preloadedImg.src; + this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerActive("setImgHoverEffectSrc"); + this.lastImgSrc = preloadedImg.src; + this.el.onload = () => { + resolve(); + }; + }; + }, +}); + export default { Widget: publicWidget.Widget, Animation: Animation, diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index de02e6cad68..0edc63180d1 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -3566,7 +3566,7 @@ options.registry.WebsiteAnimate = options.Class.extend({ this._forceAnimation(); } if (params.isAnimationTypeSelection) { - this.$target[0].classList.toggle('o_animate_preview', !!widgetValue); + this.$target[0].classList.toggle("o_animate_preview", this.$target[0].classList.contains("o_animate")); } }, /** @@ -3598,13 +3598,20 @@ options.registry.WebsiteAnimate = options.Class.extend({ delete this.$target[0].dataset.scrollZoneStart; delete this.$target[0].dataset.scrollZoneEnd; } - if (!params.activeValue && widgetValue) { - // If "Animation" was on "None" and it is no longer, it is set to - // "fade_in" by default. + if (params.activeValue === "o_animate_on_hover") { + this.trigger_up("option_update", { + optionName: "ImageTools", + name: "disable_hover_effect", + }); + } + if ((!params.activeValue || params.activeValue === "o_animate_on_hover") + && widgetValue && widgetValue !== "onHover") { + // If "Animation" was on "None" or "o_animate_on_hover" and it is no + // longer, it is set to "fade_in" by default. targetClassList.add('o_anim_fade_in'); this._toggleImagesLazyLoading(false); } - if (!widgetValue) { + if (!widgetValue || widgetValue === "onHover") { const possibleEffects = this._requestUserValueWidgets('animation_effect_opt')[0].getMethodsParams('selectClass').possibleValues; const possibleDirections = this._requestUserValueWidgets('animation_direction_opt')[0].getMethodsParams('selectClass').possibleValues; const possibleEffectsAndDirections = possibleEffects.concat(possibleDirections); @@ -3619,6 +3626,12 @@ options.registry.WebsiteAnimate = options.Class.extend({ this.$target[0].style.animationDuration = ''; this._toggleImagesLazyLoading(true); } + if (widgetValue === "onHover") { + this.trigger_up("option_update", { + optionName: "ImageTools", + name: "enable_hover_effect", + }); + } }, /** * Sets the animation intensity. @@ -3665,18 +3678,28 @@ options.registry.WebsiteAnimate = options.Class.extend({ * @override */ _computeWidgetVisibility(widgetName, params) { + const hasAnimateClass = this.$target[0].classList.contains("o_animate"); switch (widgetName) { case 'no_animation_opt': { return !this.isAnimatedText; } + case 'animation_effect_opt': { + return hasAnimateClass; + } case 'animation_trigger_opt': { return !this.$target[0].closest('.dropdown'); } case 'animation_on_scroll_opt': case 'animation_direction_opt': { + if (widgetName === "animation_direction_opt" && !hasAnimateClass) { + return false; + } return !this.limitedAnimations.some(className => this.$target[0].classList.contains(className)); } case 'animation_intensity_opt': { + if (!hasAnimateClass) { + return false; + } const possibleDirections = this._requestUserValueWidgets('animation_direction_opt')[0].getMethodsParams('selectClass').possibleValues; if (this.$target[0].classList.contains('o_anim_fade_in')) { for (const targetClass of this.$target[0].classList) { @@ -3691,6 +3714,15 @@ options.registry.WebsiteAnimate = options.Class.extend({ } return true; } + case 'animation_on_hover_opt': { + const [hoverEffectOverlayWidget] = this._requestUserValueWidgets("hover_effect_overlay_opt"); + if (hoverEffectOverlayWidget) { + const hoverEffectWidget = hoverEffectOverlayWidget.getParent(); + const imageToolsOpt = hoverEffectWidget.getParent(); + return !imageToolsOpt._isDeviceShape() && !imageToolsOpt._isAnimatedShape(); + } + return false; + } } return this._super(...arguments); }, diff --git a/addons/website/static/src/svg/hover_effects.svg b/addons/website/static/src/svg/hover_effects.svg new file mode 100644 index 00000000000..bd15d241cd7 --- /dev/null +++ b/addons/website/static/src/svg/hover_effects.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml index ad5a5a0bd65..bbd521ded25 100644 --- a/addons/website/views/snippets/snippets.xml +++ b/addons/website/views/snippets/snippets.xml @@ -347,6 +347,30 @@ no_animation_opt + + +
+ + None + Overlay + Zoom In + Zoom Out + Dolly Zoom + Outline + Mirror Blur + + + + +
+
+ @@ -1391,6 +1415,9 @@ data-select-class="o_animate o_animate_on_scroll" data-force-animation="true" data-name="animation_on_scroll_opt">On Scroll
+ On Hover @@ -1399,8 +1426,7 @@
- + None @@ -1438,8 +1464,7 @@ data-trigger="animation_direction_in_place_opt">Flip-In-Y
- Every Time -