[feature][editor] Сохранение позиции чтения (scroll position) при перезагрузке страницы #266
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Проблема / мотивация
Сейчас при перезагрузке страницы документа (F5) или повторном открытии страницы окно
прокручивается в самое начало. Для длинных страниц читатель теряет место, на котором
остановился, и вынужден прокручивать заново. Хочется, чтобы позиция чтения (scroll
position) сохранялась и восстанавливалась.
Как сейчас устроено (результаты исследования)
window), а не внутренним контейнером сoverflow. Подтверждение: оглавление используетwindow.scrollY/window.scrollToв
apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx(строки ~61–65). В
app-shell.module.cssнет своего скролл-контейнера — MantineAppShellскроллит body/window.apps/client/src/features/editor/page-editor.tsx.Контент грузится асинхронно: сначала показывается статический рендер из кэша
(
showStatic = true), затем, после синхронизации collab-провайдера (Yjs/Hocuspocus),происходит переключение на «живой» редактор (
showStatic = false). Высота документастановится финальной только после этого переключения.
apps/client/src/features/editor/hooks/use-editor-scroll.ts, которыйскроллит к якорю по
#hashв URL при загрузке (handleScrollToвызывается вonCreateредактора). Новая логика восстановления позиции обязана уступать этомумеханизму: если в URL есть
#hash— приоритет у якоря, позицию из хранилища невосстанавливаем.
Предлагаемое решение
Новый хук
useScrollPosition(pageId)вapps/client/src/features/editor/hooks/use-scroll-position.ts, подключаемый внутриpage-editor.tsx. Хук:window.scrollYдля текущей страницы (ключ —pageId):scroll(throttle ~250 мс),pagehideиvisibilitychange(на момент перезагрузки/закрытия/смены вкладки),(
!showStatic && editor):#hash— выходим (приоритет у якорной прокрутки);как обработчики успеют перезаписать его свежим
0);documentElement.scrollHeight - innerHeight >= targetY(контент догрузился), либо потаймауту (~5 с) скроллим к максимально достижимой позиции;
страницу).
Где именно подключить
В
page-editor.tsx:const { restoreScrollPosition } = useScrollPosition(pageId);onCreateуже вызываетhandleScrollTo(editor)для якоря — конфликта нет, т.к.методы взаимоисключающие: якорь работает только при наличии
#hash, восстановление —только при его отсутствии.)
Эскиз хука (иллюстративно; все комментарии в коде — на английском)
Решение по хранилищу
Рекомендуется
sessionStorageкак значение по умолчанию (MVP):вкладке, очищается при её закрытии;
Альтернатива —
localStorage(позиция переживает закрытие/перезапуск браузера,удобнее для «продолжить чтение»). Минус — рост хранилища, поэтому потребуется единая
JSON-карта
{ pageId: { y, ts } }с LRU-ограничением (например, последние 50 записей поts). Предлагается оставить как опциональное улучшение/follow-up, если потребуется.Краевые случаи
#hashв URL — не восстанавливаем позицию (приоритет у якоря).«пустой» документ до его размещения.
showStatic → live— восстанавливаем только после!showStatic,чтобы пересборка DOM «живого» редактора не сбросила прокрутку.
монтировании в
initialTargetRef.FullEditor/PageEditorмонтируются сkey={page.id}, поэтому при смене страницы инстанс пересоздаётся;pageIdRefстарогоинстанса не меняется. Дополнительно ключ хранилища привязан к
pageId.тихо игнорируются (фича не критична).
min(targetY, maxScroll).showStatic) — в MVPвосстановление не сработает (нет перехода в
!showStatic); приемлемо. При желании —отдельный fallback по таймауту.
Затрагиваемые файлы
apps/client/src/features/editor/hooks/use-scroll-position.ts— новый хук.apps/client/src/features/editor/page-editor.tsx— подключение хука + эффектвосстановления.
Критерии приёмки (DoD)
(с точностью до высоты, появившейся при догрузке контента).
#hashпо-прежнему скроллит к якорю, позиция из хранилища неперебивает якорь.
страницами.
од��оразово.
Storageне валят страницу.Риски
handleScrollTo— снимается взаимоисключающими условиями(
#hash).