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 = $('