mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
aa860a1402
commit
425c01dde5
3 files changed
+74
-8
No files matched your search
@@ -33,6 +33,7 @@
|
|||||||
<button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button>
|
<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="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="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>
|
<button id="annotationButton" class="button" type="button" data-short="注" title="渲染注解" aria-label="渲染注解" aria-pressed="true">注解</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tool-group">
|
<div class="tool-group">
|
||||||
|
|||||||
@@ -513,6 +513,11 @@ body[data-hide-meta] .meta-panel {
|
|||||||
padding-block: var(--fit-space, var(--page-space));
|
padding-block: var(--fit-space, var(--page-space));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-frame.continuous {
|
||||||
|
--page-space: 0px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
.svg-host {
|
.svg-host {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
@@ -529,6 +534,11 @@ body[data-hide-meta] .meta-panel {
|
|||||||
background: var(--page);
|
background: var(--page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-frame.continuous .page-shell {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.page-shell.error {
|
.page-shell.error {
|
||||||
background: #fff8f7;
|
background: #fff8f7;
|
||||||
}
|
}
|
||||||
@@ -910,6 +920,7 @@ body[data-hide-meta] .meta-panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-footer {
|
.app-footer {
|
||||||
|
z-index: 12;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
|
|||||||
+62
-8
@@ -62,6 +62,7 @@ const state = {
|
|||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
fitMode: "width",
|
fitMode: "width",
|
||||||
|
continuous: false,
|
||||||
renderAnnotations: true,
|
renderAnnotations: true,
|
||||||
pageCache: new Map(),
|
pageCache: new Map(),
|
||||||
pageInFlight: new Map(),
|
pageInFlight: new Map(),
|
||||||
@@ -98,6 +99,7 @@ const el = {
|
|||||||
zoomLabel: document.querySelector("#zoomLabel"),
|
zoomLabel: document.querySelector("#zoomLabel"),
|
||||||
fitButton: document.querySelector("#fitButton"),
|
fitButton: document.querySelector("#fitButton"),
|
||||||
fitHeightButton: document.querySelector("#fitHeightButton"),
|
fitHeightButton: document.querySelector("#fitHeightButton"),
|
||||||
|
continuousButton: document.querySelector("#continuousButton"),
|
||||||
annotationButton: document.querySelector("#annotationButton"),
|
annotationButton: document.querySelector("#annotationButton"),
|
||||||
imageDPI: document.querySelector("#imageDPI"),
|
imageDPI: document.querySelector("#imageDPI"),
|
||||||
pageExportFormat: document.querySelector("#pageExportFormat"),
|
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.zoomInButton.addEventListener("click", () => setScale(state.scale + 0.1));
|
||||||
el.fitButton.addEventListener("click", fitWidth);
|
el.fitButton.addEventListener("click", fitWidth);
|
||||||
el.fitHeightButton.addEventListener("click", fitHeight);
|
el.fitHeightButton.addEventListener("click", fitHeight);
|
||||||
|
el.continuousButton.addEventListener("click", toggleContinuous);
|
||||||
el.annotationButton.addEventListener("click", toggleAnnotations);
|
el.annotationButton.addEventListener("click", toggleAnnotations);
|
||||||
el.pageExportFormat.addEventListener("change", () => updateDPIControl());
|
el.pageExportFormat.addEventListener("change", () => updateDPIControl());
|
||||||
el.exportPageButton.addEventListener("click", exportCurrentPage);
|
el.exportPageButton.addEventListener("click", exportCurrentPage);
|
||||||
@@ -1121,6 +1124,7 @@ async function exportCurrentPage() {
|
|||||||
|
|
||||||
function renderPageFlow() {
|
function renderPageFlow() {
|
||||||
el.svgHost.replaceChildren();
|
el.svgHost.replaceChildren();
|
||||||
|
el.viewerPanel.classList.remove("single-page-fits-height");
|
||||||
if (!state.doc) {
|
if (!state.doc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1374,7 +1378,7 @@ function pageShell(index) {
|
|||||||
function scrollToPage(index) {
|
function scrollToPage(index) {
|
||||||
const shell = pageShell(index);
|
const shell = pageShell(index);
|
||||||
if (shell) {
|
if (shell) {
|
||||||
if (state.fitMode === "height") {
|
if (state.fitMode === "height" && !state.continuous) {
|
||||||
shell.scrollIntoView({ block: "center", inline: "nearest" });
|
shell.scrollIntoView({ block: "center", inline: "nearest" });
|
||||||
} else {
|
} else {
|
||||||
const viewerRect = el.viewerPanel.getBoundingClientRect();
|
const viewerRect = el.viewerPanel.getBoundingClientRect();
|
||||||
@@ -1418,6 +1422,16 @@ function syncCurrentPageFromScroll() {
|
|||||||
function pageShellFromView() {
|
function pageShellFromView() {
|
||||||
const rect = el.viewerPanel.getBoundingClientRect();
|
const rect = el.viewerPanel.getBoundingClientRect();
|
||||||
const x = rect.left + rect.width / 2;
|
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)
|
return pageShellAtPoint(x, rect.top + rect.height * 0.45)
|
||||||
|| pageShellAtPoint(x, rect.top + rect.height * 0.25)
|
|| pageShellAtPoint(x, rect.top + rect.height * 0.25)
|
||||||
|| pageShellAtPoint(x, rect.top + rect.height * 0.65);
|
|| pageShellAtPoint(x, rect.top + rect.height * 0.65);
|
||||||
@@ -2069,6 +2083,20 @@ function fontMeta(font) {
|
|||||||
return parts.join(" · ");
|
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) {
|
function fitWidth(updateStatus = true) {
|
||||||
const page = currentPageInfo();
|
const page = currentPageInfo();
|
||||||
if (!page) {
|
if (!page) {
|
||||||
@@ -2083,11 +2111,15 @@ function fitWidth(updateStatus = true) {
|
|||||||
function fitWidthScale(page) {
|
function fitWidthScale(page) {
|
||||||
const space = pageSpace();
|
const space = pageSpace();
|
||||||
const width = Math.max(1, page.width * MM_TO_PX);
|
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);
|
let available = Math.max(1, el.viewerPanel.clientWidth - space * 2);
|
||||||
if (!viewerHasVerticalScrollbar() && height * (available / width) > Math.max(1, el.viewerPanel.clientHeight - space * 2)) {
|
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());
|
available = Math.max(1, available - scrollbarWidth());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Math.min(4, Math.max(0.2, available / width));
|
return Math.min(4, Math.max(0.2, available / width));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2097,10 +2129,32 @@ function fitHeight(updateStatus = true) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const space = pageSpace();
|
const space = pageSpace();
|
||||||
const availableWidth = Math.max(1, el.viewerPanel.clientWidth - space * 2);
|
let availableWidth = Math.max(1, el.viewerPanel.clientWidth - space * 2);
|
||||||
const availableHeight = Math.max(1, el.viewerPanel.clientHeight - space * 2);
|
let availableHeight = Math.max(1, el.viewerPanel.clientHeight - space * 2);
|
||||||
const width = Math.max(1, page.width * MM_TO_PX);
|
const width = Math.max(1, page.width * MM_TO_PX);
|
||||||
const height = Math.max(1, page.height * 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");
|
setScale(Math.min(availableWidth / width, availableHeight / height), updateStatus, "height");
|
||||||
if (updateStatus) {
|
if (updateStatus) {
|
||||||
scrollToPage(state.pageIndex);
|
scrollToPage(state.pageIndex);
|
||||||
@@ -2120,8 +2174,7 @@ function applyFit(updateStatus = true) {
|
|||||||
function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
||||||
const anchor = updateStatus && fitMode === "free" ? scaleAnchor() : null;
|
const anchor = updateStatus && fitMode === "free" ? scaleAnchor() : null;
|
||||||
const scale = Math.min(4, Math.max(0.2, nextScale));
|
const scale = Math.min(4, Math.max(0.2, nextScale));
|
||||||
const scaleChanged = Math.abs(scale - state.scale) > 0.001;
|
const layoutChanged = scale !== state.scale || fitMode !== state.fitMode;
|
||||||
const layoutChanged = scaleChanged || fitMode !== state.fitMode;
|
|
||||||
state.fitMode = fitMode;
|
state.fitMode = fitMode;
|
||||||
state.scale = scale;
|
state.scale = scale;
|
||||||
if (layoutChanged) {
|
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 height = shell ? shell.getBoundingClientRect().height : Math.max(1, page.height * MM_TO_PX * state.scale);
|
||||||
const base = pageSpace();
|
const base = pageSpace();
|
||||||
el.viewerPanel.classList.toggle("single-page-fits-height", state.doc.pageCount === 1 && height <= el.viewerPanel.clientHeight - base * 2);
|
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;
|
const gap = space > base ? space + 1 : space;
|
||||||
el.pageFrame.style.setProperty("--fit-space", `${space}px`);
|
el.pageFrame.style.setProperty("--fit-space", `${space}px`);
|
||||||
el.pageFrame.style.setProperty("--fit-gap", `${gap}px`);
|
el.pageFrame.style.setProperty("--fit-gap", `${gap}px`);
|
||||||
@@ -2226,6 +2279,7 @@ function updateControls() {
|
|||||||
el.zoomInButton.disabled = !hasDoc || state.scale >= 4;
|
el.zoomInButton.disabled = !hasDoc || state.scale >= 4;
|
||||||
el.fitButton.disabled = !hasDoc;
|
el.fitButton.disabled = !hasDoc;
|
||||||
el.fitHeightButton.disabled = !hasDoc;
|
el.fitHeightButton.disabled = !hasDoc;
|
||||||
|
el.continuousButton.disabled = !hasDoc;
|
||||||
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
|
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
|
||||||
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
|
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
|
||||||
updateAnnotationButton();
|
updateAnnotationButton();
|
||||||
|
|||||||
Reference in new issue
Block a user