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-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 заново выполняет рендер всех страниц.
|
Используйте `@Reactive` для обычных функций уровня модуля, например для локализации или общих значений. Декоратор сохраняет аргументы и возвращаемое значение функции. При редактировании её файла live preview заново выполняет рендер всех страниц.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ def create_app(
|
||||||
@app.get("/", include_in_schema=False)
|
@app.get("/", include_in_schema=False)
|
||||||
@app.get("/index.html", response_class=HTMLResponse)
|
@app.get("/index.html", response_class=HTMLResponse)
|
||||||
def index() -> 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)
|
@app.get("/manifest.json", response_class=JSONResponse)
|
||||||
def manifest() -> JSONResponse:
|
def manifest() -> JSONResponse:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
let lastPlaceholderPage = -1;
|
let lastPlaceholderPage = -1;
|
||||||
let customScriptLoaded = false;
|
let customScriptLoaded = false;
|
||||||
let lastSyncedRevision = String(manifest.revision || "");
|
let lastSyncedRevision = String(manifest.revision || "");
|
||||||
|
let liveReloading = false;
|
||||||
let pageTransitionId = 0;
|
let pageTransitionId = 0;
|
||||||
let pageAnimation = null;
|
let pageAnimation = null;
|
||||||
let skipNextPageAnimation = false;
|
let skipNextPageAnimation = false;
|
||||||
|
|
@ -838,21 +839,54 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollLive() {
|
async function pollLive() {
|
||||||
|
if (liveReloading) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(config.liveUrl, { cache: "no-store" });
|
const response = await fetch(config.liveUrl, { cache: "no-store" });
|
||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
const live = await response.json();
|
const live = await response.json();
|
||||||
if (live.revision && live.revision !== lastSyncedRevision) {
|
if (liveReloading || !live.revision) return;
|
||||||
const changed = await syncManifest();
|
const revisionChanged = live.revision !== manifest.revision;
|
||||||
if (changed) location.reload();
|
if (!revisionChanged && live.updated_at === manifest.updated_at) return;
|
||||||
}
|
liveReloading = true;
|
||||||
} catch (_) { /* local preview may stop */ }
|
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() {
|
async function start() {
|
||||||
applyTheme(localStorage.getItem("docslib-theme") || "light");
|
applyTheme(localStorage.getItem("docslib-theme") || "light");
|
||||||
loadAccessibilityPreferences();
|
loadAccessibilityPreferences();
|
||||||
renderAll();
|
renderAll();
|
||||||
|
if (config.livePreview) await restoreLiveScroll();
|
||||||
announceRendered();
|
announceRendered();
|
||||||
syncManifest();
|
syncManifest();
|
||||||
if (config.livePreview) setInterval(pollLive, 1200);
|
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"));
|
if (!window.indexedDB) return reject(new Error("IndexedDB"));
|
||||||
const request = indexedDB.open("docslib-cache", 1);
|
const request = indexedDB.open("docslib-cache", 1);
|
||||||
request.onupgradeneeded = () => {
|
request.onupgradeneeded = () => {
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectManifest() {
|
async function selectManifest() {
|
||||||
|
if (config.livePreview) return embedded;
|
||||||
try {
|
try {
|
||||||
const cached = await cacheGet();
|
const cached = await cacheGet();
|
||||||
if (isManifest(cached) && cached.schema_version === embedded.schema_version) {
|
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 "return cached;" in loader
|
||||||
assert "await cachePut(embedded);" in loader
|
assert "await cachePut(embedded);" in loader
|
||||||
assert "fetch(" not 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
|
assert "manifest = remote" not in client
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue