Restore scroll position after live preview reload
This commit is contained in:
parent
b36009bfc0
commit
6afc48778e
5 changed files with 46 additions and 9 deletions
|
|
@ -349,7 +349,7 @@ PrintFullDocumentation();
|
|||
|
||||
## Live-preview
|
||||
|
||||
В live-режиме браузер опрашивает только `/live`. Сервер следит за файлами страниц, импортированными модулями с функциями `@Reactive`, а также за системными и пользовательскими CSS/JS. При изменении он пересобирает manifest; клиент сохраняет его и перезагружается с новым runtime. В запускаемом файле вызов `run()` обязательно помещать под `if __name__ == "__main__"`, чтобы watcher не запускал второй сервер.
|
||||
В live-режиме браузер опрашивает `/live`. Сервер следит за файлами страниц, импортированными модулями с функциями `@Reactive`, а также за системными и пользовательскими CSS/JS. После успешной пересборки браузер загружает новый HTML и восстанавливает позицию прокрутки открытой страницы в пикселях без анимации. В запускаемом файле вызов `run()` обязательно помещать под `if __name__ == "__main__"`, чтобы watcher не запускал второй сервер.
|
||||
|
||||
Используйте `@Reactive` для обычных функций уровня модуля, например для локализации или общих значений. Декоратор сохраняет аргументы и возвращаемое значение функции. При редактировании её файла live preview заново выполняет рендер всех страниц.
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ def create_app(
|
|||
@app.get("/", include_in_schema=False)
|
||||
@app.get("/index.html", response_class=HTMLResponse)
|
||||
def index() -> HTMLResponse:
|
||||
return HTMLResponse(site.result.html)
|
||||
return HTMLResponse(site.result.html, headers=NO_CACHE_HEADERS if live_preview else None)
|
||||
|
||||
@app.get("/manifest.json", response_class=JSONResponse)
|
||||
def manifest() -> JSONResponse:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
let lastPlaceholderPage = -1;
|
||||
let customScriptLoaded = false;
|
||||
let lastSyncedRevision = String(manifest.revision || "");
|
||||
let liveReloading = false;
|
||||
let pageTransitionId = 0;
|
||||
let pageAnimation = null;
|
||||
let skipNextPageAnimation = false;
|
||||
|
|
@ -838,21 +839,54 @@
|
|||
}
|
||||
|
||||
async function pollLive() {
|
||||
if (liveReloading) return;
|
||||
try {
|
||||
const response = await fetch(config.liveUrl, { cache: "no-store" });
|
||||
if (!response.ok) return;
|
||||
const live = await response.json();
|
||||
if (live.revision && live.revision !== lastSyncedRevision) {
|
||||
const changed = await syncManifest();
|
||||
if (changed) location.reload();
|
||||
}
|
||||
} catch (_) { /* local preview may stop */ }
|
||||
if (liveReloading || !live.revision) return;
|
||||
const revisionChanged = live.revision !== manifest.revision;
|
||||
if (!revisionChanged && live.updated_at === manifest.updated_at) return;
|
||||
liveReloading = true;
|
||||
if (revisionChanged) await syncManifest();
|
||||
const docslibLiveScroll = { page: currentNumber, top: elements.pageSurface.scrollTop };
|
||||
try {
|
||||
history.replaceState({ ...history.state, docslibLiveScroll }, "");
|
||||
} catch (_) { /* reload even when history state is unavailable */ }
|
||||
location.reload();
|
||||
} catch (_) {
|
||||
liveReloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreLiveScroll() {
|
||||
history.scrollRestoration = "manual";
|
||||
if (!history.state?.docslibLiveScroll) return;
|
||||
const { docslibLiveScroll, ...state } = history.state;
|
||||
history.replaceState(state, "");
|
||||
const { page, top } = docslibLiveScroll;
|
||||
if (page !== currentNumber || !Number.isFinite(top) || top <= 0) return;
|
||||
|
||||
const surface = elements.pageSurface;
|
||||
surface.scrollTo({ top, behavior: "instant" });
|
||||
if (surface.scrollTop >= top - 1) return;
|
||||
|
||||
// Images can contribute to the scroll height only after their dimensions are known.
|
||||
const pendingImages = [...elements.content.querySelectorAll("img")].filter((image) => !image.complete);
|
||||
if (!pendingImages.length) return;
|
||||
pendingImages.forEach((image) => { image.loading = "eager"; });
|
||||
await Promise.race([
|
||||
waitForPrintAssets(elements.content),
|
||||
new Promise((resolve) => setTimeout(resolve, 300)),
|
||||
]);
|
||||
if (page === currentNumber) surface.scrollTo({ top, behavior: "instant" });
|
||||
}
|
||||
|
||||
async function start() {
|
||||
applyTheme(localStorage.getItem("docslib-theme") || "light");
|
||||
loadAccessibilityPreferences();
|
||||
renderAll();
|
||||
if (config.livePreview) await restoreLiveScroll();
|
||||
announceRendered();
|
||||
syncManifest();
|
||||
if (config.livePreview) setInterval(pollLive, 1200);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
);
|
||||
}
|
||||
|
||||
const dbPromise = new Promise((resolve, reject) => {
|
||||
const dbPromise = config.livePreview ? null : new Promise((resolve, reject) => {
|
||||
if (!window.indexedDB) return reject(new Error("IndexedDB"));
|
||||
const request = indexedDB.open("docslib-cache", 1);
|
||||
request.onupgradeneeded = () => {
|
||||
|
|
@ -51,6 +51,7 @@
|
|||
}
|
||||
|
||||
async function selectManifest() {
|
||||
if (config.livePreview) return embedded;
|
||||
try {
|
||||
const cached = await cacheGet();
|
||||
if (isManifest(cached) && cached.schema_version === embedded.schema_version) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ def test_loader_renders_indexeddb_before_background_sync():
|
|||
assert "return cached;" in loader
|
||||
assert "await cachePut(embedded);" in loader
|
||||
assert "fetch(" not in loader
|
||||
assert "renderAll();\n announceRendered();\n syncManifest();" in client
|
||||
assert 'if (config.livePreview) return embedded;' in loader
|
||||
assert client.index(" renderAll();") < client.index(" announceRendered();")
|
||||
assert " announceRendered();\n syncManifest();" in client
|
||||
assert "manifest = remote" not in client
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue