[IMP] web: restore scroll position
This rev. improves the way the scroll position is restored when coming back to a view (e.g. open a kanban view, scroll, click on a record to open it in a form view, go back to kanban using the breadcrumbs): - generalize the scroll position handling from web_enterprise to web to make it available in the community webclient as well - also restore left scroll position, not only top (especially useful for grouped Kanban views with a lot of columns) - handle the case where the scrolling area is a sub element of the view (e.g. the grouped Kanban view in mobile). Task 34037 Co-authored-by: Mohammed Shekha <msh@openerp.com> Co-authored-by: Ankit Sathvara <asa@odoo.com>
This commit is contained in:
co-authored by
Mohammed Shekha
Ankit Sathvara
parent
1a99d22f19
commit
4e204ed223
@@ -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
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user