[REF] web: scrolling: better handling of action scroll

This commit refactors the way actions restore their scrolling
positions (when coming back). By default, in desktop, we want the
o_content div of the action to scroll. In mobile, we want the
whole action to scroll (including the ControlPanel). With this
commit, the logic is handled at the concerned level: in the
action hook for the desktop case, and in the action service for
the mobile case.

Part-of: odoo/odoo#92475
This commit is contained in:
Aaron Bohy
2022-06-30 09:29:45 +02:00
parent 4f984568e1
commit 6982b4c5c5
2 changed files with 80 additions and 74 deletions
@@ -1,12 +1,7 @@
/** @odoo-module **/
import { getScrollPosition, setScrollPosition } from "./scrolling";
const { onMounted, useComponent, useEffect, useExternalListener } = owl;
const { useComponent, useEffect } = owl;
// -----------------------------------------------------------------------------
// Action hook
// -----------------------------------------------------------------------------
const scrollSymbol = Symbol("scroll");
export class CallbackRecorder {
@@ -66,32 +61,93 @@ export function useSetupAction(params = {}) {
__getContext__,
} = component.env;
if (__beforeLeave__ && params.beforeLeave) {
useCallbackRecorder(__beforeLeave__, params.beforeLeave);
const { beforeUnload, beforeLeave, getGlobalState, getLocalState, rootRef } = params;
if (beforeUnload) {
useExternalListener(window, "beforeunload", beforeUnload);
}
if (__getGlobalState__ && params.getGlobalState) {
useCallbackRecorder(__getGlobalState__, params.getGlobalState);
if (__beforeLeave__ && beforeLeave) {
useCallbackRecorder(__beforeLeave__, beforeLeave);
}
if (__getLocalState__) {
useCallbackRecorder(__getLocalState__, () => {
if (__getGlobalState__ && (getGlobalState || rootRef)) {
useCallbackRecorder(__getGlobalState__, () => {
const state = {};
state[scrollSymbol] = getScrollPosition(component.env);
if (params.getLocalState) {
Object.assign(state, params.getLocalState());
if (getGlobalState) {
Object.assign(state, getGlobalState());
}
if (rootRef) {
const searchPanelEl = rootRef.el.querySelector(".o_content .o_search_panel");
if (searchPanelEl) {
state[scrollSymbol] = {
searchPanel: {
left: searchPanelEl.scrollLeft,
top: searchPanelEl.scrollTop,
},
};
}
}
return state;
});
if (rootRef) {
onMounted(() => {
const { globalState } = component.props;
const scrolling = globalState && globalState[scrollSymbol];
if (scrolling) {
const searchPanelEl = rootRef.el.querySelector(".o_content .o_search_panel");
if (searchPanelEl) {
searchPanelEl.scrollLeft =
(scrolling.searchPanel && scrolling.searchPanel.left) || 0;
searchPanelEl.scrollTop =
(scrolling.searchPanel && scrolling.searchPanel.top) || 0;
}
}
});
}
}
if (__getLocalState__ && (getLocalState || rootRef)) {
useCallbackRecorder(__getLocalState__, () => {
const state = {};
if (getLocalState) {
Object.assign(state, getLocalState());
}
if (rootRef) {
if (component.env.isSmall) {
state[scrollSymbol] = {
root: { left: rootRef.el.scrollLeft, top: rootRef.el.scrollTop },
};
} else {
const contentEl = rootRef.el.querySelector(".o_content");
if (contentEl) {
state[scrollSymbol] = {
content: { left: contentEl.scrollLeft, top: contentEl.scrollTop },
};
}
}
}
return state;
});
if (rootRef) {
onMounted(() => {
const { state } = component.props;
const scrolling = state && state[scrollSymbol];
if (scrolling) {
if (component.env.isSmall) {
rootRef.el.scrollTop = (scrolling.root && scrolling.root.top) || 0;
rootRef.el.scrollLeft = (scrolling.root && scrolling.root.left) || 0;
} else if (scrolling.content) {
const contentEl = rootRef.el.querySelector(".o_content");
if (contentEl) {
contentEl.scrollTop = scrolling.content.top || 0;
contentEl.scrollLeft = scrolling.content.left || 0;
}
}
}
});
}
}
if (__getContext__ && params.getContext) {
useCallbackRecorder(__getContext__, params.getContext);
}
useEffect(
() => {
if (component.props.state && component.props.state[scrollSymbol]) {
setScrollPosition(component.env, component.props.state[scrollSymbol]);
}
},
() => []
);
}
@@ -1,50 +0,0 @@
/** @odoo-module **/
/*
* Returns the main scrollable element relevant in a context of an action.
* In desktop, the relevant element is the .o_content of the action.
* In mobile, it is the html node itself, provided no other children is scrollable
* Most of the times in mobile, we don't want to scroll individual elements, so every bit
* of code should enforce that html is the scrollable element.
*
* @param {OdooEnv} env
* @return {Element}
*/
function getScrollableElement(env) {
if (env.isSmall) {
return document.firstElementChild; // aka html node;
} else {
return document.querySelector(".o_web_client > .o_action_manager > .o_action > .o_content");
}
}
/**
* Retrieves the current top and left scroll position. By default, the scrolling
* area is the '.o_content' main div. In mobile, it is the body.
*
* @param {OdooEnv} env
*/
export function getScrollPosition(env) {
const scrollingEl = getScrollableElement(env);
return {
left: scrollingEl ? scrollingEl.scrollLeft : 0,
top: scrollingEl ? scrollingEl.scrollTop : 0,
};
}
/**
* Sets top and left scroll positions to the given values. By default, the
* scrolling area is the '.o_content' main div. In mobile, it is the body.
*
* @param {OdooEnv} env
* @param {Object} offset
* @param {number} [offset.left=0]
* @param {number} [offset.top=0]
*/
export function setScrollPosition(env, offset) {
const scrollingEl = getScrollableElement(env);
if (scrollingEl) {
scrollingEl.scrollLeft = offset.left || 0;
scrollingEl.scrollTop = offset.top || 0;
}
}