From 7f730f81ec541cc7791fc6b3fded17c838433f85 Mon Sep 17 00:00:00 2001 From: Benjamin Vray Date: Thu, 20 Apr 2023 10:35:44 +0200 Subject: [PATCH] [IMP] web_editor, website: add an "on hover" option for animations This commit adds an "on hover" option for animations. We've added the "on hover" option to the animation selector. This feature is only available for images. It allows you to pick an animation that triggers when you hover your mouse over an image. There are six different animations to choose from. This new animation option doesn't work with animated shapes and devices shapes. task-3094258 closes odoo/odoo#119197 Signed-off-by: Quentin Smetz (qsm) --- .../components/media_dialog/media_dialog.js | 12 + .../static/src/js/editor/snippets.options.js | 324 +++++++++++++++++- addons/web_editor/views/snippets.xml | 27 +- .../src/js/content/snippets.animation.js | 133 +++++++ .../static/src/js/editor/snippets.options.js | 42 ++- .../website/static/src/svg/hover_effects.svg | 201 +++++++++++ addons/website/views/snippets/snippets.xml | 36 +- 7 files changed, 744 insertions(+), 31 deletions(-) create mode 100644 addons/website/static/src/svg/hover_effects.svg 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 -