diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index 9fe38adbde6..87219284e4d 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -88,6 +88,8 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, { } }); }, + getScrollPosition: '_onGetScrollPosition', + scrollTo: '_onScrollTo', }, init: function (parent) { this.client_options = {}; @@ -373,6 +375,16 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, { this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky); } }, + /** + * This function must be implemented to provide to the caller the current + * scroll position (left and top) of the webclient. + * + * @abstract + * @param {OdooEvent} ev + * @param {function} ev.data.callback + */ + _onGetScrollPosition: function (ev) { + }, /** * Loads an action from the database given its ID. * @@ -394,6 +406,20 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, { _onPushState: function (e) { this.do_push_state(e.data.state); }, + /** + * This function must be implemented by actual webclient to scroll either to + * a given offset or to a target element (given a selector). + * It must be called with: trigger_up('scrollTo', options). + * + * @abstract + * @param {OdooEvent} ev + * @param {integer} [ev.data.top] the number of pixels to scroll from top + * @param {integer} [ev.data.left] the number of pixels to scroll from left + * @param {string} [ev.data.selector] the selector of the target element to + * scroll to + */ + _onScrollTo: function (ev) { + }, /** * Displays a visual effect (for example, a rainbowman0 * diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js index 1e9c52238bc..4cf10b8813a 100644 --- a/addons/web/static/src/js/chrome/action_manager.js +++ b/addons/web/static/src/js/chrome/action_manager.js @@ -258,7 +258,9 @@ var ActionManager = Widget.extend({ callbacks: [{widget: controller.widget}], }); - this.trigger_up('scrollTo', {offset: controller.scrollTop || 0}); + if (controller.scrollPosition) { + this.trigger_up('scrollTo', controller.scrollPosition); + } if (!controller.widget.need_control_panel) { this.controlPanel.do_hide(); @@ -294,7 +296,7 @@ var ActionManager = Widget.extend({ _detachCurrentController: function () { var currentController = this.getCurrentController(); if (currentController) { - currentController.scrollTop = this._getScrollTop(); + currentController.scrollPosition = this._getScrollPosition(); dom.detach([{widget: currentController.widget}]); } }, @@ -613,19 +615,19 @@ var ActionManager = Widget.extend({ return state; }, /** - * Returns the current vertical scroll position. + * Returns the current horizontal and vertical scroll positions. * * @private - * @returns {integer} + * @returns {Object} */ - _getScrollTop: function () { - var scrollTop; - this.trigger_up('getScrollTop', { - callback: function (value) { - scrollTop = value; + _getScrollPosition: function () { + var scrollPosition; + this.trigger_up('getScrollPosition', { + callback: function (_scrollPosition) { + scrollPosition = _scrollPosition; } }); - return scrollTop; + return scrollPosition; }, /** * Dispatches the given action to the corresponding handler to execute it, diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js index 76aadf49463..3f832a6a49f 100644 --- a/addons/web/static/src/js/chrome/web_client.js +++ b/addons/web/static/src/js/chrome/web_client.js @@ -4,6 +4,7 @@ odoo.define('web.WebClient', function (require) { var AbstractWebClient = require('web.AbstractWebClient'); var config = require('web.config'); var data_manager = require('web.data_manager'); +var dom = require('web.dom'); var framework = require('web.framework'); var Menu = require('web.Menu'); var session = require('web.session'); @@ -177,6 +178,39 @@ return AbstractWebClient.extend({ this.menu.reflow(); } }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _onGetScrollPosition: function (ev) { + ev.data.callback({ + left: this.action_manager.el.scrollLeft, + top: this.action_manager.el.scrollTop, + }); + }, + /** + * @override + */ + _onScrollTo: function (ev) { + var offset; + if (ev.data.selector) { + offset = dom.getPosition(document.querySelector(ev.data.selector)); + // substract the position of the ActionManager as it is the + // scrolling element + var actionManagerOffset = dom.getPosition(this.action_manager.el); + offset.left -= actionManagerOffset.left; + offset.top -= actionManagerOffset.top; + } else { + offset = {top: ev.data.top || 0, left: ev.data.left || 0}; + } + + this.action_manager.el.scrollTop = offset.top; + this.action_manager.el.scrollLeft = offset.left; + }, }); }); diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index 995a5102c37..ee39fd95596 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -102,6 +102,18 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { } return this._super.apply(this, arguments); }, + /** + * Called each time the controller is attached into the DOM. + */ + on_attach_callback: function () { + this.renderer.on_attach_callback(); + }, + /** + * Called each time the controller is detached from the DOM. + */ + on_detach_callback: function () { + this.renderer.on_detach_callback(); + }, //-------------------------------------------------------------------------- // Public diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js index 935fcc6643e..2394fb31a64 100644 --- a/addons/web/static/src/js/views/abstract_renderer.js +++ b/addons/web/static/src/js/views/abstract_renderer.js @@ -36,6 +36,14 @@ return Widget.extend({ this.$el.addClass(this.arch.attrs.class); return $.when(this._render(), this._super()); }, + /** + * Called each time the renderer is attached into the DOM. + */ + on_attach_callback: function () {}, + /** + * Called each time the renderer is detached from the DOM. + */ + on_detach_callback: function () {}, //-------------------------------------------------------------------------- // Public diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 05031ca2463..263412fd2a2 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -144,7 +144,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { .then(function () { // Reset the scroll position to the top on page changed only if (!limitChanged) { - self.trigger_up('scrollTo', {offset: 0}); + self.trigger_up('scrollTo', {top: 0}); } }) .then(this.pager.enable.bind(this.pager)); diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js index c609e45fc60..8619448e793 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js @@ -30,6 +30,41 @@ KanbanRenderer.include({ init: function () { this._super.apply(this, arguments); this.activeColumnIndex = 0; // index of the currently displayed column + this._scrollPosition = null; + }, + /** + * As this renderer defines its own scrolling area (the column in grouped + * mode), we override this hook to restore the scroll position like it was + * when the renderer has been last detached. + * + * @override + */ + on_attach_callback: function () { + if (this._scrollPosition && this.state.groupedBy.length && this.widgets.length) { + var $column = this.widgets[this.activeColumnIndex].$el; + $column.scrollLeft(this._scrollPosition.left); + $column.scrollTop(this._scrollPosition.top); + } + this._super.apply(this, arguments); + }, + /** + * As this renderer defines its own scrolling area (the column in grouped + * mode), we override this hook to store the scroll position, so that we can + * restore it if the renderer is re-attached to the DOM later. + * + * @override + */ + on_detach_callback: function () { + if (this.state.groupedBy.length && this.widgets.length) { + var $column = this.widgets[this.activeColumnIndex].$el; + this._scrollPosition = { + left: $column.scrollLeft(), + top: $column.scrollTop(), + }; + } else { + this._scrollPosition = null; + } + this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 9f7bacc1689..4d46b9167d1 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -350,6 +350,52 @@ QUnit.module('ActionManager', { actionManager.destroy(); }); + QUnit.test('stores and restores scroll position', function (assert) { + assert.expect(7); + + var left; + var top; + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + getScrollPosition: function (ev) { + assert.step('getScrollPosition'); + ev.data.callback({left: left, top: top}); + }, + scrollTo: function (ev) { + assert.step('scrollTo left ' + ev.data.left + ', top ' + ev.data.top); + }, + }, + }); + + // execute a first action and simulate a scroll + assert.step('execute action 3'); + actionManager.doAction(3); + left = 50; + top = 100; + + // execute a second action (in which we don't scroll) + assert.step('execute action 4'); + actionManager.doAction(4); + + // go back using the breadcrumbs + assert.step('go back to action 3'); + $('.o_control_panel .breadcrumb a').click(); + + assert.verifySteps([ + 'execute action 3', + 'execute action 4', + 'getScrollPosition', // of action 3, before leaving it + 'go back to action 3', + 'getScrollPosition', // of action 4, before leaving it + 'scrollTo left 50, top 100', // restore scroll position of action 3 + ]); + + actionManager.destroy(); + }); + QUnit.module('Push State'); QUnit.test('properly push state', function (assert) {