mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
aa860a1402
commit
425c01dde5
3 files changed
+75
-9
No files matched your search
@@ -33,6 +33,7 @@
|
||||
<button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button>
|
||||
<button id="fitButton" class="button" type="button" data-short="宽" aria-label="适宽" disabled>适宽</button>
|
||||
<button id="fitHeightButton" class="button" type="button" data-short="高" aria-label="适高" disabled>适高</button>
|
||||
<button id="continuousButton" class="button" type="button" data-short="连" title="无间距连续显示" aria-label="无间距连续显示" aria-pressed="false" disabled>连续</button>
|
||||
<button id="annotationButton" class="button" type="button" data-short="注" title="渲染注解" aria-label="渲染注解" aria-pressed="true">注解</button>
|
||||
</div>
|
||||
<div class="tool-group">
|
||||
|
||||
@@ -513,6 +513,11 @@ body[data-hide-meta] .meta-panel {
|
||||
padding-block: var(--fit-space, var(--page-space));
|
||||
}
|
||||
|
||||
.page-frame.continuous {
|
||||
--page-space: 0px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.svg-host {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
@@ -529,6 +534,11 @@ body[data-hide-meta] .meta-panel {
|
||||
background: var(--page);
|
||||
}
|
||||
|
||||
.page-frame.continuous .page-shell {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.page-shell.error {
|
||||
background: #fff8f7;
|
||||
}
|
||||
@@ -910,6 +920,7 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
z-index: 12;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--line);
|
||||
|
||||
+63
-9
@@ -62,6 +62,7 @@ const state = {
|
||||
pageIndex: 0,
|
||||
scale: 1,
|
||||
fitMode: "width",
|
||||
continuous: false,
|
||||
renderAnnotations: true,
|
||||
pageCache: new Map(),
|
||||
pageInFlight: new Map(),
|
||||
@@ -98,6 +99,7 @@ const el = {
|
||||
zoomLabel: document.querySelector("#zoomLabel"),
|
||||
fitButton: document.querySelector("#fitButton"),
|
||||
fitHeightButton: document.querySelector("#fitHeightButton"),
|
||||
continuousButton: document.querySelector("#continuousButton"),
|
||||
annotationButton: document.querySelector("#annotationButton"),
|
||||
imageDPI: document.querySelector("#imageDPI"),
|
||||
pageExportFormat: document.querySelector("#pageExportFormat"),
|
||||
@@ -145,6 +147,7 @@ el.zoomOutButton.addEventListener("click", () => setScale(state.scale - 0.1));
|
||||
el.zoomInButton.addEventListener("click", () => setScale(state.scale + 0.1));
|
||||
el.fitButton.addEventListener("click", fitWidth);
|
||||
el.fitHeightButton.addEventListener("click", fitHeight);
|
||||
el.continuousButton.addEventListener("click", toggleContinuous);
|
||||
el.annotationButton.addEventListener("click", toggleAnnotations);
|
||||
el.pageExportFormat.addEventListener("change", () => updateDPIControl());
|
||||
el.exportPageButton.addEventListener("click", exportCurrentPage);
|
||||
@@ -1121,6 +1124,7 @@ async function exportCurrentPage() {
|
||||
|
||||
function renderPageFlow() {
|
||||
el.svgHost.replaceChildren();
|
||||
el.viewerPanel.classList.remove("single-page-fits-height");
|
||||
if (!state.doc) {
|
||||
return;
|
||||
}
|
||||
@@ -1374,7 +1378,7 @@ function pageShell(index) {
|
||||
function scrollToPage(index) {
|
||||
const shell = pageShell(index);
|
||||
if (shell) {
|
||||
if (state.fitMode === "height") {
|
||||
if (state.fitMode === "height" && !state.continuous) {
|
||||
shell.scrollIntoView({ block: "center", inline: "nearest" });
|
||||
} else {
|
||||
const viewerRect = el.viewerPanel.getBoundingClientRect();
|
||||
@@ -1418,6 +1422,16 @@ function syncCurrentPageFromScroll() {
|
||||
function pageShellFromView() {
|
||||
const rect = el.viewerPanel.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
if (state.continuous) {
|
||||
if (el.viewerPanel.scrollTop + el.viewerPanel.clientHeight >= el.viewerPanel.scrollHeight - 1) {
|
||||
// 末尾无法顶对齐时保留完整可见的当前页
|
||||
const shell = pageShell(state.pageIndex);
|
||||
const bounds = shell?.getBoundingClientRect();
|
||||
return bounds && bounds.top >= rect.top - 1 && bounds.bottom <= rect.bottom + 1
|
||||
? shell : el.svgHost.lastElementChild;
|
||||
}
|
||||
return pageShellAtPoint(x, rect.top + 1);
|
||||
}
|
||||
return pageShellAtPoint(x, rect.top + rect.height * 0.45)
|
||||
|| pageShellAtPoint(x, rect.top + rect.height * 0.25)
|
||||
|| pageShellAtPoint(x, rect.top + rect.height * 0.65);
|
||||
@@ -2069,6 +2083,20 @@ function fontMeta(font) {
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
function toggleContinuous() {
|
||||
const anchor = state.fitMode === "free" ? scaleAnchor() : null;
|
||||
state.continuous = !state.continuous;
|
||||
el.pageFrame.classList.toggle("continuous", state.continuous);
|
||||
el.continuousButton.setAttribute("aria-pressed", String(state.continuous));
|
||||
clearStampHighlights();
|
||||
if (state.fitMode === "free") {
|
||||
restoreScaleAnchor(anchor);
|
||||
} else {
|
||||
applyFit(false);
|
||||
scrollToPage(state.pageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function fitWidth(updateStatus = true) {
|
||||
const page = currentPageInfo();
|
||||
if (!page) {
|
||||
@@ -2083,10 +2111,14 @@ function fitWidth(updateStatus = true) {
|
||||
function fitWidthScale(page) {
|
||||
const space = pageSpace();
|
||||
const width = Math.max(1, page.width * MM_TO_PX);
|
||||
const height = Math.max(1, page.height * MM_TO_PX);
|
||||
let available = Math.max(1, el.viewerPanel.clientWidth - space * 2);
|
||||
if (!viewerHasVerticalScrollbar() && height * (available / width) > Math.max(1, el.viewerPanel.clientHeight - space * 2)) {
|
||||
available = Math.max(1, available - scrollbarWidth());
|
||||
if (!viewerHasVerticalScrollbar()) {
|
||||
const height = state.continuous
|
||||
? state.doc.pages.reduce((total, item) => total + available * item.height / item.width, 0)
|
||||
: Math.max(1, page.height * MM_TO_PX) * (available / width);
|
||||
if (height > Math.max(1, el.viewerPanel.clientHeight - space * 2)) {
|
||||
available = Math.max(1, available - scrollbarWidth());
|
||||
}
|
||||
}
|
||||
return Math.min(4, Math.max(0.2, available / width));
|
||||
}
|
||||
@@ -2097,10 +2129,32 @@ function fitHeight(updateStatus = true) {
|
||||
return;
|
||||
}
|
||||
const space = pageSpace();
|
||||
const availableWidth = Math.max(1, el.viewerPanel.clientWidth - space * 2);
|
||||
const availableHeight = Math.max(1, el.viewerPanel.clientHeight - space * 2);
|
||||
let availableWidth = Math.max(1, el.viewerPanel.clientWidth - space * 2);
|
||||
let availableHeight = Math.max(1, el.viewerPanel.clientHeight - space * 2);
|
||||
const width = Math.max(1, page.width * MM_TO_PX);
|
||||
const height = Math.max(1, page.height * MM_TO_PX);
|
||||
if (state.continuous) {
|
||||
availableWidth = Math.max(1, el.viewerPanel.offsetWidth);
|
||||
availableHeight = Math.max(1, el.viewerPanel.offsetHeight);
|
||||
const contentWidth = state.doc.pages.reduce((max, item) => Math.max(max, item.width), 0) * MM_TO_PX;
|
||||
const contentHeight = state.doc.pages.reduce((total, item) => total + item.height, 0) * MM_TO_PX;
|
||||
const scale = Math.min(availableWidth / width, availableHeight / height);
|
||||
const vertical = scale > availableHeight / contentHeight;
|
||||
const horizontal = scale > availableWidth / contentWidth;
|
||||
if (vertical || horizontal) {
|
||||
const scrollbar = scrollbarWidth();
|
||||
availableWidth = Math.max(1, availableWidth - (vertical ? scrollbar : 0));
|
||||
availableHeight = Math.max(1, availableHeight - (horizontal ? scrollbar : 0));
|
||||
// 一侧滚动条占用空间后,另一侧也可能需要滚动条
|
||||
const nextScale = Math.min(availableWidth / width, availableHeight / height);
|
||||
if (!vertical && nextScale > availableHeight / contentHeight) {
|
||||
availableWidth = Math.max(1, availableWidth - scrollbar);
|
||||
}
|
||||
if (!horizontal && nextScale > availableWidth / contentWidth) {
|
||||
availableHeight = Math.max(1, availableHeight - scrollbar);
|
||||
}
|
||||
}
|
||||
}
|
||||
setScale(Math.min(availableWidth / width, availableHeight / height), updateStatus, "height");
|
||||
if (updateStatus) {
|
||||
scrollToPage(state.pageIndex);
|
||||
@@ -2120,8 +2174,7 @@ function applyFit(updateStatus = true) {
|
||||
function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
||||
const anchor = updateStatus && fitMode === "free" ? scaleAnchor() : null;
|
||||
const scale = Math.min(4, Math.max(0.2, nextScale));
|
||||
const scaleChanged = Math.abs(scale - state.scale) > 0.001;
|
||||
const layoutChanged = scaleChanged || fitMode !== state.fitMode;
|
||||
const layoutChanged = scale !== state.scale || fitMode !== state.fitMode;
|
||||
state.fitMode = fitMode;
|
||||
state.scale = scale;
|
||||
if (layoutChanged) {
|
||||
@@ -2208,7 +2261,7 @@ function updateFitSpace() {
|
||||
const height = shell ? shell.getBoundingClientRect().height : Math.max(1, page.height * MM_TO_PX * state.scale);
|
||||
const base = pageSpace();
|
||||
el.viewerPanel.classList.toggle("single-page-fits-height", state.doc.pageCount === 1 && height <= el.viewerPanel.clientHeight - base * 2);
|
||||
const space = Math.max(base, (el.viewerPanel.clientHeight - height) / 2);
|
||||
const space = state.continuous ? 0 : Math.max(base, (el.viewerPanel.clientHeight - height) / 2);
|
||||
const gap = space > base ? space + 1 : space;
|
||||
el.pageFrame.style.setProperty("--fit-space", `${space}px`);
|
||||
el.pageFrame.style.setProperty("--fit-gap", `${gap}px`);
|
||||
@@ -2226,6 +2279,7 @@ function updateControls() {
|
||||
el.zoomInButton.disabled = !hasDoc || state.scale >= 4;
|
||||
el.fitButton.disabled = !hasDoc;
|
||||
el.fitHeightButton.disabled = !hasDoc;
|
||||
el.continuousButton.disabled = !hasDoc;
|
||||
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
|
||||
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
|
||||
updateAnnotationButton();
|
||||
|
||||
Reference in new issue
Block a user