mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
f0094e2af6
commit
20d8dcebde
3 files changed
+173
-19
No files matched your search
@@ -46,6 +46,7 @@
|
|||||||
<button id="fitHeightButton" class="button" type="button" data-short="高" disabled>适高</button>
|
<button id="fitHeightButton" class="button" type="button" data-short="高" disabled>适高</button>
|
||||||
<button id="rotateButton" class="button" type="button" data-short="转" disabled>旋转</button>
|
<button id="rotateButton" class="button" type="button" data-short="转" disabled>旋转</button>
|
||||||
<button id="panButton" class="button" type="button" data-short="拖" data-pressed="false" disabled>拖动</button>
|
<button id="panButton" class="button" type="button" data-short="拖" data-pressed="false" disabled>拖动</button>
|
||||||
|
<button id="pageLayoutButton" class="button" type="button" data-short="一" disabled>一页</button>
|
||||||
<button id="continuousButton" class="button" type="button" data-short="连" data-pressed="false" disabled>连续</button>
|
<button id="continuousButton" class="button" type="button" data-short="连" data-pressed="false" disabled>连续</button>
|
||||||
<button id="annotationButton" class="button" type="button" data-short="注" data-pressed="true">注解</button>
|
<button id="annotationButton" class="button" type="button" data-short="注" data-pressed="true">注解</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+164
-16
@@ -7,6 +7,7 @@ const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
|
|||||||
const DEFAULT_IMAGE_DPI = 300;
|
const DEFAULT_IMAGE_DPI = 300;
|
||||||
const PAGE_CACHE_LIMIT = 16;
|
const PAGE_CACHE_LIMIT = 16;
|
||||||
const PAGE_CACHE_BYTES = 32 * 1024 * 1024;
|
const PAGE_CACHE_BYTES = 32 * 1024 * 1024;
|
||||||
|
const PAGE_LAYOUT_GAP = 16;
|
||||||
const SCROLL_SEGMENT_SIZE = 1000000;
|
const SCROLL_SEGMENT_SIZE = 1000000;
|
||||||
const OBJECT_CLIPBOARD_TYPE = "application/x-ofdgo-objects";
|
const OBJECT_CLIPBOARD_TYPE = "application/x-ofdgo-objects";
|
||||||
const STATUS = {
|
const STATUS = {
|
||||||
@@ -121,6 +122,7 @@ const state = {
|
|||||||
pan: null,
|
pan: null,
|
||||||
fitMode: "width",
|
fitMode: "width",
|
||||||
continuous: false,
|
continuous: false,
|
||||||
|
pageLayout: 1,
|
||||||
renderAnnotations: true,
|
renderAnnotations: true,
|
||||||
renderBackend: "canvas",
|
renderBackend: "canvas",
|
||||||
renderMode: "svg",
|
renderMode: "svg",
|
||||||
@@ -474,6 +476,7 @@ const el = {
|
|||||||
rotateButton: document.querySelector("#rotateButton"),
|
rotateButton: document.querySelector("#rotateButton"),
|
||||||
panButton: document.querySelector("#panButton"),
|
panButton: document.querySelector("#panButton"),
|
||||||
continuousButton: document.querySelector("#continuousButton"),
|
continuousButton: document.querySelector("#continuousButton"),
|
||||||
|
pageLayoutButton: document.querySelector("#pageLayoutButton"),
|
||||||
annotationButton: document.querySelector("#annotationButton"),
|
annotationButton: document.querySelector("#annotationButton"),
|
||||||
imageDPI: document.querySelector("#imageDPI"),
|
imageDPI: document.querySelector("#imageDPI"),
|
||||||
outputCompression: document.querySelector("#outputCompression"),
|
outputCompression: document.querySelector("#outputCompression"),
|
||||||
@@ -618,7 +621,11 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
busy: () => document.body.hasAttribute("data-busy"),
|
busy: () => document.body.hasAttribute("data-busy"),
|
||||||
rotation: () => state.rotation,
|
rotation: () => state.rotation,
|
||||||
canInsert: (index) => pageCan("insert", index),
|
canInsert: (index) => pageCan("insert", index),
|
||||||
|
onPage: (index) => {
|
||||||
|
if (index !== state.pageIndex) setCurrentPage(index);
|
||||||
|
},
|
||||||
onSelect: (item, previous) => {
|
onSelect: (item, previous) => {
|
||||||
|
if (item && item.index !== state.pageIndex) setCurrentPage(item.index);
|
||||||
updateObjectControls(item);
|
updateObjectControls(item);
|
||||||
const reason = objectEditReason(item), previousReason = objectEditReason(previous);
|
const reason = objectEditReason(item), previousReason = objectEditReason(previous);
|
||||||
if (state.editing && state.doc && !document.body.hasAttribute("data-busy")
|
if (state.editing && state.doc && !document.body.hasAttribute("data-busy")
|
||||||
@@ -1609,11 +1616,11 @@ el.localFontButton.addEventListener("click", loadLocalFonts);
|
|||||||
el.ofdInput.addEventListener("change", () => openOFD(el.ofdInput.files[0]));
|
el.ofdInput.addEventListener("change", () => openOFD(el.ofdInput.files[0]));
|
||||||
el.fontInput.addEventListener("change", openSelectedFonts);
|
el.fontInput.addEventListener("change", openSelectedFonts);
|
||||||
el.fontDirectoryInput.addEventListener("change", openSelectedFonts);
|
el.fontDirectoryInput.addEventListener("change", openSelectedFonts);
|
||||||
el.prevButton.addEventListener("click", () => renderPage(state.pageIndex - 1));
|
el.prevButton.addEventListener("click", () => renderPage(pageGroupStart() - state.pageLayout));
|
||||||
el.imageDPI.addEventListener("change", () => changeDisplayMode());
|
el.imageDPI.addEventListener("change", () => changeDisplayMode());
|
||||||
el.renderVectorButton.addEventListener("click", () => changeDisplayMode("svg"));
|
el.renderVectorButton.addEventListener("click", () => changeDisplayMode("svg"));
|
||||||
el.renderRasterButton.addEventListener("click", () => changeDisplayMode("raster"));
|
el.renderRasterButton.addEventListener("click", () => changeDisplayMode("raster"));
|
||||||
el.nextButton.addEventListener("click", () => renderPage(state.pageIndex + 1));
|
el.nextButton.addEventListener("click", () => renderPage(pageGroupStart() + state.pageLayout));
|
||||||
el.zoomOutButton.addEventListener("click", () => setScale(state.scale - 0.1));
|
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);
|
||||||
@@ -1621,6 +1628,7 @@ el.fitHeightButton.addEventListener("click", fitHeight);
|
|||||||
el.rotateButton.addEventListener("click", rotatePages);
|
el.rotateButton.addEventListener("click", rotatePages);
|
||||||
el.panButton.addEventListener("click", togglePan);
|
el.panButton.addEventListener("click", togglePan);
|
||||||
el.continuousButton.addEventListener("click", toggleContinuous);
|
el.continuousButton.addEventListener("click", toggleContinuous);
|
||||||
|
el.pageLayoutButton.addEventListener("click", togglePageLayout);
|
||||||
el.annotationButton.addEventListener("click", toggleAnnotations);
|
el.annotationButton.addEventListener("click", toggleAnnotations);
|
||||||
document.addEventListener("copy", copySelection);
|
document.addEventListener("copy", copySelection);
|
||||||
document.addEventListener("cut", copySelection);
|
document.addEventListener("cut", copySelection);
|
||||||
@@ -1704,6 +1712,11 @@ for (const [viewport, current] of [[el.viewerPanel, () => state.pageWindow], [el
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
el.viewerPanel.addEventListener("pointerdown", startPan);
|
el.viewerPanel.addEventListener("pointerdown", startPan);
|
||||||
|
el.viewerPanel.addEventListener("pointerdown", event => {
|
||||||
|
if (event.button !== 0 || state.editing || document.body.hasAttribute("data-busy")) return;
|
||||||
|
const shell = event.target.closest(".page-shell");
|
||||||
|
if (shell && state.doc) setCurrentPage(Number(shell.dataset.pageIndex));
|
||||||
|
});
|
||||||
el.viewerPanel.addEventListener("pointermove", movePan);
|
el.viewerPanel.addEventListener("pointermove", movePan);
|
||||||
el.viewerPanel.addEventListener("pointerup", endPan);
|
el.viewerPanel.addEventListener("pointerup", endPan);
|
||||||
el.viewerPanel.addEventListener("pointercancel", endPan);
|
el.viewerPanel.addEventListener("pointercancel", endPan);
|
||||||
@@ -5435,7 +5448,7 @@ async function openDocument(options = {}) {
|
|||||||
state.rotation = 0;
|
state.rotation = 0;
|
||||||
setContinuous(window.matchMedia("(max-width: 640px)").matches);
|
setContinuous(window.matchMedia("(max-width: 640px)").matches);
|
||||||
}
|
}
|
||||||
state.fitMode = options.fitMode || (!state.continuous && pageCount === 1 ? "height" : "width");
|
state.fitMode = options.fitMode || (state.pageLayout > 1 || !state.continuous && pageCount === 1 ? "height" : "width");
|
||||||
if (!options.skipAutoFonts && await autoLoadDocumentLocalFonts(openSeq)) {
|
if (!options.skipAutoFonts && await autoLoadDocumentLocalFonts(openSeq)) {
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
@@ -5594,6 +5607,9 @@ async function renderPage(index, options = {}) {
|
|||||||
if (index < 0 || index >= pageCount) {
|
if (index < 0 || index >= pageCount) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (!options.keepBusy && index !== state.pageIndex) {
|
||||||
|
if (!await canvasEditor.commitNudge() || !await canvasEditor.commitText() || !await canvasEditor.commitCrop() || openSeq !== state.openSeq) return null;
|
||||||
|
}
|
||||||
const pageSeq = ++state.pageSeq;
|
const pageSeq = ++state.pageSeq;
|
||||||
if (!state.exporting) {
|
if (!state.exporting) {
|
||||||
if (options.keepBusy) {
|
if (options.keepBusy) {
|
||||||
@@ -8851,8 +8867,12 @@ function syncPageWindow(view, target = null) {
|
|||||||
view.scrollTop = viewport.scrollTop;
|
view.scrollTop = viewport.scrollTop;
|
||||||
view.logicalTop = logical;
|
view.logicalTop = logical;
|
||||||
view.origin = logical - viewport.scrollTop + inset;
|
view.origin = logical - viewport.scrollTop + inset;
|
||||||
const first = virtualIndex(offsets, Math.max(view.origin, logical - view.overscan));
|
let first = virtualIndex(offsets, Math.max(view.origin, logical - view.overscan));
|
||||||
const last = virtualIndex(offsets, Math.min(view.origin + extent, logical + height + view.overscan));
|
let last = virtualIndex(offsets, Math.min(view.origin + extent, logical + height + view.overscan));
|
||||||
|
if (view.columns) {
|
||||||
|
first -= first % view.columns;
|
||||||
|
last = Math.min(offsets.length - 2, last + view.columns - 1 - last % view.columns);
|
||||||
|
}
|
||||||
const selection = host === el.svgHost && state.documentSelection ? document.getSelection() : null;
|
const selection = host === el.svgHost && state.documentSelection ? document.getSelection() : null;
|
||||||
const selected = selection?.rangeCount && isDocumentSelection(selection.getRangeAt(0));
|
const selected = selection?.rangeCount && isDocumentSelection(selection.getRangeAt(0));
|
||||||
let changed = false;
|
let changed = false;
|
||||||
@@ -8875,7 +8895,11 @@ function syncPageWindow(view, target = null) {
|
|||||||
}
|
}
|
||||||
for (const [index, node] of nodes) {
|
for (const [index, node] of nodes) {
|
||||||
const top = offsets[index] - view.origin;
|
const top = offsets[index] - view.origin;
|
||||||
const itemHeight = offsets[index + 1] - offsets[index] - view.gap;
|
const itemHeight = view.heights?.[index] ?? offsets[index + 1] - offsets[index] - view.gap;
|
||||||
|
if (view.lefts) {
|
||||||
|
node.style.left = `${view.lefts[index]}px`;
|
||||||
|
node.style.transform = "none";
|
||||||
|
}
|
||||||
node.style.top = `${Math.max(-itemHeight, Math.min(extent, top))}px`;
|
node.style.top = `${Math.max(-itemHeight, Math.min(extent, top))}px`;
|
||||||
node.style.visibility = top + itemHeight < 0 || top >= extent ? "hidden" : "";
|
node.style.visibility = top + itemHeight < 0 || top >= extent ? "hidden" : "";
|
||||||
}
|
}
|
||||||
@@ -8890,7 +8914,7 @@ function pageWindowAnchor(view) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function releaseFlowShell(shell, index) {
|
function releaseFlowShell(shell, index) {
|
||||||
if (shell.contains(document.activeElement) || canvasEditor.drag?.item?.index === index) return false;
|
if (shell.contains(document.activeElement) || canvasEditor.drag?.item?.index === index || canvasEditor.drag?.page?.index === index) return false;
|
||||||
unmountPage(index);
|
unmountPage(index);
|
||||||
if (state.selectedPages.has(index)) return false;
|
if (state.selectedPages.has(index)) return false;
|
||||||
state.pageObserver?.unobserve(shell);
|
state.pageObserver?.unobserve(shell);
|
||||||
@@ -9825,7 +9849,8 @@ function markFlowPageError(index, err) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function queueNearbyPages(index, openSeq = state.openSeq) {
|
function queueNearbyPages(index, openSeq = state.openSeq) {
|
||||||
for (let i = Math.max(0, index - 1); i <= Math.min((state.doc?.pageCount || 1) - 1, index + 2); i += 1) {
|
const start = pageGroupStart(index);
|
||||||
|
for (let i = Math.max(0, start - 1); i <= Math.min((state.doc?.pageCount || 1) - 1, start + state.pageLayout + 1); i += 1) {
|
||||||
renderFlowPage(i, { openSeq, priority: 2 });
|
renderFlowPage(i, { openSeq, priority: 2 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9835,6 +9860,21 @@ function pageShell(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scrollToPage(index) {
|
function scrollToPage(index) {
|
||||||
|
if (state.pageLayout > 1 && state.pageWindow) {
|
||||||
|
const view = state.pageWindow, start = pageGroupStart(index);
|
||||||
|
const size = pageGroupSize(start), gap = PAGE_LAYOUT_GAP;
|
||||||
|
const height = size.height * MM_TO_PX * state.scale + (size.rows - 1) * gap;
|
||||||
|
const whole = height <= el.viewerPanel.clientHeight - pageBlockSpace() * 2 + 1;
|
||||||
|
const space = state.fitMode === "height" && !state.continuous
|
||||||
|
? Math.max(pageBlockSpace(), (el.viewerPanel.clientHeight - height) / 2) : pageBlockSpace();
|
||||||
|
syncPageWindow(view, view.offsets[whole ? start : index] - space);
|
||||||
|
el.viewerPanel.scrollLeft = Math.max(0, (el.viewerPanel.scrollWidth - el.viewerPanel.clientWidth) / 2);
|
||||||
|
if (size.width * MM_TO_PX * state.scale + gap > el.viewerPanel.clientWidth) {
|
||||||
|
const shell = pageShell(index);
|
||||||
|
if (shell) centerPageInline(shell);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state.pageWindow) syncPageWindow(state.pageWindow, state.pageWindow.offsets[index]);
|
if (state.pageWindow) syncPageWindow(state.pageWindow, state.pageWindow.offsets[index]);
|
||||||
const shell = pageShell(index);
|
const shell = pageShell(index);
|
||||||
if (shell) {
|
if (shell) {
|
||||||
@@ -9875,6 +9915,7 @@ function schedulePageSync() {
|
|||||||
function syncCurrentPageFromScroll() {
|
function syncCurrentPageFromScroll() {
|
||||||
if (state.openActionTask?.navigating) return;
|
if (state.openActionTask?.navigating) return;
|
||||||
syncPageWindow(state.pageWindow);
|
syncPageWindow(state.pageWindow);
|
||||||
|
if (canvasEditor.input || canvasEditor.drag || canvasEditor.crop) return;
|
||||||
const shell = pageShellFromView();
|
const shell = pageShellFromView();
|
||||||
if (!shell) {
|
if (!shell) {
|
||||||
return;
|
return;
|
||||||
@@ -9900,6 +9941,19 @@ function prunePageRenderQueue() {
|
|||||||
|
|
||||||
function pageShellFromView() {
|
function pageShellFromView() {
|
||||||
const rect = el.viewerPanel.getBoundingClientRect();
|
const rect = el.viewerPanel.getBoundingClientRect();
|
||||||
|
if (state.pageLayout > 1) {
|
||||||
|
let best = null, area = 0;
|
||||||
|
const current = pageShell(state.pageIndex);
|
||||||
|
for (const shell of [current, ...(state.pageWindow?.nodes.values() || [])]) {
|
||||||
|
if (!shell || shell.style.visibility === "hidden") continue;
|
||||||
|
const bounds = shell.getBoundingClientRect();
|
||||||
|
const visible = Math.max(0, Math.min(rect.right, bounds.right) - Math.max(rect.left, bounds.left))
|
||||||
|
* Math.max(0, Math.min(rect.bottom, bounds.bottom) - Math.max(rect.top, bounds.top));
|
||||||
|
const ratio = visible / Math.max(1, bounds.width * bounds.height);
|
||||||
|
if (ratio > area + 0.01) { best = shell; area = ratio; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
const x = rect.left + rect.width / 2;
|
const x = rect.left + rect.width / 2;
|
||||||
if (el.viewerPanel.scrollTop + el.viewerPanel.clientHeight >= el.viewerPanel.scrollHeight - 1) {
|
if (el.viewerPanel.scrollTop + el.viewerPanel.clientHeight >= el.viewerPanel.scrollHeight - 1) {
|
||||||
const shell = pageShell(state.pageIndex);
|
const shell = pageShell(state.pageIndex);
|
||||||
@@ -9924,7 +9978,7 @@ function setCurrentPage(index) {
|
|||||||
if (state.composite && state.composite.index !== index) resetCompositeScope();
|
if (state.composite && state.composite.index !== index) resetCompositeScope();
|
||||||
state.pageIndex = index;
|
state.pageIndex = index;
|
||||||
if (state.editing) state.openActionPage = state.doc?.pages[index]?.id;
|
if (state.editing) state.openActionPage = state.doc?.pages[index]?.id;
|
||||||
if (state.fitMode === "width") {
|
if (state.fitMode === "width" && state.pageLayout === 1) {
|
||||||
state.scale = fitWidthScale(currentPageInfo());
|
state.scale = fitWidthScale(currentPageInfo());
|
||||||
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
|
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
|
||||||
}
|
}
|
||||||
@@ -9971,6 +10025,37 @@ function setPageItemCurrent(item, current) {
|
|||||||
item.removeAttribute("data-current");
|
item.removeAttribute("data-current");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pageGroupStart(index = state.pageIndex) {
|
||||||
|
return Math.floor(index / state.pageLayout) * state.pageLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageGroupSize(index = state.pageIndex) {
|
||||||
|
const start = pageGroupStart(index), rows = state.pageLayout === 4 ? 2 : 1;
|
||||||
|
const widths = [0, 0], heights = Array(rows).fill(0);
|
||||||
|
for (let offset = 0; offset < state.pageLayout; offset++) {
|
||||||
|
const page = state.doc.pages[start + offset];
|
||||||
|
if (!page) continue;
|
||||||
|
const size = pageViewSize(page);
|
||||||
|
widths[offset % 2] = Math.max(widths[offset % 2], size.width);
|
||||||
|
heights[Math.floor(offset / 2)] = Math.max(heights[Math.floor(offset / 2)], size.height);
|
||||||
|
}
|
||||||
|
if (!widths[1]) widths[1] = widths[0];
|
||||||
|
for (let row = 0; row < rows; row++) if (!heights[row]) heights[row] = heights[0];
|
||||||
|
return { widths, heights, rows, width: widths[0] + widths[1], height: heights.reduce((sum, height) => sum + height, 0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function togglePageLayout() {
|
||||||
|
if (!state.doc || document.body.hasAttribute("data-busy")) return;
|
||||||
|
const openSeq = state.openSeq;
|
||||||
|
if (!await canvasEditor.commitNudge() || !await canvasEditor.commitText() || !await canvasEditor.commitCrop() || openSeq !== state.openSeq) return;
|
||||||
|
state.pageLayout = state.pageLayout === 1 ? 2 : state.pageLayout === 2 ? 4 : 1;
|
||||||
|
el.pageLayoutButton.textContent = { 1: "一页", 2: "两页", 4: "四页" }[state.pageLayout];
|
||||||
|
el.pageLayoutButton.dataset.short = { 1: "一", 2: "两", 4: "四" }[state.pageLayout];
|
||||||
|
layoutPages();
|
||||||
|
fitHeight();
|
||||||
|
queueNearbyPages(state.pageIndex);
|
||||||
|
}
|
||||||
|
|
||||||
function layoutPages() {
|
function layoutPages() {
|
||||||
if (!state.doc) {
|
if (!state.doc) {
|
||||||
return;
|
return;
|
||||||
@@ -9979,7 +10064,53 @@ function layoutPages() {
|
|||||||
const view = state.pageWindow;
|
const view = state.pageWindow;
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
const anchor = view.offsets.length > 1 ? pageWindowAnchor(view) : { index: state.pageIndex, offset: 0 };
|
const anchor = view.offsets.length > 1 ? pageWindowAnchor(view) : { index: state.pageIndex, offset: 0 };
|
||||||
const gap = Number.parseFloat(getComputedStyle(el.svgHost).rowGap) || 0;
|
const gap = Math.max(state.pageLayout > 1 ? PAGE_LAYOUT_GAP : 0, Number.parseFloat(getComputedStyle(el.svgHost).rowGap) || 0);
|
||||||
|
if (state.pageLayout > 1) {
|
||||||
|
const count = state.doc.pages.length, spacing = PAGE_LAYOUT_GAP;
|
||||||
|
view.columns = 2;
|
||||||
|
view.offsets = new Float64Array(count + 1);
|
||||||
|
view.heights = new Float64Array(count);
|
||||||
|
view.lefts = new Float64Array(count);
|
||||||
|
let top = 0, width = 0;
|
||||||
|
const groups = [];
|
||||||
|
for (let start = 0; start < count; start += state.pageLayout) {
|
||||||
|
const size = pageGroupSize(start), factor = MM_TO_PX * state.scale;
|
||||||
|
const groupWidth = size.width * factor + spacing;
|
||||||
|
groups.push({ start, size, width: groupWidth });
|
||||||
|
width = Math.max(width, groupWidth);
|
||||||
|
for (let row = 0; row < size.rows; row++) {
|
||||||
|
for (let column = 0; column < 2; column++) {
|
||||||
|
const index = start + row * 2 + column;
|
||||||
|
if (index >= count) break;
|
||||||
|
view.offsets[index] = top;
|
||||||
|
view.heights[index] = pageViewSize(state.doc.pages[index]).height * factor;
|
||||||
|
}
|
||||||
|
top += size.heights[row] * factor + (row + 1 === size.rows ? gap : spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const group of groups) {
|
||||||
|
for (let index = group.start; index < Math.min(count, group.start + state.pageLayout); index++) {
|
||||||
|
const column = (index - group.start) % 2, factor = MM_TO_PX * state.scale;
|
||||||
|
view.lefts[index] = (width - group.width) / 2
|
||||||
|
+ (column ? group.size.widths[0] * factor + spacing : 0)
|
||||||
|
+ (group.size.widths[column] - pageViewSize(state.doc.pages[index]).width) * factor / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.offsets[count] = top;
|
||||||
|
view.gap = gap;
|
||||||
|
view.total = Math.max(0, top - gap);
|
||||||
|
view.host.style.width = `${width}px`;
|
||||||
|
for (const [index, shell] of view.nodes) layoutPageShell(shell, state.doc.pages[index]);
|
||||||
|
syncPageWindow(view, view.offsets[Math.min(anchor.index, count - 1)] + anchor.offset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
view.columns = 0;
|
||||||
|
view.heights = null;
|
||||||
|
view.lefts = null;
|
||||||
|
for (const shell of view.nodes.values()) {
|
||||||
|
shell.style.removeProperty("left");
|
||||||
|
shell.style.removeProperty("transform");
|
||||||
|
}
|
||||||
const offsets = new Float64Array(state.doc.pages.length + 1);
|
const offsets = new Float64Array(state.doc.pages.length + 1);
|
||||||
const fittedWidth = state.scale * pageViewSize(currentPageInfo()).width;
|
const fittedWidth = state.scale * pageViewSize(currentPageInfo()).width;
|
||||||
let width = 1;
|
let width = 1;
|
||||||
@@ -10003,7 +10134,7 @@ function layoutPageShell(shell, page) {
|
|||||||
const size = pageViewSize(page);
|
const size = pageViewSize(page);
|
||||||
const viewWidth = Math.max(1, size.width * MM_TO_PX);
|
const viewWidth = Math.max(1, size.width * MM_TO_PX);
|
||||||
const viewHeight = Math.max(1, size.height * MM_TO_PX);
|
const viewHeight = Math.max(1, size.height * MM_TO_PX);
|
||||||
const scale = state.fitMode === "width" ? state.scale * pageViewSize(currentPageInfo()).width / size.width : state.scale;
|
const scale = state.fitMode === "width" && state.pageLayout === 1 ? state.scale * pageViewSize(currentPageInfo()).width / size.width : state.scale;
|
||||||
shell.style.width = `${viewWidth * scale}px`;
|
shell.style.width = `${viewWidth * scale}px`;
|
||||||
shell.style.height = `${viewHeight * scale}px`;
|
shell.style.height = `${viewHeight * scale}px`;
|
||||||
const surface = shell.querySelector(".page-surface");
|
const surface = shell.querySelector(".page-surface");
|
||||||
@@ -11917,6 +12048,10 @@ function fitWidth(updateStatus = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fitWidthScale(page) {
|
function fitWidthScale(page) {
|
||||||
|
if (state.pageLayout > 1) {
|
||||||
|
const size = pageGroupSize();
|
||||||
|
return Math.min(4, Math.max(1, el.viewerPanel.clientWidth - pageSpace() * 2 - PAGE_LAYOUT_GAP - scrollbarWidth()) / (size.width * MM_TO_PX));
|
||||||
|
}
|
||||||
page = pageViewSize(page);
|
page = pageViewSize(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);
|
||||||
@@ -11945,6 +12080,14 @@ function fitHeight(updateStatus = true) {
|
|||||||
if (!page) {
|
if (!page) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.pageLayout > 1) {
|
||||||
|
const size = pageGroupSize(), space = pageSpace(), gap = PAGE_LAYOUT_GAP;
|
||||||
|
const width = Math.max(1, el.viewerPanel.clientWidth - space * 2 - gap - scrollbarWidth());
|
||||||
|
const height = Math.max(1, el.viewerPanel.clientHeight - space * 2 - (size.rows - 1) * gap);
|
||||||
|
setScale(Math.min(width / (size.width * MM_TO_PX), height / (size.height * MM_TO_PX)), updateStatus, "height");
|
||||||
|
if (updateStatus) scrollToPage(state.pageIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const space = pageSpace();
|
const space = pageSpace();
|
||||||
let availableWidth = Math.max(1, el.viewerPanel.clientWidth - space * 2);
|
let availableWidth = Math.max(1, el.viewerPanel.clientWidth - space * 2);
|
||||||
let availableHeight = Math.max(1, el.viewerPanel.clientHeight - space * 2);
|
let availableHeight = Math.max(1, el.viewerPanel.clientHeight - space * 2);
|
||||||
@@ -12001,7 +12144,8 @@ function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
|||||||
layoutPages();
|
layoutPages();
|
||||||
}
|
}
|
||||||
updateFitSpace();
|
updateFitSpace();
|
||||||
if (state.pageWindow && state.pageWindow.gap !== (Number.parseFloat(getComputedStyle(el.svgHost).rowGap) || 0)) layoutPages();
|
const gap = Math.max(state.pageLayout > 1 ? PAGE_LAYOUT_GAP : 0, Number.parseFloat(getComputedStyle(el.svgHost).rowGap) || 0);
|
||||||
|
if (state.pageWindow && state.pageWindow.gap !== gap) layoutPages();
|
||||||
if (layoutChanged) {
|
if (layoutChanged) {
|
||||||
restoreScaleAnchor(anchor);
|
restoreScaleAnchor(anchor);
|
||||||
}
|
}
|
||||||
@@ -12037,7 +12181,7 @@ function restoreScaleAnchor(anchor) {
|
|||||||
}
|
}
|
||||||
if (state.pageWindow) {
|
if (state.pageWindow) {
|
||||||
const view = state.pageWindow;
|
const view = state.pageWindow;
|
||||||
const height = view.offsets[anchor.index + 1] - view.offsets[anchor.index] - view.gap;
|
const height = view.heights?.[anchor.index] ?? view.offsets[anchor.index + 1] - view.offsets[anchor.index] - view.gap;
|
||||||
syncPageWindow(view, view.offsets[anchor.index] + height * anchor.y - el.viewerPanel.clientHeight * anchor.viewY);
|
syncPageWindow(view, view.offsets[anchor.index] + height * anchor.y - el.viewerPanel.clientHeight * anchor.viewY);
|
||||||
}
|
}
|
||||||
const shell = pageShell(anchor.index);
|
const shell = pageShell(anchor.index);
|
||||||
@@ -12091,7 +12235,9 @@ function updateFitSpace() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const shell = pageShell(state.pageIndex);
|
const shell = pageShell(state.pageIndex);
|
||||||
const height = shell ? shell.getBoundingClientRect().height : Math.max(1, pageViewSize(page).height * MM_TO_PX * state.scale);
|
const group = state.pageLayout > 1 ? pageGroupSize() : null;
|
||||||
|
const height = group ? group.height * MM_TO_PX * state.scale + (group.rows - 1) * PAGE_LAYOUT_GAP
|
||||||
|
: shell ? shell.getBoundingClientRect().height : Math.max(1, pageViewSize(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 = state.continuous ? 0 : Math.max(base, (el.viewerPanel.clientHeight - height) / 2);
|
const space = state.continuous ? 0 : Math.max(base, (el.viewerPanel.clientHeight - height) / 2);
|
||||||
@@ -12153,8 +12299,8 @@ function updateControls() {
|
|||||||
updateDisplayControls();
|
updateDisplayControls();
|
||||||
updateEditorTools();
|
updateEditorTools();
|
||||||
const pageCount = state.doc ? state.doc.pageCount : 0;
|
const pageCount = state.doc ? state.doc.pageCount : 0;
|
||||||
el.prevButton.disabled = !hasDoc || state.pageIndex <= 0;
|
el.prevButton.disabled = !hasDoc || pageGroupStart() === 0;
|
||||||
el.nextButton.disabled = !hasDoc || state.pageIndex >= pageCount - 1;
|
el.nextButton.disabled = !hasDoc || pageGroupStart() + state.pageLayout >= pageCount;
|
||||||
el.pageInput.disabled = !hasDoc;
|
el.pageInput.disabled = !hasDoc;
|
||||||
el.pageInput.max = String(pageCount || 1);
|
el.pageInput.max = String(pageCount || 1);
|
||||||
el.pageInput.value = String(hasDoc ? state.pageIndex + 1 : 0);
|
el.pageInput.value = String(hasDoc ? state.pageIndex + 1 : 0);
|
||||||
@@ -12166,6 +12312,7 @@ function updateControls() {
|
|||||||
el.rotateButton.disabled = !hasDoc;
|
el.rotateButton.disabled = !hasDoc;
|
||||||
el.panButton.disabled = !hasDoc;
|
el.panButton.disabled = !hasDoc;
|
||||||
el.continuousButton.disabled = !hasDoc;
|
el.continuousButton.disabled = !hasDoc;
|
||||||
|
el.pageLayoutButton.disabled = !hasDoc || document.body.hasAttribute("data-busy");
|
||||||
el.fitButton.toggleAttribute("data-pressed", hasDoc && state.fitMode === "width");
|
el.fitButton.toggleAttribute("data-pressed", hasDoc && state.fitMode === "width");
|
||||||
el.fitHeightButton.toggleAttribute("data-pressed", hasDoc && state.fitMode === "height");
|
el.fitHeightButton.toggleAttribute("data-pressed", hasDoc && state.fitMode === "height");
|
||||||
updateAnnotationButton();
|
updateAnnotationButton();
|
||||||
@@ -12373,6 +12520,7 @@ function showError(err, empty = !state.doc) {
|
|||||||
|
|
||||||
function setBusy(busy, text = "", percent = 0, status = "") {
|
function setBusy(busy, text = "", percent = 0, status = "") {
|
||||||
document.body.toggleAttribute("data-busy", busy);
|
document.body.toggleAttribute("data-busy", busy);
|
||||||
|
el.pageLayoutButton.disabled = busy || !state.doc;
|
||||||
el.closeButton.disabled = busy || !state.doc || state.exporting || state.saving;
|
el.closeButton.disabled = busy || !state.doc || state.exporting || state.saving;
|
||||||
el.refreshAppButton.disabled = busy;
|
el.refreshAppButton.disabled = busy;
|
||||||
updateStructureControls();
|
updateStructureControls();
|
||||||
|
|||||||
@@ -838,7 +838,7 @@ export class CanvasEditor {
|
|||||||
if (this.input.composing) this.commitText();
|
if (this.input.composing) this.commitText();
|
||||||
else {
|
else {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.selectAfterText(this.targetAt(event));
|
this.selectAfterText(this.targetAt(event), this.pages.get(event.target.closest(".page-surface"))?.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -860,6 +860,8 @@ export class CanvasEditor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.tool) {
|
if (this.tool) {
|
||||||
|
const page = this.pages.get(event.target.closest(".page-surface"));
|
||||||
|
if (page) this.options.onPage?.(page.index);
|
||||||
if (this.tool.startsWith("erase-")) this.startErase(event);
|
if (this.tool.startsWith("erase-")) this.startErase(event);
|
||||||
else if (this.tool.startsWith("pen-")) this.startInk(event);
|
else if (this.tool.startsWith("pen-")) this.startInk(event);
|
||||||
else this.startShape(event);
|
else this.startShape(event);
|
||||||
@@ -870,6 +872,8 @@ export class CanvasEditor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const target = this.targetAt(event), node = target?.node;
|
const target = this.targetAt(event), node = target?.node;
|
||||||
|
const page = this.pages.get(event.target.closest(".page-surface"));
|
||||||
|
if (page) this.options.onPage?.(page.index);
|
||||||
if (event.target.closest(".edit-object, .edit-selection") || target) event.preventDefault();
|
if (event.target.closest(".edit-object, .edit-selection") || target) event.preventDefault();
|
||||||
this.focus();
|
this.focus();
|
||||||
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
||||||
@@ -939,12 +943,13 @@ export class CanvasEditor {
|
|||||||
: this.contains(item, point, tolerance));
|
: this.contains(item, point, tolerance));
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectAfterText(target) {
|
async selectAfterText(target, index) {
|
||||||
if (!await this.commitText() || this.input || !this.enabled || this.options.busy() || this.tool) return;
|
if (!await this.commitText() || this.input || !this.enabled || this.options.busy() || this.tool) return;
|
||||||
|
if (index !== undefined) this.options.onPage?.(index);
|
||||||
if (target) {
|
if (target) {
|
||||||
const item = this.pageItems(target.index).find(item => item.id === target.id);
|
const item = this.pageItems(target.index).find(item => item.id === target.id);
|
||||||
if (item) this.select(item);
|
if (item) this.select(item);
|
||||||
}
|
} else if (index !== undefined) this.select(null);
|
||||||
this.focus();
|
this.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user