diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index 5eb0863019d..5ad96d1bbcb 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -1082,6 +1082,165 @@ registry.BottomFixedElement = publicWidget.Widget.extend({ }, }); +registry.WebsiteAnimate = publicWidget.Widget.extend({ + selector: '#wrapwrap', + disabledInEditableMode: false, + + offsetRatio: 0.3, // Dynamic offset ratio: 0.3 = (element's height/3) + offsetMin: 10, // Minimum offset for small elements (in pixels) + + /** + * @override + */ + start() { + this.lastScroll = 0; + this.$scrollingElement = $().getScrollingElement(this.ownerDocument); + // By default, elements are hidden by the css of o_animate. + // Render elements and trigger the animation then pause it in state 0. + this.$animatedElements = this.$target.find('.o_animate'); + _.each(this.$animatedElements, el => { + this._resetAnimation($(el)); + }); + // Then we render all the elements, the ones which are invisible + // in state 0 (like fade_in for example) will stay invisible. + this.$animatedElements.css("visibility", "visible"); + + // We use addEventListener instead of jQuery because we need 'capture'. + // Setting capture to true allows to take advantage of event bubbling + // for events that otherwise don’t support it. (e.g. useful when + // scrolling a modal) + this.__onScrollWebsiteAnimate = _.throttle(this._onScrollWebsiteAnimate.bind(this), 200); + this.$scrollingElement[0].addEventListener('scroll', this.__onScrollWebsiteAnimate, {capture: true}); + + $(window).on('resize.o_animate, shown.bs.modal.o_animate, slid.bs.carousel.o_animate', () => { + this.windowsHeight = $(window).height(); + this.$scrollingElement[0].dispatchEvent(new Event('scroll')); + }).trigger("resize"); + + return this._super(...arguments); + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + this.$target.find('.o_animate') + .removeClass('o_animating o_animated o_animate_preview') + .css({ + 'animation-name': '', + 'animation-play-state': '', + 'visibility': '', + }); + $(window).off('.o_animate'); + this.$scrollingElement[0].removeEventListener('scroll', this.__onScrollWebsiteAnimate, {capture: true}); + this.$scrollingElement[0].classList.remove('o_wanim_overflow_x_hidden'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Starts animation and/or update element's state. + * + * @private + * @param {jQuery} $el + */ + _startAnimation($el) { + // Forces the browser to redraw using setTimeout. + setTimeout(() => { + this._toggleOverflowXHidden(true); + $el + .css({"animation-play-state": "running"}) + .addClass("o_animating") + .one('webkitAnimationEnd oanimationend msAnimationEnd animationend', () => { + $el.addClass("o_animated").removeClass("o_animating"); + this._toggleOverflowXHidden(false); + $(window).trigger("resize"); + }); + }); + }, + /** + * @private + * @param {jQuery} $el + */ + _resetAnimation($el) { + const animationName = $el.css("animation-name"); + $el.css({"animation-name": "dummy-none", "animation-play-state": ""}) + .removeClass("o_animated o_animating"); + + this._toggleOverflowXHidden(false); + // trigger a DOM reflow + void $el[0].offsetWidth; + $el.css({'animation-name': animationName , 'animation-play-state': 'paused'}); + }, + /** + * Shows/hides the horizontal scrollbar (on the #wrapwrap). + * + * @private + * @param {Boolean} add + */ + _toggleOverflowXHidden(add) { + if (add) { + this.$scrollingElement[0].classList.add('o_wanim_overflow_x_hidden'); + } else if (!this.$scrollingElement.find('.o_animating').length) { + this.$scrollingElement[0].classList.remove('o_wanim_overflow_x_hidden'); + } + }, + /** + * Gets element top offset by not taking CSS transforms into calculations. + * + * @private + * @param {Element} el + */ + _getElementOffsetTop(el) { + // Loop through the DOM tree and add its parent's offset to get page offset. + var top = 0; + do { + top += el.offsetTop || 0; + el = el.offsetParent; + } while (el); + return top; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onScrollWebsiteAnimate(ev) { + const scroll = $(ev.currentTarget).scrollTop(); + // Handle reverse scrolling + const direction = (scroll < this.lastScroll) ? -1 : 1; + this.lastScroll = scroll; + + _.each(this.$target.find('.o_animate'), el => { + const $el = $(el); + const elHeight = $el.height(); + const elOffset = direction * Math.max((elHeight * this.offsetRatio), this.offsetMin); + const state = $el.css("animation-play-state"); + + // We need to offset for the change in position from some animation. + // So we get the top value by not taking CSS transforms into calculations. + const $openModal = this.$target.find('.modal:visible'); + const scrollTop = $openModal.length ? $openModal.scrollTop() : this.$scrollingElement.scrollTop(); + const elTop = this._getElementOffsetTop($el[0]) - scrollTop; + + const visible = this.windowsHeight > (elTop + elOffset) && 0 < (elTop + elHeight - elOffset); + if (visible && state === "paused") { + $el.addClass("o_visible"); + this._startAnimation($el); + } else if (!visible && $el.hasClass("o_animate_both_scroll") && state === "running") { + $el.removeClass("o_visible"); + this._resetAnimation($el); + } + }); + }, +}); + return { 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 cf3e6696204..15193dd8c77 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -3037,6 +3037,45 @@ options.registry.ConditionalVisibility = options.Class.extend({ }, }); +options.registry.WebsiteAnimate = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @override + */ + async selectClass(previewMode, widgetValue, params) { + await this._super(...arguments); + if (params.isAnimationTypeSelection) { + this._forceAnimation(); + this.$target.toggleClass('o_animate_preview o_animate', !!widgetValue); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _forceAnimation() { + const $scrollingElement = $().getScrollingElement(); + this.$target.css('animation-name', 'dummy'); + // Trigger a DOM reflow. + void this.$target[0].offsetWidth; + this.$target.addClass('o_animating'); + $scrollingElement[0].classList.add('o_wanim_overflow_x_hidden'); + this.$target.css('animation-name', ''); + this.$target.one('webkitAnimationEnd oanimationend msAnimationEnd animationend', () => { + $scrollingElement[0].classList.remove('o_wanim_overflow_x_hidden'); + this.$target.removeClass('o_animating'); + }); + }, +}); + return { UrlPickerUserValueWidget: UrlPickerUserValueWidget, FontFamilyPickerUserValueWidget: FontFamilyPickerUserValueWidget, diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index e23ce3f0ef0..ebb647b4a9b 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -1429,3 +1429,472 @@ ul.o_checklist > li.o_checked::after { // hide them if a modal is open. display: none !important; } + +//------------------------------------------------------------------------------ +// Website Animate +//------------------------------------------------------------------------------ + +.o_animate { + animation-duration: 1s; + animation-fill-mode: both; + transform: translate3d(0,0,0); // force GPU acceleration + backface-visibility: hidden; // avoid flickering + text-rendering: geometricPrecision; // take care of animated titles + visibility: hidden; + + &:not(.o_animating) { + transform: none !important; + } +} +.o_animate_preview { + visibility: visible; +} +.o_wanim_overflow_x_hidden { + overflow-x: hidden !important; +} + +@keyframes o_anim_bounce_in { + 0%, 20%, 40%, 60%, 80%, 100% { + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + transform: scale(0.3); + } + 20% { + transform: scale(1.1); + } + 40% { + transform: scale(0.9); + } + 60% { + opacity: 1; + transform: scale(1.03); + } + 80% { + transform: scale(0.97); + } + 100% { + opacity: 1; + transform: scale(1); + } +} +.o_anim_bounce_in { + animation-name: o_anim_bounce_in; +} + +@keyframes o_anim_bounce_in_down { + 0%, 20%, 40%, 60%, 80%, 100% { + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + transform: translate(0, -3000px); + } + 60% { + opacity: 1; + transform: translate(0, 25px); + } + 75% { + transform: translate(0, -10px); + } + 90% { + transform: translate(0, 5px); + } + 100% { + transform: translate(0, 0); + } +} +.o_anim_bounce_in_down { + animation-name: o_anim_bounce_in_down; +} + +@keyframes o_anim_bounce_in_left { + 0%, 20%, 40%, 60%, 80%, 100% { + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + transform: translate(-3000px, 0); + } + 60% { + opacity: 1; + transform: translate(25px, 0); + } + 75% { + transform: translate(-10px, 0); + } + 90% { + transform: translate(5px, 0); + } + 100% { + transform: translate(0, 0); + } +} +.o_anim_bounce_in_left { + animation-name: o_anim_bounce_in_left; +} + +@keyframes o_anim_bounce_in_right { + 0%, 20%, 40%, 60%, 80%, 100% { + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + transform: translate(3000px, 0); + } + 60% { + opacity: 1; + transform: translate(-25px, 0); + } + 75% { + transform: translate(10px, 0); + } + 90% { + transform: translate(-5px, 0); + } + 100% { + transform: translate(0, 0); + } +} +.o_anim_bounce_in_right { + animation-name: o_anim_bounce_in_right; +} + +@keyframes o_anim_fade_in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +.o_anim_fade_in { + animation-name: o_anim_fade_in; +} + +@keyframes o_anim_fade_in_down { + 0% { + opacity: 0; + transform: translate(0, -100%); + } + 100% { + opacity: 1; + transform: translate(0, 0); + } +} +.o_anim_fade_in_down { + animation-name: o_anim_fade_in_down; +} + +@keyframes o_anim_fade_in_left { + 0% { + opacity: 0; + transform: translate(-100%, 0); + } + 100% { + opacity: 1; + transform: translate(0, 0); + } +} +.o_anim_fade_in_left { + animation-name: o_anim_fade_in_left; +} + +@keyframes o_anim_fade_in_right { + 0% { + opacity: 0; + transform: translate(100%, 0); + } + 100% { + opacity: 1; + transform: translate(0, 0); + } +} +.o_anim_fade_in_right { + animation-name: o_anim_fade_in_right; +} + +@keyframes o_anim_fade_in_up { + 0% { + opacity: 0; + transform: translate(0, 100%); + } + 100% { + opacity: 1; + transform: translate(0, 0); + } +} +.o_anim_fade_in_up { + animation-name: o_anim_fade_in_up; +} + +@keyframes o_anim_fade_out { + 0% { opacity: 1; } + 100% { opacity: 0; } +} +.o_anim_fade_out { + animation-name: o_anim_fade_out; +} + +@keyframes o_anim_rotate_in { + 0% { + opacity: 0; + transform: rotate(-200deg); + } + 100% { + opacity: 1; + transform: rotate(0); + } +} +.o_anim_rotate_in { + animation-name: o_anim_rotate_in; +} + +@keyframes o_anim_rotate_in_down_left { + 0% { + opacity: 0; + transform-origin: left bottom; + transform: rotate(-45deg); + } + 100% { + opacity: 1; + transform-origin: left bottom; + transform: rotate(0); + } +} +.o_anim_rotate_in_down_left { + animation-name: o_anim_rotate_in_down_left; +} + +@keyframes o_anim_rotate_in_down_right { + 0% { + opacity: 0; + transform-origin: right bottom; + transform: rotate(45deg); + } + 100% { + opacity: 1; + transform-origin: right bottom; + transform: rotate(0); + } +} +.o_anim_rotate_in_down_right { + animation-name: o_anim_rotate_in_down_right; +} + +@keyframes o_anim_zoom_in { + 0% { + opacity: 0; + transform: scale(0.3); + } + 50% { + opacity: 1; + } +} +.o_anim_zoom_in { + animation-name: o_anim_zoom_in; +} + +@keyframes o_anim_zoom_in_down { + 0% { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.o_anim_zoom_in_down { + animation-name: o_anim_zoom_in_down; +} + +@keyframes o_anim_zoom_in_left { + 0% { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.o_anim_zoom_in_left { + animation-name: o_anim_zoom_in_left; +} + +@keyframes o_anim_zoom_in_right { + 0% { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.o_anim_zoom_in_right { + animation-name: o_anim_zoom_in_right; +} + +@keyframes o_anim_flash { + 0%, 50%, 100% { + opacity: 1; + } + 25%, 75% { + opacity: 0; + } +} +.o_anim_flash { + animation-name: o_anim_flash; +} + +@keyframes o_anim_pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} +.o_anim_pulse { + animation-name: o_anim_pulse; +} + +@keyframes o_anim_shake { + 0%, 100% { + transform: translate(0, 0); + } + 10%, 30%, 50%, 70%, 90% { + transform: translate(-10px, 0); + } + 20%, 40%, 60%, 80% { + transform: translate(10px, 0); + } +} +.o_anim_shake { + animation-name: o_anim_shake; +} + +@keyframes o_anim_tada { + 0% { + transform: scale3d(1, 1, 1); + } + 10%, 20% { + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + 30%, 50%, 70%, 90% { + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + 40%, 60%, 80% { + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + 100% { + transform: scale3d(1, 1, 1); + } +} +.o_anim_tada { + animation-name: o_anim_tada; +} + +@keyframes o_anim_flip_in_x { + 0% { + perspective: 400px; + transform: rotateX(90deg); + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + perspective: 400px; + transform: rotateX(-20deg); + transition-timing-function: ease-in; + } + 60% { + perspective: 400px; + transform: rotateX(10deg); + opacity: 1; + } + 80% { + perspective: 400px; + transform: rotateX(-5deg); + } + 100% { + perspective: 400px; + } +} +.o_anim_flip_in_x { + backface-visibility: visible; + animation-name: o_anim_flip_in_x; +} + +@keyframes o_anim_flip_in_y { + 0% { + perspective: 400px; + transform: rotateY(90deg); + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + perspective: 400px; + transform: rotateY(-20deg); + transition-timing-function: ease-in; + } + 60% { + perspective: 400px; + transform: rotateY(10deg); + opacity: 1; + } + 80% { + perspective: 400px; + transform: rotateY(-5deg); + } + 100% { + perspective: 400px; + } +} +.o_anim_flip_in_y { + backface-visibility: visible; + animation-name: o_anim_flip_in_y; +} + +// Compatibility <= 13.0 +.o_anim_dur500 { + animation-duration: 500ms; +} +.o_anim_dur1500 { + animation-duration: 1500ms; +} +.o_anim_dur2000 { + animation-duration: 2000ms; +} +.o_anim_dur2500 { + animation-duration: 2500ms; +} +.o_anim_dur3000 { + animation-duration: 3000ms; +} +.o_anim_del500 { + animation-delay: 500ms; +} +.o_anim_del1000 { + animation-delay: 1000ms; +} +.o_anim_del1500 { + animation-delay: 1500ms; +} +.o_anim_del2000 { + animation-delay: 2000ms; +} +.o_anim_del2500 { + animation-delay: 2500ms; +} diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml index 12d266440cc..a9cf02988a9 100644 --- a/addons/website/views/snippets/snippets.xml +++ b/addons/website/views/snippets/snippets.xml @@ -1148,6 +1148,51 @@ + +