From 7ed3f3151a5352958f929ac8a4f10ea181be5ab1 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Tue, 1 Jun 2021 13:53:24 +0000 Subject: [PATCH] [IMP] mass_mailing, web, web_editor: remove scrollbar on iframe contents Mass mailing form view currently has a scrollbar for the contents of the editor, one for the form view itself, and one for the sidebar. In consequence we can sometimes end up with three scrollbars side by side, which is ugly and confusing. This ensures the height of the iframe is always that of its contents so there is no need for a scrollbar. When inserting a new snippet we want to autoscroll to that snippet. That is a complicated situation because the element we want to scroll is not in the same document as the element we want to scroll _to_. To address that situation, a new option is added to `scrollTo` so we can pass it the element to scroll. It can then check if we are in the aforementioned situation, in which case it can correct the scrolling and apply it to the right element. We use that option in mass_mailing to prevent the bug. Task-2545445 closes #71511 --- .../mailing_mailing_view_form_full_width.js | 141 ++++++++++++++++++ .../static/src/js/mass_mailing_widget.js | 31 +++- addons/web/static/src/legacy/js/core/dom.js | 13 +- .../static/src/js/backend/field_html.js | 3 +- .../static/src/js/editor/snippets.editor.js | 35 +++-- .../static/src/js/wysiwyg/wysiwyg_iframe.js | 2 +- .../static/src/scss/wysiwyg_iframe.scss | 1 - addons/web_editor/static/src/xml/wysiwyg.xml | 3 +- 8 files changed, 208 insertions(+), 21 deletions(-) diff --git a/addons/mass_mailing/static/src/js/mailing_mailing_view_form_full_width.js b/addons/mass_mailing/static/src/js/mailing_mailing_view_form_full_width.js index f03dce94cdb..4131c41a15d 100644 --- a/addons/mass_mailing/static/src/js/mailing_mailing_view_form_full_width.js +++ b/addons/mass_mailing/static/src/js/mailing_mailing_view_form_full_width.js @@ -1,10 +1,150 @@ /** @odoo-module **/ import FormView from 'web.FormView'; +import FormController from 'web.FormController'; import FormRenderer from 'web.FormRenderer'; +import { bus, _t } from 'web.core'; import viewRegistry from 'web.view_registry'; import config from 'web.config'; +const MassMailingFullWidthFormController = FormController.extend({ + custom_events: _.extend({}, FormController.prototype.custom_events,{ + iframe_updated: '_onIframeUpdated', + }), + + /** + * @override + */ + init() { + this._super(...arguments); + this._boundOnDomUpdated = this._onDomUpdated.bind(this); + bus.on('DOM_updated', this, this._onDomUpdated); + this._resizeObserver = new ResizeObserver(this._onResizeIframeContents.bind(this)); + }, + /** + * @override + */ + destroy() { + bus.off('DOM_updated', this, this._boundOnDomUpdated); + this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Resize the mailing editor's iframe container so its height fits its + * contents. This needs to be called whenever the iframe's contents might + * have changed, eg. when adding/removing content to/from it or when a + * template is picked. + * + * @private + * @param {JQuery} $iframe + */ + _resizeMailingEditorIframe() { + const VERTICAL_OFFSET = 12; // Vertical offset picked for visual design purposes. + const minHeight = $(window).height() - Math.abs(this.$iframe.offset().top) - (VERTICAL_OFFSET / 2); + const $iframeDoc = this.$iframe.contents(); + const $themeSelectorNew = $iframeDoc.find('.o_mail_theme_selector_new'); + if ($themeSelectorNew.length) { + this.$iframe.height(Math.max($themeSelectorNew[0].scrollHeight + VERTICAL_OFFSET, minHeight)); + } else { + const ref = $iframeDoc.find('#iframe_target')[0]; + if (ref) { + this.$iframe.height(Math.max(ref.scrollHeight + VERTICAL_OFFSET, minHeight)); + } + } + }, + /** + * Return true if the mailing editor is in full screen mode, false + * otherwise. + * + * @private + * @returns {boolean} + */ + _isFullScreen() { + return window.top.document.body.classList.contains('o_field_widgetTextHtml_fullscreen'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Assume the iframe was updated on each dom_updated event. + * + * @private + */ + _onDomUpdated() { + const data = { $iframe: this.$('iframe.wysiwyg_iframe, iframe.o_readonly') }; + this._onIframeUpdated({ data }); + }, + /** + * Resize the given iframe so its height fits its contents and initialize a + * resize observer to resize on each size change in its contents. + * + * @private + * @param {JQuery} ev.data.$iframe + */ + _onIframeUpdated(ev) { + const $iframe = ev.data.$iframe; + if (!$iframe.length || !$iframe.contents().length) { + return; + } + const hasIframeChanged = $iframe !== this.$iframe; + this.$iframe = $iframe; + this._resizeMailingEditorIframe(); + + const $iframeDoc = $iframe.contents(); + const iframeTarget = $iframeDoc.find('#iframe_target'); + if (hasIframeChanged) { + $iframeDoc.find('body').on('click', '.o_fullscreen_btn', this._onToggleFullscreen.bind(this)); + if (iframeTarget[0]) { + this._resizeObserver.disconnect(); + this._resizeObserver.observe(iframeTarget[0]); + } + } + if (iframeTarget[0]) { + const isFullscreen = this._isFullScreen(); + iframeTarget.css({ + display: isFullscreen ? '' : 'flex', + 'flex-direction': isFullscreen ? '' : 'column', + }); + } + }, + /** + * Switch "scrolling modes" on toggle fullscreen mode: in fullscreen mode, + * the scroll happens within the iframe whereas in regular mode we pretend + * there is no iframe and scroll in the top document. + * + * @private + */ + _onToggleFullscreen() { + const $iframeDoc = this.$iframe.contents(); + const iframeTarget = $iframeDoc.find('#iframe_target'); + const isFullscreen = this._isFullScreen(); + iframeTarget.css({ + display: isFullscreen ? '' : 'flex', + 'flex-direction': isFullscreen ? '' : 'column', + }); + const wysiwyg = $iframeDoc.find('.note-editable').data('wysiwyg'); + if (wysiwyg && wysiwyg.snippetsMenu) { + // Restore the appropriate scrollable depending on the mode. + this._$scrollable = this._$scrollable || wysiwyg.snippetsMenu.$scrollable; + wysiwyg.snippetsMenu.$scrollable = isFullscreen ? $iframeDoc.find('.note-editable') : this._$scrollable; + } + }, + /** + * Resize the iframe whenever the contents of the iframe change height. + * + * @private + */ + _onResizeIframeContents() { + this._resizeMailingEditorIframe(); + }, +}); + const MassMailingFullWidthFormRenderer = FormRenderer.extend({ /** * Overload the rendering of the header in order to add a child to it: move @@ -34,6 +174,7 @@ const MassMailingFullWidthFormRenderer = FormRenderer.extend({ export const MassMailingFullWidthFormView = FormView.extend({ config: Object.assign({}, FormView.prototype.config, { + Controller: MassMailingFullWidthFormController, Renderer: MassMailingFullWidthFormRenderer, }), }); diff --git a/addons/mass_mailing/static/src/js/mass_mailing_widget.js b/addons/mass_mailing/static/src/js/mass_mailing_widget.js index 25433713643..2218c68ab12 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_widget.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_widget.js @@ -299,6 +299,20 @@ var MassMailingFieldHtml = FieldHtml.extend({ } } this.wysiwyg.trigger('reload_snippet_dropzones'); + this.trigger_up('iframe_updated', { $iframe: this.wysiwyg.$iframe }); + }, + + /** + * @private + * @override + */ + _toggleCodeView: function ($codeview) { + this._super(...arguments); + const isFullWidth = !!$(window.top.document).find('.o_mass_mailing_form_full_width')[0]; + $codeview.css('height', isFullWidth ? $(window).height() : ''); + if ($codeview.hasClass('d-none')) { + this.trigger_up('iframe_updated', { $iframe: this.wysiwyg.$iframe }); + } }, /** @@ -330,12 +344,8 @@ var MassMailingFieldHtml = FieldHtml.extend({ this._super(); this.wysiwyg.odooEditor.observerFlush(); this.wysiwyg.odooEditor.resetHistory(); - //this will make the editor take all the bottom space - this.wysiwyg.$iframe.css({ - 'min-height': '100vh', - width: '100%' - }); this.wysiwyg.$iframeBody.addClass('o_mass_mailing_iframe'); + this.trigger_up('iframe_updated', { $iframe: this.wysiwyg.$iframe }); }, /** * @private @@ -353,6 +363,15 @@ var MassMailingFieldHtml = FieldHtml.extend({ */ _onSnippetsLoaded: function (ev) { var self = this; + if (this.wysiwyg.snippetsMenu && $(window.top.document).find('.o_mass_mailing_form_full_width')[0]) { + // In full width form mode, ensure the snippets menu's scrollable is + // in the form view, not in the iframe. + this.wysiwyg.snippetsMenu.$scrollable = this.$el.closestScrollable(); + // Ensure said scrollable keeps its scrollbar at all times to + // prevent the scrollbar from appearing at awkward moments (ie: when + // previewing an option) + this.wysiwyg.snippetsMenu.$scrollable.css('overflow-y', 'scroll'); + } if (!this.$content) { this.snippetsLoaded = ev; return; @@ -373,7 +392,7 @@ var MassMailingFieldHtml = FieldHtml.extend({ if (!odoo.debug) { $snippetsSideBar.find('.o_codeview_btn').hide(); } - const $codeview = this.$content.parent().find('textarea.o_codeview'); + const $codeview = this.wysiwyg.$iframe.contents().find('textarea.o_codeview'); $snippetsSideBar.on('click', '.o_codeview_btn', () => this._toggleCodeView($codeview)); if ($themes.length === 0) { diff --git a/addons/web/static/src/legacy/js/core/dom.js b/addons/web/static/src/legacy/js/core/dom.js index 249a56eefea..063ee3742b8 100644 --- a/addons/web/static/src/legacy/js/core/dom.js +++ b/addons/web/static/src/legacy/js/core/dom.js @@ -545,11 +545,17 @@ var dom = { * @param {number} [options.forcedOffset] * offset used instead of the automatic one (extraOffset will be * ignored too) + * @param {JQuery} [options.$scrollable] the $element to scroll * @return {Promise} */ scrollTo(el, options = {}) { const $el = $(el); - const $scrollable = $el.parent().closestScrollable(); + const $scrollable = options.$scrollable || $el.parent().closestScrollable(); + // If $scrollable and $el are not in the same document, we can safely + // assume $el is in an $iframe. We retrieve it by filtering the list of + // iframes in $scrollable to keep only the one that contains $el. + const isInOneDocument = $scrollable[0].ownerDocument === $el[0].ownerDocument; + const $iframe = !isInOneDocument && $scrollable.find('iframe').filter((i, node) => $(node).contents().has($el)); const $topLevelScrollable = $().getScrollingElement(); const isTopScroll = $scrollable.is($topLevelScrollable); @@ -560,7 +566,10 @@ var dom = { offsetTop = $el.offset().top; el.classList.add('d-none'); } - const elPosition = $scrollable[0].scrollTop + (offsetTop - $scrollable.offset().top); + let elPosition = $scrollable[0].scrollTop + (offsetTop - $scrollable.offset().top); + if (!isInOneDocument && $iframe.length) { + elPosition += $iframe.offset().top; + } let offset = options.forcedOffset; if (offset === undefined) { offset = (isTopScroll ? dom.scrollFixedOffset() : 0) + (options.extraOffset || 0); diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js index be431141baa..cb4be5c314d 100644 --- a/addons/web_editor/static/src/js/backend/field_html.js +++ b/addons/web_editor/static/src/js/backend/field_html.js @@ -209,7 +209,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { */ _toggleCodeView: function ($codeview, $codeviewButton) { this.wysiwyg.odooEditor.observerUnactive(); - $codeview.height(this.$content.height()) + $codeview.height(this.$content.height()); $codeview.toggleClass('d-none'); this.$content.toggleClass('d-none'); if ($codeview.hasClass('d-none')) { @@ -309,6 +309,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { } self.$content = $('#iframe_target', self.$iframe[0].contentWindow.document.body); resolver(); + self.trigger_up('iframe_updated', { $iframe: self.$iframe }); }; this.$iframe.on('load', function onLoad() { diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 2bf6f3ee522..113b0d6087b 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -161,6 +161,9 @@ var SnippetEditor = Widget.extend({ this.isTargetParentEditable = false; this.isTargetMovable = false; this.$scrollingElement = $().getScrollingElement(this.ownerDocument); + if (!this.$scrollingElement[0]) { + this.$scrollingElement = $(this.ownerDocument).find('.o_editable'); + } this.displayOverlayOptions = false; this.__isStarted = new Promise(resolve => { @@ -209,7 +212,10 @@ var SnippetEditor = Widget.extend({ }, }, }); - this.draggableComponent = new SmoothScrollOnDrag(this, this.$el, $().getScrollingElement(this.ownerDocument), smoothScrollOptions); + const $scrollable = (this.options.wysiwyg.snippetsMenu && this.options.wysiwyg.snippetsMenu.$scrollable) + || (this.$scrollingElement.length && this.$scrollingElement) + || $().getScrollingElement(this.ownerDocument); + this.draggableComponent = new SmoothScrollOnDrag(this, this.$el, $scrollable, smoothScrollOptions); } else { this.$('.o_overlay_move_options').addClass('d-none'); $customize.find('.oe_snippet_clone').addClass('d-none'); @@ -329,8 +335,8 @@ var SnippetEditor = Widget.extend({ // be outside of the viewport (the whole header during an effect for // example). const rect = targetEl.getBoundingClientRect(); - const vpWidth = window.innerWidth || document.documentElement.clientWidth; - const vpHeight = window.innerHeight || document.documentElement.clientHeight; + const vpWidth = targetEl.ownerDocument.defaultView.innerWidth || document.documentElement.clientWidth; + const vpHeight = targetEl.ownerDocument.defaultView.innerHeight || document.documentElement.clientHeight; const isInViewport = ( rect.bottom > -0.1 && rect.right > -0.1 && @@ -831,7 +837,9 @@ var SnippetEditor = Widget.extend({ // If a modal is open, the scroll target must be that modal const $openModal = self.$editable.find('.modal:visible'); - self.draggableComponent.$scrollTarget = $openModal.length ? $openModal : self.$scrollingElement; + if ($openModal.length) { + self.draggableComponent.$scrollTarget = $openModal; + } // Trigger a scroll on the draggable element so that jQuery updates // the position of the drop zones. @@ -1294,6 +1302,9 @@ var SnippetsMenu = Widget.extend({ // Hide the active overlay when scrolling. // Show it again and recompute all the overlays after the scroll. this.$scrollingElement = $().getScrollingElement(this.ownerDocument); + if (!this.$scrollingElement[0]) { + this.$scrollingElement = $(this.ownerDocument).find('.o_editable'); + } this._onScrollingElementScroll = _.throttle(() => { for (const editor of this.snippetEditors) { editor.toggleOverlayVisibility(false); @@ -2229,7 +2240,10 @@ var SnippetsMenu = Widget.extend({ var $toInsert, dropped, $snippet; let dragAndDropResolve; - const $scrollingElement = $().getScrollingElement(this.ownerDocument); + let $scrollingElement = $().getScrollingElement(this.ownerDocument); + if (!$scrollingElement[0]) { + $scrollingElement = $(this.ownerDocument).find('.o_editable'); + } const smoothScrollOptions = this._getScrollOptions({ jQueryDraggableOptions: { @@ -2306,7 +2320,9 @@ var SnippetsMenu = Widget.extend({ // If a modal is open, the scroll target must be that modal const $openModal = self.getEditableArea().find('.modal:visible'); - self.draggableComponent.$scrollTarget = $openModal.length ? $openModal : $scrollingElement; + if ($openModal.length) { + self.draggableComponent.$scrollTarget = $openModal; + } // Trigger a scroll on the draggable element so that jQuery updates // the position of the drop zones. @@ -2361,7 +2377,7 @@ var SnippetsMenu = Widget.extend({ } var $target = $toInsert; - await self._scrollToSnippet($target); + await self._scrollToSnippet($target, self.$scrollable); _.defer(async function () { self._disableUndroppableSnippets(); @@ -2446,10 +2462,11 @@ var SnippetsMenu = Widget.extend({ * * @private * @param {jQuery} $el - snippet to scroll to + * @param {jQuery} [$scrollable] - $element to scroll * @return {Promise} */ - async _scrollToSnippet($el) { - return dom.scrollTo($el[0], {extraOffset: 50}); + async _scrollToSnippet($el, $scrollable) { + return dom.scrollTo($el[0], {extraOffset: 50, $scrollable: $scrollable}); }, /** * @private diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js index 84c849428e0..e67ab0b1bfd 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js @@ -123,9 +123,9 @@ Wysiwyg.include({ const $codeview = $('