diff options
Diffstat (limited to 'addons/web/static/src')
| -rw-r--r-- | addons/web/static/src/js/core/dom.js | 101 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/bootstrap.js | 14 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/jquery.js | 87 | ||||
| -rw-r--r-- | addons/web/static/src/scss/base_frontend.scss | 12 |
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; |
