summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/core/dom.js101
-rw-r--r--addons/web/static/src/js/libs/bootstrap.js14
-rw-r--r--addons/web/static/src/js/libs/jquery.js87
-rw-r--r--addons/web/static/src/scss/base_frontend.scss12
4 files changed, 212 insertions, 2 deletions
diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js
index 716593eb558..f4aba50be8a 100644
--- a/addons/web/static/src/js/core/dom.js
+++ b/addons/web/static/src/js/core/dom.js
@@ -142,6 +142,12 @@ var dom = {
}
},
/**
+ * @return {HTMLElement}
+ */
+ closestScrollable(el) {
+ return $(el).closestScrollable()[0];
+ },
+ /**
* jQuery find function behavior is::
*
* $('A').find('A B') <=> $('A A B')
@@ -229,6 +235,26 @@ var dom = {
return position;
},
/**
+ * @returns {HTMLElement}
+ */
+ getScrollingElement() {
+ return $().getScrollingElement()[0];
+ },
+ /**
+ * @param {HTMLElement} el
+ * @returns {boolean}
+ */
+ hasScrollableContent(el) {
+ return $(el).hasScrollableContent();
+ },
+ /**
+ * @param {HTMLElement} el
+ * @returns {boolean}
+ */
+ isScrollable(el) {
+ return $(el).isScrollable();
+ },
+ /**
* Protects a function which is to be used as a handler by preventing its
* execution for the duration of a previous call to it (including async
* parts of that call).
@@ -455,6 +481,81 @@ var dom = {
}
},
/**
+ * Computes the size by which a scrolling point should be decreased so that
+ * the top fixed elements of the page appear above that scrolling point.
+ *
+ * @returns {number}
+ */
+ scrollFixedOffset() {
+ let size = 0;
+ for (const el of $('.o_top_fixed_element')) {
+ size += $(el).outerHeight();
+ }
+ return size;
+ },
+ /**
+ * @param {HTMLElement} el - the element to stroll to
+ * @param {number} [options] - same as animate of jQuery
+ * @param {number} [options.extraOffset=0]
+ * extra offset to add on top of the automatic one (the automatic one
+ * being computed based on fixed header sizes)
+ * @param {number} [options.forcedOffset]
+ * offset used instead of the automatic one (extraOffset will be
+ * ignored too)
+ * @return {Promise}
+ */
+ scrollTo(el, options = {}) {
+ const $el = $(el);
+ const $scrollable = $el.parent().closestScrollable();
+ const $topLevelScrollable = $().getScrollingElement();
+ const isTopScroll = $scrollable.is($topLevelScrollable);
+
+ function _computeScrollTop() {
+ const elPosition = $scrollable[0].scrollTop + ($el.offset().top - $scrollable.offset().top);
+ let offset = options.forcedOffset;
+ if (offset === undefined) {
+ offset = (isTopScroll ? dom.scrollFixedOffset() : 0) + (options.extraOffset || 0);
+ }
+ return Math.max(0, elPosition - offset);
+ }
+
+ const originalScrollTop = _computeScrollTop();
+
+ return new Promise(resolve => {
+ const clonedOptions = Object.assign({}, options);
+
+ // During the animation, detect any change needed for the scroll
+ // offset. If any occurs, stop the animation and continuing it to
+ // the new scroll point for the remaining time.
+ // Note: limitation, the animation won't be as fluid as possible if
+ // the easing mode is different of 'linear'.
+ clonedOptions.progress = function (a, b, remainingMs) {
+ if (options.progress) {
+ options.progress.apply(this, ...arguments);
+ }
+ const newScrollTop = _computeScrollTop();
+ if (Math.abs(newScrollTop - originalScrollTop) <= 1.0) {
+ return;
+ }
+ $scrollable.stop();
+ dom.scrollTo(el, Object.assign({}, options, {
+ duration: remainingMs,
+ })).then(() => resolve());
+ };
+
+ // Detect the end of the animation to be able to indicate it to
+ // the caller via the returned Promise.
+ clonedOptions.complete = function () {
+ if (options.complete) {
+ options.complete.apply(this, ...arguments);
+ }
+ resolve();
+ };
+
+ $scrollable.animate({scrollTop: originalScrollTop}, clonedOptions);
+ });
+ },
+ /**
* Creates an automatic 'more' dropdown-menu for a set of navbar items.
*
* @param {jQuery} $el
diff --git a/addons/web/static/src/js/libs/bootstrap.js b/addons/web/static/src/js/libs/bootstrap.js
index 8f200794fa8..07e04c64a39 100644
--- a/addons/web/static/src/js/libs/bootstrap.js
+++ b/addons/web/static/src/js/libs/bootstrap.js
@@ -82,6 +82,20 @@ $.fn.tooltip.Constructor.prototype.show = function () {
return bootstrapShowFunction.call(this);
};
+/* Bootstrap scrollspy fix for non-body to spy */
+
+const bootstrapSpyRefreshFunction = $.fn.scrollspy.Constructor.prototype.refresh;
+$.fn.scrollspy.Constructor.prototype.refresh = function () {
+ bootstrapSpyRefreshFunction.apply(this, arguments);
+ if (this._scrollElement === window || this._config.method !== 'offset') {
+ return;
+ }
+ const baseScrollTop = this._getScrollTop();
+ for (let i = 0; i < this._offsets.length; i++) {
+ this._offsets[i] += baseScrollTop;
+ }
+};
+
return {
makeExtendedSanitizeWhiteList: makeExtendedSanitizeWhiteList,
};
diff --git a/addons/web/static/src/js/libs/jquery.js b/addons/web/static/src/js/libs/jquery.js
index 1a33d43c2e5..6921b004019 100644
--- a/addons/web/static/src/js/libs/jquery.js
+++ b/addons/web/static/src/js/libs/jquery.js
@@ -122,5 +122,92 @@ $.fn.extend({
});
});
},
+ /**
+ * @return {jQuery}
+ */
+ closestScrollable() {
+ let $el = this;
+ while ($el[0] !== document.scrollingElement) {
+ if ($el.isScrollable()) {
+ return $el;
+ }
+ $el = $el.parent();
+ }
+ return $el;
+ },
+ /**
+ * @returns {jQuery}
+ */
+ getScrollingElement() {
+ const $baseScrollingElement = $(document.scrollingElement);
+ if ($baseScrollingElement.isScrollable()
+ && $baseScrollingElement.hasScrollableContent()) {
+ return $baseScrollingElement;
+ }
+ const bodyHeight = $(document.body).height();
+ for (const el of document.body.children) {
+ // Search for a body child which is at least as tall as the body
+ // and which has the ability to scroll if enough content in it. If
+ // found, suppose this is the top scrolling element.
+ if (el.scrollHeight < bodyHeight) {
+ continue;
+ }
+ const $el = $(el);
+ if ($el.isScrollable()) {
+ return $el;
+ }
+ }
+ return $baseScrollingElement;
+ },
+ /**
+ * @return {boolean}
+ */
+ hasScrollableContent() {
+ return this[0].scrollHeight > this[0].clientHeight;
+ },
+ /**
+ * @returns {boolean}
+ */
+ isScrollable() {
+ const overflow = this.css('overflow-y');
+ return overflow === 'auto' || overflow === 'scroll'
+ || (overflow === 'visible' && this === document.scrollingElement);
+ },
});
+
+// jQuery functions monkey-patching
+
+// Some magic to ensure scrolltop and animate on html/body animate the top level
+// scrollable element even if not html or body.
+const originalScrollTop = $.fn.scrollTop;
+$.fn.scrollTop = function (value) {
+ if (value !== undefined && this.filter('html, body').length) {
+ // The caller wants to scroll a set of elements including html and/or
+ // body to a specific point -> do that but make sure to add the real
+ // top level element to that set of elements if any different is found.
+ originalScrollTop.apply(this.not('html, body').add($().getScrollingElement()), arguments);
+ return this;
+ } else if (value === undefined && this.eq(0).is('html, body')) {
+ // The caller wants to get the scroll point of a set of elements, jQuery
+ // will return the scroll point of the first one, if it is html or body
+ // return the scroll point of the real top level element.
+ return originalScrollTop.apply($().getScrollingElement(), arguments);
+ }
+ return originalScrollTop.apply(this, arguments);
+};
+const originalAnimate = $.fn.animate;
+$.fn.animate = function (properties, ...rest) {
+ const props = Object.assign({}, properties);
+ if ('scrollTop' in props && this.filter('html, body').length) {
+ // The caller wants to scroll a set of elements including html and/or
+ // body to a specific point -> do that but make sure to add the real
+ // top level element to that set of elements if any different is found.
+ originalAnimate.call(this.not('html, body').add($().getScrollingElement()), {'scrollTop': props['scrollTop']}, ...rest);
+ delete props['scrollTop'];
+ }
+ if (!Object.keys(props).length) {
+ return this;
+ }
+ return originalAnimate.call(this, props, ...rest);
+};
});
diff --git a/addons/web/static/src/scss/base_frontend.scss b/addons/web/static/src/scss/base_frontend.scss
index f1c341a89f9..585ce965ec5 100644
--- a/addons/web/static/src/scss/base_frontend.scss
+++ b/addons/web/static/src/scss/base_frontend.scss
@@ -1,4 +1,9 @@
// Frontend general
+html, body, #wrapwrap {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
#wrapwrap {
// The z-index is useful to prevent that children with a negative z-index
// go behind the wrapwrap (we create a new stacking context).
@@ -6,8 +11,11 @@
position: relative;
display: flex;
flex-flow: column nowrap;
- width: 100%;
- min-height: 100%;
+ // ... we delegate the scroll to that top element instead of the window/body
+ // This is at least needed for the edit mode to not have a double scrollbar
+ // due to the right editor panel (and since we want to minimize the style
+ // difference between edit mode and non-edit mode (wysiwyg)...).
+ overflow: auto;
> * {
flex: 0 0 auto;