diff --git a/addons/web_editor/static/src/scss/web_editor.common.scss b/addons/web_editor/static/src/scss/web_editor.common.scss index 681077a976a..7a16047d4fb 100644 --- a/addons/web_editor/static/src/scss/web_editor.common.scss +++ b/addons/web_editor/static/src/scss/web_editor.common.scss @@ -805,4 +805,14 @@ section, .oe_img_bg, [data-oe-shape-data] { .text-gradient { -webkit-background-clip: text; -webkit-text-fill-color: transparent; + + // FIXME (or wait for a fix in Chrome): the code below is needed to make + // animated text work with gradient background on Chrome. But the side + // effect is that text node wrapping by a span (e.g. bold, italic) no longer + // have the "gradient crossing all the text" on it but its own gradient. + * { + background-image: inherit; + -webkit-background-clip: inherit; + -webkit-text-fill-color: inherit; + } } diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss index 459fad65679..3f1d03f1806 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss @@ -487,7 +487,7 @@ body.editor_enable.editor_has_snippets { @extend %we-generic-button; } - #removeFormat, #oe-table-delete-table { + #removeFormat, #oe-table-delete-table, .o_we_highlight_animated_text { @extend %we-generic-link; } @@ -496,9 +496,9 @@ body.editor_enable.editor_has_snippets { background: transparent; margin-top: $o-we-sidebar-content-field-spacing; padding: 0 $o-we-sidebar-content-padding-base 0 $o-we-sidebar-content-indent; - grid-template-areas: "typo typo style colors" "size align list link" "options options options options"; + grid-template-areas: "typo typo style colors" "size align list link" "options options options options" "options2 options2 options2 options2"; grid-template-columns: 1fr 1fr 1fr 1fr; - grid-template-rows: minmax($o-we-sidebar-content-field-height, auto) minmax($o-we-sidebar-content-field-height, auto) auto ; + grid-template-rows: minmax($o-we-sidebar-content-field-height, auto) minmax($o-we-sidebar-content-field-height, auto) auto auto ; row-gap: $o-we-sidebar-content-field-spacing; column-gap: $o-we-sidebar-content-field-multi-spacing; width: 100%; @@ -565,9 +565,14 @@ body.editor_enable.editor_has_snippets { input::placeholder { font-style: italic; } + we-input:not(.o_we_small_input) { + &, div, input { + width: 100% !important; + } + } - we-input, we-input div, we-input input { - width: 100% !important; + & + we-customizeblock-option { + grid-area: options2; } } diff --git a/addons/website/static/src/js/editor/snippets.editor.js b/addons/website/static/src/js/editor/snippets.editor.js index 71c871872ce..67aaff9f195 100644 --- a/addons/website/static/src/js/editor/snippets.editor.js +++ b/addons/website/static/src/js/editor/snippets.editor.js @@ -5,6 +5,9 @@ const {qweb, _t, _lt} = require('web.core'); const Dialog = require('web.Dialog'); const weSnippetEditor = require('web_editor.snippet.editor'); const wSnippetOptions = require('website.editor.snippets.options'); +const OdooEditorLib = require('@web_editor/../lib/odoo-editor/src/utils/utils'); +const getDeepRange = OdooEditorLib.getDeepRange; +const getTraversedNodes = OdooEditorLib.getTraversedNodes; const FontFamilyPickerUserValueWidget = wSnippetOptions.FontFamilyPickerUserValueWidget; @@ -13,6 +16,8 @@ weSnippetEditor.SnippetsMenu.include({ .concat(['/website/static/src/xml/website.editor.xml']), events: _.extend({}, weSnippetEditor.SnippetsMenu.prototype.events, { 'click .o_we_customize_theme_btn': '_onThemeTabClick', + 'click .o_we_animate_text': '_onAnimateTextClick', + 'click .o_we_highlight_animated_text': '_onHighlightAnimatedTextClick', }), custom_events: Object.assign({}, weSnippetEditor.SnippetsMenu.prototype.custom_events, { 'gmap_api_request': '_onGMapAPIRequest', @@ -27,6 +32,27 @@ weSnippetEditor.SnippetsMenu.include({ ['website-settings', _lt("Website Settings")], ], + /** + * @override + */ + start() { + this._super(...arguments); + this.$currentAnimatedText = $(); + + this.__onSelectionChange = ev => { + this._toggleAnimatedTextButton(); + }; + this.ownerDocument.addEventListener('selectionchange', this.__onSelectionChange); + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + this.ownerDocument.removeEventListener('selectionchange', this.__onSelectionChange); + document.body.classList.remove('o_animated_text_highlighted'); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -158,6 +184,63 @@ weSnippetEditor.SnippetsMenu.include({ this._super(...arguments); this.$('.o_we_customize_theme_btn').toggleClass('active', tab === this.tabs.THEME); }, + /** + * Returns the animated text element wrapping the selection if it exists. + * + * @private + * @return {Element|false} + */ + _getAnimatedTextElement() { + const editable = this.options.wysiwyg.$editable[0]; + const animatedTextNode = getTraversedNodes(editable).find(n => n.parentElement.closest(".o_animated_text")); + return animatedTextNode ? animatedTextNode.parentElement.closest('.o_animated_text') : false; + }, + /** + * @override + */ + _addToolbar() { + this._super(...arguments); + this.$('#o_we_editor_toolbar_container > we-title > span').after($(` +
+ `)); + this._toggleAnimatedTextButton(); + this._toggleHighlightAnimatedTextButton(); + }, + /** + * Activates the button to animate text if the selection is in an + * animated text element or deactivates the button if not. + * + * @private + */ + _toggleAnimatedTextButton() { + if (!this._isValidSelection(window.getSelection())) { + return; + } + const animatedText = this._getAnimatedTextElement(); + this.$('.o_we_animate_text').toggleClass('active', !!animatedText); + this.$currentAnimatedText = animatedText ? $(animatedText) : $(); + }, + /** + * Displays the button that allows to highlight the animated text if there + * is animated text in the page. + * + * @private + */ + _toggleHighlightAnimatedTextButton() { + const $animatedText = this.getEditableArea().find('.o_animated_text'); + this.$('#o_we_editor_toolbar_container .o_we_highlight_animated_text').toggleClass('d-none', !$animatedText.length); + }, + /** + * @private + * @param {Node} node + * @return {Boolean} + */ + _isValidSelection(sel) { + return sel.rangeCount && [...this.getEditableArea()].some(el => el.contains(sel.anchorNode)); + }, //-------------------------------------------------------------------------- // Handlers @@ -233,6 +316,64 @@ weSnippetEditor.SnippetsMenu.include({ throw e; } }, + /** + * @private + */ + _onAnimateTextClick(ev) { + const sel = window.getSelection(); + if (!this._isValidSelection(sel)) { + return; + } + const editable = this.options.wysiwyg.$editable[0]; + const range = getDeepRange(editable, { splitText: true, select: true, correctTripleClick: true }); + if (this.$currentAnimatedText.length) { + this.$currentAnimatedText.contents().unwrap(); + this.options.wysiwyg.odooEditor.historyResetLatestComputedSelection(); + this._toggleHighlightAnimatedTextButton(); + ev.target.classList.remove('active'); + this.options.wysiwyg.odooEditor.historyStep(); + } else { + if (sel.getRangeAt(0).collapsed) { + return; + } + const animatedTextEl = document.createElement('span'); + animatedTextEl.classList.add('o_animated_text', 'o_animate', 'o_animate_preview', 'o_anim_fade_in'); + let $snippet = null; + try { + range.surroundContents(animatedTextEl); + $snippet = $(animatedTextEl); + } catch (e) { + // This try catch is needed because 'surroundContents' may + // fail when the range has partially selected a non-Text node. + if (range.commonAncestorContainer.textContent === range.toString()) { + const $commonAncestor = $(range.commonAncestorContainer); + $commonAncestor.wrapInner(animatedTextEl); + $snippet = $commonAncestor.find('.o_animated_text'); + } + } + if ($snippet) { + $snippet[0].normalize(); + this.trigger_up('activate_snippet', { + $snippet: $snippet, + previewMode: false, + }); + this.options.wysiwyg.odooEditor.historyStep(); + } else { + this.displayNotification({ + message: _t("The current text selection cannot be animated. Try clearing the format and try again."), + type: 'danger', + sticky: true, + }); + } + } + }, + /** + * @private + */ + _onHighlightAnimatedTextClick(ev) { + $('body').toggleClass('o_animated_text_highlighted'); + $(ev.target).toggleClass('fa-eye fa-eye-slash').toggleClass('text-success'); + }, }); weSnippetEditor.SnippetEditor.include({ diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 4b3851a093d..94b650d9a94 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -3171,6 +3171,36 @@ options.registry.ConditionalVisibility = options.Class.extend({ }); options.registry.WebsiteAnimate = options.Class.extend({ + /** + * @override + */ + async start() { + await this._super(...arguments); + this.isAnimatedText = this.$target.hasClass('o_animated_text'); + this.$optionsSection = this.$overlay.data('$optionsSection'); + }, + /** + * @override + */ + onFocus() { + if (this.isAnimatedText) { + // For animated text, the animation options must be in the editor + // toolbar. + const $toolbar = this.options.wysiwyg.toolbar.$el; + $toolbar.append(this.$el); + this.$optionsSection.addClass('d-none'); + } + }, + /** + * @override + */ + onBlur() { + if (this.isAnimatedText) { + // For animated text, the options must be returned to their + // original location as they were moved in the toolbar. + this.$optionsSection.append(this.$el); + } + }, //-------------------------------------------------------------------------- // Options @@ -3216,6 +3246,15 @@ options.registry.WebsiteAnimate = options.Class.extend({ this.$target.removeClass('o_animating'); }); }, + /** + * @override + */ + _computeWidgetVisibility(widgetName, params) { + if (widgetName === 'no_animation_opt') { + return !this.isAnimatedText; + } + return this._super(...arguments); + }, }); /** diff --git a/addons/website/static/src/scss/website.edit_mode.scss b/addons/website/static/src/scss/website.edit_mode.scss index eb52198612e..fd01858f398 100644 --- a/addons/website/static/src/scss/website.edit_mode.scss +++ b/addons/website/static/src/scss/website.edit_mode.scss @@ -226,3 +226,21 @@ body.editor_enable { } } } + +// Website Animate +.editor_enable.o_animated_text_highlighted { + .o_animated_text { + position: relative; + + &:after { + content: ""; + pointer-events: none; + @include o-position-absolute(0, 0, 0, 0); + z-index: 1; + // This border is useful when there is a green background behind + // the text. + border: 1px dotted white; + background-color: rgba(greenyellow, .2); + } + } +} diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index 2d21949a5ed..12853c7020d 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -1497,6 +1497,9 @@ ul.o_checklist > li.o_checked::after { .o_wanim_overflow_x_hidden { overflow-x: hidden !important; } +.o_animated_text { + display: inline-block; +} @keyframes o_anim_bounce_in { 0%, 20%, 40%, 60%, 80%, 100% { diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml index ce8ec56a520..ae8fe9a8762 100644 --- a/addons/website/static/src/xml/website.editor.xml +++ b/addons/website/static/src/xml/website.editor.xml @@ -158,4 +158,12 @@
+ + + + + +
+ + diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml index d6c6423c9dd..3fbe29c8130 100644 --- a/addons/website/views/snippets/snippets.xml +++ b/addons/website/views/snippets/snippets.xml @@ -1210,7 +1210,7 @@
No Animation @@ -1248,9 +1248,9 @@ First Time Only Every Time - -