[FIX] web, website, web_editor: fix popup scrollbar
Steps to reproduce the bug: - Drag and drop several snippets onto a web page in edit mode to make the vertical scrollbar appear. - Drag and drop a popup onto the page. - Drag and drop snippets into the popup so that the height of the popup exceeds the height of the viewport. - Try to scroll the popup downwards by clicking and dragging on the scrollbar (not using the mouse wheel). - Bug: the page is scrolled instead of the popup. Note: This bug occurs in Chrome (not in Firefox). This issue occurs because two scrollbars are present at the same location (one for the page and one for the popup) and they overlap each other. Normally, Bootstrap removes the scrollbar from the body when a modal is opened, and this behavior was adapted for the #wrapwrap element with this commit [1]. However, when transitioning to Bootstrap 5, the code that overrides Bootstrap was removed instead of being adapted (this was done in this commit [2]). In later commits [3] and [4], we continued to remove code that updated the scrollbar based on the opening of a modal because this code was causing errors due to the fact that it was incomplete without the part removed by commit [2]. This commit restores the original behavior (before the deletions made by the aforementioned commits) by restoring the missing code and properly adapting it to Bootstrap 5. [1]: https://github.com/odoo/odoo/commit/9cf8b97fe40444b44ebb6e6fb992bc658a087d32 [2]: https://github.com/odoo/odoo/commit/0b94da214b7017e8580e671cbaa68ade6de2fbc7 [3]: https://github.com/odoo/odoo/commit/cb7cf77ed080e86f800af61c9a3dc4ad36ad6cfb [4]: https://github.com/odoo/odoo/commit/51939d09f84579f61f1ff77aacd754beba036dc2 task-3102275 closes odoo/odoo#126709 X-original-commit: 0e75f1c01ca4fcc3eb483a7256bcf72b2bca39c3 Signed-off-by: Benoit Socias (bso) <bso@odoo.com>
This commit is contained in:
@@ -152,6 +152,13 @@ var dom = {
|
||||
closestScrollable(el) {
|
||||
return $(el).closestScrollable()[0] || null;
|
||||
},
|
||||
/**
|
||||
* @param {HTMLElement} el
|
||||
* @see $.compensateScrollbar
|
||||
*/
|
||||
compensateScrollbar(el, ...rest) {
|
||||
$(el).compensateScrollbar(...rest);
|
||||
},
|
||||
/**
|
||||
* jQuery find function behavior is::
|
||||
*
|
||||
|
||||
@@ -158,3 +158,27 @@ ScrollSpy.prototype._activate = function (target) {
|
||||
Dropdown.prototype._detectNavbar = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
/* Bootstrap modal scrollbar compensation on non-body */
|
||||
const bsAdjustDialogFunction = Modal.prototype._adjustDialog;
|
||||
Modal.prototype._adjustDialog = function () {
|
||||
const document = this._element.ownerDocument;
|
||||
document.body.classList.remove('modal-open');
|
||||
const $scrollable = $().getScrollingElement(document);
|
||||
if (document.body.contains($scrollable[0])) {
|
||||
$scrollable.compensateScrollbar(true);
|
||||
}
|
||||
document.body.classList.add('modal-open');
|
||||
return bsAdjustDialogFunction.apply(this, arguments);
|
||||
};
|
||||
|
||||
const bsResetAdjustmentsFunction = Modal.prototype._resetAdjustments;
|
||||
Modal.prototype._resetAdjustments = function () {
|
||||
const document = this._element.ownerDocument;
|
||||
document.body.classList.remove('modal-open');
|
||||
const $scrollable = $().getScrollingElement(document);
|
||||
if (document.body.contains($scrollable[0])) {
|
||||
$scrollable.compensateScrollbar(false);
|
||||
}
|
||||
return bsResetAdjustmentsFunction.apply(this, arguments);
|
||||
};
|
||||
|
||||
+31
@@ -146,6 +146,37 @@ $.fn.extend({
|
||||
}
|
||||
return $el;
|
||||
},
|
||||
/**
|
||||
* Adapt the given css property by adding the size of a scrollbar if any.
|
||||
* Limitation: only works if the given css property is not already used as
|
||||
* inline style for another reason.
|
||||
*
|
||||
* @param {boolean} [add=true]
|
||||
* @param {boolean} [isScrollElement=true]
|
||||
* @param {string} [cssProperty='padding-right']
|
||||
*/
|
||||
compensateScrollbar(add = true, isScrollElement = true, cssProperty = 'padding-right') {
|
||||
for (const el of this) {
|
||||
// Compensate scrollbar
|
||||
const scrollableEl = isScrollElement ? el : $(el).parent().closestScrollable()[0];
|
||||
const isRTL = scrollableEl.matches(".o_rtl");
|
||||
if (isRTL) {
|
||||
cssProperty = cssProperty.replace("right", "left");
|
||||
}
|
||||
el.style.removeProperty(cssProperty);
|
||||
if (!add) {
|
||||
return;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
// Round up to the nearest integer to be as close as possible to
|
||||
// the correct value in case of browser zoom.
|
||||
const borderLeftWidth = Math.ceil(parseFloat(style.borderLeftWidth.replace('px', '')));
|
||||
const borderRightWidth = Math.ceil(parseFloat(style.borderRightWidth.replace('px', '')));
|
||||
const bordersWidth = borderLeftWidth + borderRightWidth;
|
||||
const newValue = parseInt(style[cssProperty]) + scrollableEl.offsetWidth - scrollableEl.clientWidth - bordersWidth;
|
||||
el.style.setProperty(cssProperty, `${newValue}px`, 'important');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
|
||||
@@ -24,3 +24,6 @@ html, body, #wrapwrap {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
.modal-open #wrapwrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -409,6 +409,12 @@ var SnippetEditor = Widget.extend({
|
||||
|
||||
const editableOffsetTop = this.$editable.offset().top - manipulatorOffset.top;
|
||||
this.$el.toggleClass('o_top_cover', offset.top - editableOffsetTop < 25);
|
||||
// If the element covered by the overlay has a scrollbar, we remove its
|
||||
// right border as it interferes with proper scrolling. (e.g. modal)
|
||||
const handleEReadonlyEl = this.$el[0].querySelector('.o_handle.e.readonly');
|
||||
if (handleEReadonlyEl) {
|
||||
handleEReadonlyEl.style.width = dom.hasScrollableContent(targetEl) ? 0 : '';
|
||||
}
|
||||
},
|
||||
/**
|
||||
* DOMElements have a default name which appears in the overlay when they
|
||||
|
||||
@@ -4,6 +4,7 @@ import config from "web.config";
|
||||
import publicWidget from "web.public.widget";
|
||||
import animations from "website.content.snippets.animation";
|
||||
const extraMenuUpdateCallbacks = [];
|
||||
import dom from "web.dom";
|
||||
|
||||
const BaseAnimatedHeader = animations.Animation.extend({
|
||||
disabledInEditableMode: false,
|
||||
@@ -78,17 +79,7 @@ const BaseAnimatedHeader = animations.Animation.extend({
|
||||
* @private
|
||||
*/
|
||||
_adaptFixedHeaderPosition() {
|
||||
// Compensate scrollbar
|
||||
this.el.style.removeProperty('right');
|
||||
if (this.fixedHeader) {
|
||||
const scrollableEl = $(this.el).parent().closestScrollable()[0];
|
||||
const style = window.getComputedStyle(this.el);
|
||||
const borderLeftWidth = parseInt(style.borderLeftWidth.replace('px', ''));
|
||||
const borderRightWidth = parseInt(style.borderRightWidth.replace('px', ''));
|
||||
const bordersWidth = borderLeftWidth + borderRightWidth;
|
||||
const newValue = parseInt(style['right']) + scrollableEl.offsetWidth - scrollableEl.clientWidth - bordersWidth;
|
||||
this.el.style.setProperty('right', `${newValue}px`, 'important');
|
||||
}
|
||||
dom.compensateScrollbar(this.el, this.fixedHeader, false, 'right');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import config from "web.config";
|
||||
import publicWidget from "web.public.widget";
|
||||
import {getCookie, setCookie} from "web.utils.cookies";
|
||||
import dom from "web.dom";
|
||||
import throttleForAnimation from "@web/core/utils/timing";
|
||||
|
||||
// TODO In master, export this class too or merge it with PopupWidget
|
||||
const SharedPopupWidget = publicWidget.Widget.extend({
|
||||
@@ -178,6 +180,109 @@ const PopupWidget = publicWidget.Widget.extend({
|
||||
|
||||
publicWidget.registry.popup = PopupWidget;
|
||||
|
||||
const noBackdropPopupWidget = publicWidget.Widget.extend({
|
||||
selector: '.s_popup_no_backdrop',
|
||||
disabledInEditableMode: false,
|
||||
events: {
|
||||
'shown.bs.modal': '_onModalNoBackdropShown',
|
||||
'hide.bs.modal': '_onModalNoBackdropHide',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start() {
|
||||
this.throttledUpdateScrollbar = throttleForAnimation(() => this._updateScrollbar());
|
||||
if (this.editableMode && this.el.classList.contains('show')) {
|
||||
// Use case: When the "Backdrop" option is disabled in edit mode.
|
||||
// The page scrollbar must be adjusted and events must be added.
|
||||
this._updateScrollbar();
|
||||
this._addModalNoBackdropEvents();
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
this._super(...arguments);
|
||||
this._removeModalNoBackdropEvents();
|
||||
// After destroying the widget, we need to trigger a resize event so that
|
||||
// the scrollbar can adjust to its default behavior.
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_updateScrollbar() {
|
||||
// When there is no backdrop the element with the scrollbar is
|
||||
// '.modal-content' (see comments in CSS).
|
||||
const modalContent = this.el.querySelector('.modal-content');
|
||||
const isOverflowing = dom.hasScrollableContent(modalContent);
|
||||
const modalInstance = window.Modal.getInstance(this.el);
|
||||
if (isOverflowing) {
|
||||
// If the "no-backdrop" modal has a scrollbar, the page's scrollbar
|
||||
// must be hidden. This is because if the two scrollbars overlap, it
|
||||
// is no longer possible to scroll using the modal's scrollbar.
|
||||
modalInstance._adjustDialog();
|
||||
} else {
|
||||
// If the "no-backdrop" modal does not have a scrollbar, the page
|
||||
// scrollbar must be displayed because we must be able to scroll the
|
||||
// page (e.g. a "cookies bar" popup at the bottom of the page must
|
||||
// not prevent scrolling the page).
|
||||
modalInstance._resetAdjustments();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_addModalNoBackdropEvents() {
|
||||
window.addEventListener('resize', this.throttledUpdateScrollbar);
|
||||
this.resizeObserver = new window.ResizeObserver(() => {
|
||||
// When the size of the modal changes, the scrollbar needs to be
|
||||
// adjusted.
|
||||
this._updateScrollbar();
|
||||
});
|
||||
this.resizeObserver.observe(this.el.querySelector('.modal-content'));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeModalNoBackdropEvents() {
|
||||
this.throttledUpdateScrollbar.cancel();
|
||||
window.removeEventListener('resize', this.throttledUpdateScrollbar);
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
delete this.resizeObserver;
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onModalNoBackdropShown() {
|
||||
this._updateScrollbar();
|
||||
this._addModalNoBackdropEvents();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onModalNoBackdropHide() {
|
||||
this._removeModalNoBackdropEvents();
|
||||
},
|
||||
});
|
||||
|
||||
publicWidget.registry.noBackdropPopup = noBackdropPopupWidget;
|
||||
|
||||
// Extending the popup widget with cookiebar functionality.
|
||||
// This allows for refusing optional cookies for now and can be
|
||||
// extended to picking which cookies categories are accepted.
|
||||
|
||||
Reference in New Issue
Block a user