Skip to content

Commit ee1076c

Browse files
feat: implement the scroll state handling and restore
Signed-off-by: Rostislav Nagimov <[email protected]>
1 parent 2d38b86 commit ee1076c

File tree

3 files changed

+171
-50
lines changed

3 files changed

+171
-50
lines changed

plugins/workbench-resources/src/components/Workbench.svelte

Lines changed: 151 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,6 @@
9191
parseLinkId,
9292
updateFocus
9393
} from '@hcengineering/view-resources'
94-
import type {
95-
Application,
96-
NavigatorModel,
97-
SpecialNavModel,
98-
ViewConfiguration,
99-
WorkbenchTab
100-
} from '@hcengineering/workbench'
10194
import { getContext, onDestroy, onMount, tick } from 'svelte'
10295
import { subscribeMobile } from '../mobile'
10396
import workbench from '../plugin'
@@ -115,16 +108,19 @@
115108
import TopMenu from './icons/TopMenu.svelte'
116109
import WidgetsBar from './sidebar/Sidebar.svelte'
117110
import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar'
111+
import { get } from 'svelte/store'
112+
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration, WorkbenchTab} from '@hcengineering/workbench';
113+
118114
import {
119115
getTabDataByLocation,
120116
getTabLocation,
121117
prevTabIdStore,
122118
selectTab,
123119
syncWorkbenchTab,
124120
tabIdStore,
125-
tabsStore
126-
} from '../workbench'
127-
import { get } from 'svelte/store'
121+
tabsStore,
122+
updateTabUiState
123+
} from '../workbench';
128124
129125
const HIDE_NAVIGATOR = 720
130126
const FLOAT_ASIDE = 1024 // lg
@@ -254,6 +250,112 @@
254250
}
255251
}
256252
253+
let currentTabId: Ref<WorkbenchTab> | undefined = $tabIdStore;
254+
255+
function saveScrollState() {
256+
if (!currentTabId || !contentPanel) {
257+
return;
258+
}
259+
260+
const allScrollers = contentPanel.querySelectorAll<HTMLElement>('.scroll');
261+
if (allScrollers.length === 0) {
262+
return;
263+
}
264+
265+
console.log(`[SAVE] Tab ${currentTabId}. Found a total of ${allScrollers.length} elements with class .scroll.`);
266+
267+
const scrollableElements = Array.from(allScrollers).filter(
268+
(el) => el.scrollHeight > el.clientHeight
269+
);
270+
271+
console.log(`[SAVE] Of these, ${scrollableElements.length} are actually scrollable.`);
272+
273+
if (scrollableElements.length > 0) {
274+
const scrollPositions: Record<string, number> = {};
275+
scrollableElements.forEach((scroller, index) => {
276+
const id = `index-${index}`;
277+
const scrollTop = scroller.scrollTop;
278+
scrollPositions[id] = scrollTop;
279+
console.log(`[SAVE] -> Saving for element #${id} ORIGINAL scrollTop value: ${scrollTop}`);
280+
});
281+
282+
console.log('[SAVE] Final object to save:', scrollPositions);
283+
updateTabUiState(currentTabId, { scrollPositions });
284+
}
285+
}
286+
287+
async function restoreScrollState() {
288+
const tabId = $tabIdStore;
289+
if (!tabId || !contentPanel) {
290+
return;
291+
}
292+
293+
console.log('[RESTORE] Waiting for two ticks for components to render...');
294+
await tick();
295+
await tick();
296+
297+
const tab = get(tabsStore).find((t) => t._id === tabId);
298+
const savedScrollPositions = tab?.uiState?.scrollPositions;
299+
300+
console.log(`[RESTORE] Tab ${tabId}. Found saved positions:`, savedScrollPositions);
301+
302+
if (!savedScrollPositions) {
303+
console.log('[RESTORE] Positions object is undefined, exiting.');
304+
return;
305+
}
306+
307+
if (Object.keys(savedScrollPositions).length === 0) {
308+
console.log('[RESTORE] No saved positions (object is empty), exiting.');
309+
return;
310+
}
311+
312+
const finalPositions = savedScrollPositions;
313+
const expectedCount = Object.keys(finalPositions).length;
314+
let attempts = 0;
315+
const maxAttempts = 100;
316+
317+
function findAndApplyScroll() {
318+
const allScrollers = contentPanel.querySelectorAll<HTMLElement>('.scroll');
319+
const scrollableElements = Array.from(allScrollers).filter(
320+
(el) => el.scrollHeight > el.clientHeight
321+
);
322+
323+
if (scrollableElements.length === expectedCount) {
324+
console.log(`[RESTORE] Success! Found ${scrollableElements.length} scrollable elements.`);
325+
326+
scrollableElements.forEach((scroller, index) => {
327+
const id = `index-${index}`;
328+
const savedScrollTop = finalPositions[id];
329+
330+
if (savedScrollTop !== undefined) {
331+
console.log(`[RESTORE] -> Direct assignment of scroller.scrollTop = ${savedScrollTop} for element #${id}`);
332+
scroller.scrollTop = savedScrollTop;
333+
}
334+
});
335+
return;
336+
}
337+
338+
if (attempts < maxAttempts) {
339+
attempts++;
340+
requestAnimationFrame(findAndApplyScroll);
341+
} else {
342+
console.error(`[RESTORE] FAILED to wait for content to load in .scroll after ${maxAttempts} attempts.`);
343+
}
344+
}
345+
346+
findAndApplyScroll();
347+
}
348+
349+
$: {
350+
if (currentTabId !== $tabIdStore) {
351+
console.log(`%c[SWITCH] Tab changed. Old: ${currentTabId}, New: ${$tabIdStore}`, 'color: blue; font-weight: bold;');
352+
353+
saveScrollState();
354+
void restoreScrollState();
355+
currentTabId = $tabIdStore;
356+
}
357+
}
358+
257359
onMount(() => {
258360
pushRootBarComponent('right', view.component.SearchSelector)
259361
pushRootBarComponent('left', workbench.component.WorkbenchTabs, 30)
@@ -996,46 +1098,46 @@
9961098
!(mobileAdaptive && $deviceInfo.isPortrait)}
9971099
data-id={'contentPanel'}
9981100
>
999-
{#if currentApplication && currentApplication.component}
1000-
<Component
1001-
is={currentApplication.component}
1002-
props={{
1003-
currentSpace,
1004-
workbenchWidth
1005-
}}
1006-
/>
1007-
{:else if specialComponent}
1008-
<Component
1009-
is={specialComponent.component}
1010-
props={{
1011-
model: navigatorModel,
1012-
...specialComponent.componentProps,
1013-
currentSpace,
1014-
space: currentSpace,
1015-
navigationModel: specialComponent?.navigationModel,
1016-
workbenchWidth,
1017-
queryBuilder: specialComponent?.queryBuilder
1018-
}}
1019-
on:action={(e) => {
1020-
if (e?.detail) {
1021-
const loc = getCurrentLocation()
1022-
loc.query = { ...loc.query, ...e.detail }
1023-
navigate(loc)
1024-
}
1025-
}}
1026-
/>
1027-
{:else if currentView?.component !== undefined}
1028-
<Component
1029-
is={currentView.component}
1101+
{#if currentApplication && currentApplication.component}
1102+
<Component
1103+
is={currentApplication.component}
1104+
props={{
1105+
currentSpace,
1106+
workbenchWidth
1107+
}}
1108+
/>
1109+
{:else if specialComponent}
1110+
<Component
1111+
is={specialComponent.component}
1112+
props={{
1113+
model: navigatorModel,
1114+
...specialComponent.componentProps,
1115+
currentSpace,
1116+
space: currentSpace,
1117+
navigationModel: specialComponent?.navigationModel,
1118+
workbenchWidth,
1119+
queryBuilder: specialComponent?.queryBuilder
1120+
}}
1121+
on:action={(e) => {
1122+
if (e?.detail) {
1123+
const loc = getCurrentLocation()
1124+
loc.query = { ...loc.query, ...e.detail }
1125+
navigate(loc)
1126+
}
1127+
}}
1128+
/>
1129+
{:else if currentView?.component !== undefined}
1130+
<Component
1131+
is={currentView.component}
10301132
props={{ ...currentView.componentProps, currentSpace, currentView, workbenchWidth }}
1031-
/>
1032-
{:else if $accessDeniedStore}
1033-
<div class="flex-center h-full">
1034-
<h2><Label label={workbench.string.AccessDenied} /></h2>
1035-
</div>
1036-
{:else}
1037-
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
1038-
{/if}
1133+
/>
1134+
{:else if $accessDeniedStore}
1135+
<div class="flex-center h-full">
1136+
<h2><Label label={workbench.string.AccessDenied} /></h2>
1137+
</div>
1138+
{:else}
1139+
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
1140+
{/if}
10391141
</div>
10401142
</div>
10411143
{#if $sidebarStore.variant === SidebarVariant.EXPANDED && !$sidebarStore.float}

plugins/workbench-resources/src/workbench.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
} from '@hcengineering/ui'
3838
import view from '@hcengineering/view'
3939
import { parseLinkId } from '@hcengineering/view-resources'
40-
import { type Application, workbenchId, type WorkbenchTab } from '@hcengineering/workbench'
40+
import { type Application, workbenchId, type WorkbenchTab, type TabUiState } from '@hcengineering/workbench'
4141
import { derived, get, writable } from 'svelte/store'
4242

4343
import setting from '@hcengineering/setting'
@@ -62,6 +62,19 @@ locationWorkspaceStore.subscribe((workspace) => {
6262
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
6363
})
6464

65+
export function updateTabUiState(tabId: Ref<WorkbenchTab>, state: Partial<TabUiState>): void {
66+
tabsStore.update(tabs => {
67+
const tab = tabs.find(t => t._id === tabId);
68+
if (tab) {
69+
tab.uiState = {
70+
...(tab.uiState || {}),
71+
...state
72+
};
73+
}
74+
return tabs;
75+
});
76+
}
77+
6578
const syncTabLoc = reduceCalls(async (): Promise<void> => {
6679
const loc = getCurrentLocation()
6780
const workspace = get(locationWorkspaceStore)

plugins/workbench/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,18 @@ export interface WidgetTab {
9999
readonly?: boolean
100100
}
101101

102+
/** @public */
103+
export interface TabUiState {
104+
scrollPositions?: Record<string, number>;
105+
}
106+
102107
/** @public */
103108
export interface WorkbenchTab extends Preference {
104109
attachedTo: AccountUuid
105110
location: string
106111
isPinned: boolean
107112
name?: string
113+
uiState?: TabUiState;
108114
}
109115

110116
/** @public */

0 commit comments

Comments
 (0)