[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
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -123,9 +123,9 @@ Wysiwyg.include({
|
||||
const $codeview = $('<textarea class="o_codeview d-none"/>');
|
||||
self.$editable.addClass('o_editable oe_structure');
|
||||
|
||||
$iframeTarget.append($codeview);
|
||||
$iframeTarget.append($iframeWrapper);
|
||||
$iframeTarget.append($utilsZone);
|
||||
$iframeWrapper.append($codeview);
|
||||
$iframeWrapper.append(self.$editable);
|
||||
|
||||
self.options.toolbarHandler = $('#web_editor-top-edit', self.$iframe[0].contentWindow.document);
|
||||
|
||||
@@ -15,7 +15,6 @@ iframe.wysiwyg_iframe.o_fullscreen {
|
||||
|
||||
body.o_in_iframe {
|
||||
background-color: $o-view-background-color;
|
||||
display: flex;
|
||||
|
||||
.o_editable {
|
||||
position: relative;
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body id="iframe_target" class="o_in_iframe">
|
||||
<body class="o_in_iframe">
|
||||
<div id="iframe_target"/>
|
||||
<script type="text/javascript">
|
||||
odoo.define('web_editor.wysiwyg.iniframe', function (require) {
|
||||
'use strict';
|
||||
|
||||
Reference in New Issue
Block a user