diff --git a/README.md b/README.md index b88525a..2e8d605 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ console.log(globalThis.COMPILED_FROM_VERSION); // например, "0.3.7" PrintFullDocumentation(); ``` -Она формирует единый документ из всех страниц в порядке manifest и открывает системный диалог печати. Каждая страница документации начинается с нового печатного листа. Печатная тема всегда использует белый фон и чёрный текст независимо от выбранной экранной темы. +Она сразу закрывает интерфейс тематическим loading-overlay, формирует единый документ из всех страниц в порядке manifest, подставляет и декодирует все изображения, ожидает готовности шрифтов и только затем открывает системный диалог печати. Каждая страница документации начинается с нового печатного листа. Печатная тема всегда использует белый фон, чёрный текст и компактные заголовки независимо от выбранной экранной темы. ## Live-preview diff --git a/docslib/_version.py b/docslib/_version.py index 84cd7c2..89acfb9 100644 --- a/docslib/_version.py +++ b/docslib/_version.py @@ -1,3 +1,3 @@ """Single source of truth for the SharedDocsLib package version.""" -__version__ = "0.3.8" +__version__ = "0.3.9" diff --git a/docslib/templates/client.css b/docslib/templates/client.css index 38a646e..b5533ff 100644 --- a/docslib/templates/client.css +++ b/docslib/templates/client.css @@ -135,6 +135,8 @@ button { color: inherit; } .mobile-view, .mobile-dock { display: none; } .full-print-document { display: none; } +.print-loading-overlay { position: fixed; inset: 0; z-index: 2147483647; display: grid; width: 100vw; height: 100dvh; place-items: center; color: var(--text); background: var(--shell); } +.print-loading-spinner { width: 48px; height: 48px; box-sizing: border-box; border: solid 4px currentColor; border-top-color: transparent; border-radius: 9999px; animation: refresh-spin .8s linear infinite; } @media (max-width: 820px) { .app-shell { display: block; } @@ -186,7 +188,7 @@ button { color: inherit; } .content a, .content figcaption, .content blockquote { color: #000 !important; } .content pre, .content code, .content details, .content th, .callout { color: #000 !important; background: #fff !important; border-color: #777 !important; } .app-shell { display: block; } - .desktop-header, .desktop-sidebar, .mobile-dock, .page-navigation { display: none; } + .desktop-header, .desktop-sidebar, .mobile-dock, .page-navigation, .print-loading-overlay { display: none !important; } .workspace, .page-surface { display: block; height: auto; overflow: visible; } .content { width: 100%; padding: 0; } body.docslib-print-all > .app-shell { display: none; } @@ -194,6 +196,9 @@ button { color: inherit; } .print-page { break-after: page; page-break-after: always; } .print-page:last-child { break-after: auto; page-break-after: auto; } .print-page-heading { margin: 0 0 2rem; padding-bottom: 1rem; border-bottom: 1px solid #777; } - .print-page-heading h1 { margin: 0; color: #000 !important; } + .print-page-heading h1 { margin: 0; color: #000 !important; font-size: 1.35rem !important; line-height: 1.25; } .print-page-number { display: block; margin-bottom: .3rem; color: #555 !important; } + .print-page-content h1 { font-size: 1.8rem !important; line-height: 1.25; } + .print-page-content h2 { font-size: 1.4rem !important; line-height: 1.3; } + .print-page-content h3 { font-size: 1.15rem !important; line-height: 1.35; } } diff --git a/docslib/templates/client.js b/docslib/templates/client.js index df90dd2..dd962f2 100644 --- a/docslib/templates/client.js +++ b/docslib/templates/client.js @@ -226,11 +226,14 @@ return asset ? `data:${asset.mime};base64,${asset.data}` : ""; } - function hydrateAssets(root = elements.content) { + function hydrateAssets(root = elements.content, { eager = false } = {}) { root.querySelectorAll("[data-docslib-asset]").forEach((element) => { const name = element.dataset.docslibAsset; const uri = assetDataUri(name); - if (uri && element.tagName === "IMG") element.src = uri; + if (uri && element.tagName === "IMG") { + if (eager) element.loading = "eager"; + element.src = uri; + } else if (!uri) element.setAttribute("title", name); }); } @@ -240,37 +243,76 @@ document.getElementById("docslib-full-print")?.remove(); } - function PrintFullDocumentation() { + function showPrintLoading() { + document.getElementById("docslib-print-loading")?.remove(); + const overlay = document.createElement("div"); + overlay.id = "docslib-print-loading"; + overlay.className = "print-loading-overlay"; + overlay.setAttribute("aria-hidden", "true"); + const spinner = document.createElement("div"); + spinner.className = "print-loading-spinner"; + overlay.append(spinner); + document.body.append(overlay); + return overlay; + } + + async function waitForPrintAssets(root) { + const images = [...root.querySelectorAll("img")]; + await Promise.all(images.map(async (image) => { + if (!image.complete) { + await new Promise((resolve) => { + image.addEventListener("load", resolve, { once: true }); + image.addEventListener("error", resolve, { once: true }); + }); + } + if (typeof image.decode === "function" && image.naturalWidth > 0) { + await image.decode().catch(() => {}); + } + })); + if (document.fonts && document.fonts.ready) await document.fonts.ready; + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + } + + async function PrintFullDocumentation() { + const overlay = showPrintLoading(); removeFullPrintDocument(); - const printable = document.createElement("div"); - printable.id = "docslib-full-print"; - printable.className = "full-print-document"; + try { + const printable = document.createElement("div"); + printable.id = "docslib-full-print"; + printable.className = "full-print-document"; - manifest.pages.forEach((page) => { - const article = document.createElement("article"); - article.className = "content print-page"; - article.dataset.page = page.number; + manifest.pages.forEach((page) => { + const article = document.createElement("article"); + article.className = "content print-page"; + article.dataset.page = page.number; - const heading = document.createElement("header"); - heading.className = "print-page-heading"; - const number = document.createElement("span"); - number.className = "print-page-number"; - number.textContent = page.number; - const title = document.createElement("h1"); - title.textContent = page.title; - heading.append(number, title); + const heading = document.createElement("header"); + heading.className = "print-page-heading"; + const number = document.createElement("span"); + number.className = "print-page-number"; + number.textContent = page.number; + const title = document.createElement("h1"); + title.textContent = page.title; + heading.append(number, title); - const body = document.createElement("div"); - body.className = "print-page-content"; - body.innerHTML = page.content || ""; - article.append(heading, body); - hydrateAssets(article); - printable.append(article); - }); + const body = document.createElement("div"); + body.className = "print-page-content"; + body.innerHTML = page.content || ""; + article.append(heading, body); + hydrateAssets(article, { eager: true }); + printable.append(article); + }); - document.body.append(printable); - document.body.classList.add("docslib-print-all"); - requestAnimationFrame(() => requestAnimationFrame(() => window.print())); + document.body.append(printable); + await waitForPrintAssets(printable); + document.body.classList.add("docslib-print-all"); + window.print(); + } catch (error) { + removeFullPrintDocument(); + throw error; + } finally { + overlay.remove(); + } } globalThis.PrintFullDocumentation = PrintFullDocumentation; diff --git a/tests/test_server.py b/tests/test_server.py index 23b7189..46105fa 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -97,7 +97,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert html_response.status_code == 200 assert "http://127.0.0.1:9000/manifest.json" in html assert "http://127.0.0.1:9000/version" in html - assert 'globalThis.COMPILED_FROM_VERSION = "0.3.8";' in html + assert 'globalThis.COMPILED_FROM_VERSION = "0.3.9";' in html assert ".content { --custom-test: yes; }" in html assert r'document.body.dataset.customTest = \"yes\";' in html assert 'id="docslib-loading"' in html @@ -160,10 +160,20 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert 'printable.id = "docslib-full-print"' in system_js assert "manifest.pages.forEach((page) =>" in system_js assert 'window.addEventListener("afterprint", removeFullPrintDocument)' in system_js + assert "async function PrintFullDocumentation()" in system_js + assert "await waitForPrintAssets(printable)" in system_js + assert 'if (eager) element.loading = "eager"' in system_js + assert "hydrateAssets(article, { eager: true })" in system_js + assert 'image.addEventListener("load", resolve, { once: true })' in system_js + assert 'await image.decode().catch(() => {})' in system_js + assert "await document.fonts.ready" in system_js system_css = manifest["client"]["css"]["system"] assert 'body.docslib-print-all > .full-print-document { display: block; }' in system_css assert 'color: #000 !important; background: #fff !important;' in system_css assert ".print-page { break-after: page; page-break-after: always; }" in system_css + assert ".print-loading-overlay { position: fixed; inset: 0; z-index: 2147483647; display: grid; width: 100vw; height: 100dvh" in system_css + assert ".print-page-heading h1 { margin: 0; color: #000 !important; font-size: 1.35rem !important" in system_css + assert ".print-page-content h1 { font-size: 1.8rem !important" in system_css assert "--shell: #000000" in html assert 'id="desktop-search-input"' in html assert '
' in html @@ -208,7 +218,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): def test_build_writes_static_bundle(tmp_path): - assert __version__ == "0.3.8" + assert __version__ == "0.3.9" @Page("1", "Static page") def static_page():