feat: make viewer zoom responsive
This commit is contained in:
+67
-8
@@ -2888,9 +2888,10 @@ def render_main_html(data: dict) -> str:
|
||||
css = r"""
|
||||
:root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000;--viewer-canvas-scale:1;--viewer-content-scale:1}
|
||||
*{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;background:var(--desk);color:var(--ink);font-family:"Latin Modern Roman","LM Roman 12",Georgia,"Times New Roman",serif;line-height:1.25}
|
||||
.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:8px 14px;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
|
||||
.topbar-inner{max-width:1120px;margin:0 auto;display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.toc-links a{font-size:13px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:4px;padding:3px 7px}.viewer-zoom-status{flex:none;font:600 12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap}
|
||||
.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:0;overflow:hidden;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--viewer-content-scale));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm}
|
||||
.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:3px 10px;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
|
||||
.topbar-inner{max-width:1120px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:12px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-size:12px;line-height:1.35;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px}.toc-links a{font-size:11px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:3px;padding:2px 5px}.viewer-zoom-controls{display:flex;align-items:center;gap:7px;flex:none}.viewer-zoom-status{font:600 10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap}.viewer-zoom-reset{border:1px solid #bbb;border-radius:3px;background:#fff;color:#333;padding:1px 6px;font:600 10px/1.35 system-ui,-apple-system,Segoe UI,sans-serif;cursor:pointer}.viewer-zoom-reset:hover{background:#eee}.viewer-zoom-reset:focus-visible{outline:2px solid #555;outline-offset:1px}
|
||||
.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;height:297mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:0;overflow:hidden;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--sheet-effective-scale,1));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm}
|
||||
@media(max-width:900px){.sheet{height:auto}}@media print{.sheet{height:auto}}
|
||||
.pdf-main{min-width:0;width:100%}.running-header{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;border-bottom:1px solid #111;padding-bottom:3px;margin-bottom:26px;font-size:15px}.running-header span{white-space:nowrap}
|
||||
.hero h1{font-size:24px;line-height:1.15;margin:0 0 4px;border-bottom:1px solid #111;padding-bottom:4px}.hero .byline{float:right;margin-top:-31px;font-size:15px}
|
||||
.meta{clear:both;display:grid;grid-template-columns:148px minmax(0,1fr);gap:4px 18px;padding:34px 0 24px;border-bottom:1px solid #111;font-size:17px}.meta dt{color:#333}.meta dd{margin:0;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}
|
||||
@@ -2951,6 +2952,11 @@ document.addEventListener('click', event => {
|
||||
const viewerRoot = document.documentElement;
|
||||
const viewerPaper = document.querySelector('.paper');
|
||||
const viewerZoomStatus = document.getElementById('viewer-zoom-status');
|
||||
const viewerZoomReset = document.getElementById('viewer-zoom-reset');
|
||||
const viewerSheets = Array.from(document.querySelectorAll('.sheet')).map(sheet => ({
|
||||
sheet,
|
||||
content: sheet.querySelector('.sheet-content')
|
||||
})).filter(item => item.content);
|
||||
let viewerCanvasScale = 1;
|
||||
let viewerContentScale = 1;
|
||||
function clampViewerScale(value, minimum, maximum){
|
||||
@@ -2960,12 +2966,45 @@ function nextViewerScale(current, delta, minimum, maximum){
|
||||
const boundedDelta = Math.min(120, Math.max(-120, delta));
|
||||
return clampViewerScale(current * Math.exp(-boundedDelta * 0.001), minimum, maximum);
|
||||
}
|
||||
function viewerCanvasMaximum(){
|
||||
if(!viewerPaper || !viewerSheets.length){ return 1; }
|
||||
const paperStyle = getComputedStyle(viewerPaper);
|
||||
const availableWidth = viewerPaper.clientWidth
|
||||
- parseFloat(paperStyle.paddingLeft || 0)
|
||||
- parseFloat(paperStyle.paddingRight || 0);
|
||||
const renderedWidth = viewerSheets[0].sheet.getBoundingClientRect().width;
|
||||
const baseWidth = renderedWidth / Math.max(viewerCanvasScale, 0.001);
|
||||
return Math.max(0.1, availableWidth / Math.max(baseWidth, 1));
|
||||
}
|
||||
function updateViewerAutoFit(){
|
||||
for(const {sheet, content} of viewerSheets){
|
||||
const widthFit = sheet.clientWidth / Math.max(content.scrollWidth, 1);
|
||||
const heightFit = sheet.clientHeight / Math.max(content.scrollHeight, 1);
|
||||
content.dataset.viewerAutoFit = Math.min(1, widthFit, heightFit).toFixed(4);
|
||||
}
|
||||
}
|
||||
function applyViewerScale(){
|
||||
viewerRoot.style.setProperty('--viewer-canvas-scale', viewerCanvasScale.toFixed(3));
|
||||
viewerRoot.style.setProperty('--viewer-content-scale', viewerContentScale.toFixed(3));
|
||||
if(viewerZoomStatus){
|
||||
viewerZoomStatus.textContent = `Płótno ${Math.round(viewerCanvasScale * 100)}% · Zawartość ${Math.round(viewerContentScale * 100)}%`;
|
||||
let usesAutoFit = false;
|
||||
for(const {content} of viewerSheets){
|
||||
const autoFit = Number(content.dataset.viewerAutoFit || 1);
|
||||
usesAutoFit ||= autoFit < 0.999;
|
||||
content.style.setProperty('--sheet-effective-scale', (autoFit * viewerContentScale).toFixed(4));
|
||||
}
|
||||
if(viewerZoomStatus){
|
||||
viewerZoomStatus.textContent = `Płótno ${Math.round(viewerCanvasScale * 100)}% · Treść ${Math.round(viewerContentScale * 100)}%${usesAutoFit ? ' · auto-fit' : ''}`;
|
||||
}
|
||||
}
|
||||
function refreshViewerLayout(){
|
||||
updateViewerAutoFit();
|
||||
viewerCanvasScale = Math.min(viewerCanvasScale, viewerCanvasMaximum());
|
||||
applyViewerScale();
|
||||
}
|
||||
let viewerRefreshFrame = 0;
|
||||
function scheduleViewerRefresh(){
|
||||
cancelAnimationFrame(viewerRefreshFrame);
|
||||
viewerRefreshFrame = requestAnimationFrame(refreshViewerLayout);
|
||||
}
|
||||
if(viewerPaper){
|
||||
viewerPaper.addEventListener('wheel', event => {
|
||||
@@ -2975,14 +3014,34 @@ if(viewerPaper){
|
||||
if(!delta){ return; }
|
||||
event.preventDefault();
|
||||
if(event.altKey){
|
||||
viewerCanvasScale = nextViewerScale(viewerCanvasScale, delta, 0.4, 2.0);
|
||||
const maximum = viewerCanvasMaximum();
|
||||
viewerCanvasScale = nextViewerScale(
|
||||
viewerCanvasScale,
|
||||
delta,
|
||||
Math.min(0.25, maximum),
|
||||
maximum
|
||||
);
|
||||
}else{
|
||||
viewerContentScale = nextViewerScale(viewerContentScale, delta, 0.5, 2.0);
|
||||
}
|
||||
applyViewerScale();
|
||||
}, {passive:false});
|
||||
}
|
||||
applyViewerScale();
|
||||
if(viewerZoomReset){
|
||||
viewerZoomReset.addEventListener('click', () => {
|
||||
viewerCanvasScale = Math.min(1, viewerCanvasMaximum());
|
||||
viewerContentScale = 1;
|
||||
refreshViewerLayout();
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', scheduleViewerRefresh);
|
||||
window.visualViewport?.addEventListener('resize', scheduleViewerRefresh);
|
||||
if('ResizeObserver' in window){
|
||||
const viewerResizeObserver = new ResizeObserver(scheduleViewerRefresh);
|
||||
if(viewerPaper){ viewerResizeObserver.observe(viewerPaper); }
|
||||
for(const {content} of viewerSheets){ viewerResizeObserver.observe(content); }
|
||||
}
|
||||
refreshViewerLayout();
|
||||
"""
|
||||
inspector_html = (
|
||||
'<aside class="inspector"><h2>Opis kodu</h2><div id="tree-inspector">'
|
||||
@@ -3008,7 +3067,7 @@ applyViewerScale();
|
||||
</head>
|
||||
<body class="{'resource-header-enabled' if use_resource_header else ''}">
|
||||
<script type="application/json" id="tree-data">{tree_data}</script>
|
||||
<nav class="topbar"><div class="topbar-inner"><details><summary>Spis treści</summary><div class="toc-links">{toc}</div></details><output class="viewer-zoom-status" id="viewer-zoom-status" aria-live="polite" title="Alt + rolka: płótno; Shift + rolka: zawartość">Płótno 100% · Zawartość 100%</output></div></nav>
|
||||
<nav class="topbar"><div class="topbar-inner"><details><summary>Spis treści</summary><div class="toc-links">{toc}</div></details><div class="viewer-zoom-controls"><output class="viewer-zoom-status" id="viewer-zoom-status" aria-live="polite" title="Alt + rolka: płótno; Shift + rolka: zawartość">Płótno 100% · Zawartość 100%</output><button class="viewer-zoom-reset" id="viewer-zoom-reset" type="button">Reset zoom</button></div></div></nav>
|
||||
<main class="paper">
|
||||
<article class="sheet front-sheet">
|
||||
<div class="sheet-content">
|
||||
|
||||
Reference in New Issue
Block a user