feat(编辑能力): 修复空选区误操作,缩短默认标题
build.yaml / build (push) Successful in 36s

This commit is contained in:
xiaoqidun committed 2026-09-17 10:31:01 +08:00
1 parent 13eced8dd8
commit 2cc6c121f8
6 files changed
+224 -39

No files matched your search

+7 -1
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#fbfcfd"> <meta name="theme-color" content="#fbfcfd">
<title>OFDGo WebUI - 纯粹的OFD编辑器,完全本地、离线可用</title> <title>OFDGo WebUI - OFD编辑器</title>
<link rel="manifest" href="./ofdgo.json"> <link rel="manifest" href="./ofdgo.json">
<link rel="icon" type="image/svg+xml" href="./ofdgo.svg"> <link rel="icon" type="image/svg+xml" href="./ofdgo.svg">
<link rel="stylesheet" href="./ofdgo.css"> <link rel="stylesheet" href="./ofdgo.css">
@@ -264,6 +264,7 @@
</dialog> </dialog>
<dialog id="pagePanel" class="form-dialog" aria-label="页面设置"> <dialog id="pagePanel" class="form-dialog" aria-label="页面设置">
<form id="pageForm"> <form id="pageForm">
<div id="pageRangeRow" class="form-row" hidden><span>范围</span><output id="pageRange"></output></div>
<div class="form-row"> <div class="form-row">
<span id="pageDirectionLabel">方向</span> <span id="pageDirectionLabel">方向</span>
<div class="form-options" role="radiogroup" aria-labelledby="pageDirectionLabel"> <div class="form-options" role="radiogroup" aria-labelledby="pageDirectionLabel">
@@ -334,6 +335,11 @@
<button id="outlinesTab" type="button" role="tab" aria-selected="false" aria-controls="outlineList" tabindex="-1">目录</button> <button id="outlinesTab" type="button" role="tab" aria-selected="false" aria-controls="outlineList" tabindex="-1">目录</button>
<button id="searchTab" type="button" role="tab" aria-keyshortcuts="Control+F Meta+F" aria-selected="false" aria-controls="searchPanel" tabindex="-1">搜索</button> <button id="searchTab" type="button" role="tab" aria-keyshortcuts="Control+F Meta+F" aria-selected="false" aria-controls="searchPanel" tabindex="-1">搜索</button>
</div> </div>
<div id="pageSelectionTools" class="page-selection-tools" hidden>
<span id="pageSelectionCount" role="status"></span>
<button id="selectAllPages" class="small-button" type="button">全选</button>
<button id="finishPageSelection" class="small-button" type="button">完成</button>
</div>
<div id="navigationContent" class="navigation-content"> <div id="navigationContent" class="navigation-content">
<div id="pageList" class="page-list" role="tabpanel" aria-label="页面"></div> <div id="pageList" class="page-list" role="tabpanel" aria-label="页面"></div>
<div id="outlineList" role="tabpanel" aria-label="目录" hidden></div> <div id="outlineList" role="tabpanel" aria-label="目录" hidden></div>
+20
View File
@@ -884,6 +884,19 @@ body[data-hide-meta] .meta-panel {
overflow: auto; overflow: auto;
} }
.page-selection-tools {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 12px;
}
.page-selection-tools > span {
flex: 1;
font-size: 12px;
color: var(--text-muted);
}
.meta-panel { .meta-panel {
border-right: 0; border-right: 0;
border-left: 1px solid var(--line); border-left: 1px solid var(--line);
@@ -1159,6 +1172,12 @@ button.outline-link:hover {
font-weight: 700; font-weight: 700;
} }
.page-list-item[aria-pressed] {
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
.page-list-item[aria-current]:not([aria-pressed="false"]), .page-list-item[aria-current]:not([aria-pressed="false"]),
.page-list-item[aria-pressed="true"] { .page-list-item[aria-pressed="true"] {
border-color: var(--accent); border-color: var(--accent);
@@ -1272,6 +1291,7 @@ button.outline-link:hover {
.panel-row > [hidden], .panel-row > [hidden],
.meta-list > [hidden], .meta-list > [hidden],
.navigation-tabs[hidden], .navigation-tabs[hidden],
.page-selection-tools[hidden],
.form-row[hidden], .form-row[hidden],
.tool-group[hidden], .tool-group[hidden],
.page-list[hidden], .page-list[hidden],
+134 -26
View File
@@ -42,6 +42,8 @@ const state = {
importing: false, importing: false,
pageSelection: new Set(), pageSelection: new Set(),
pageSelectionAnchor: null, pageSelectionAnchor: null,
pageMultiSelect: false,
pageTouchPointer: null,
outlineSelection: null, outlineSelection: null,
outlineAction: "add", outlineAction: "add",
styleOriginal: null, styleOriginal: null,
@@ -223,6 +225,8 @@ const el = {
pagePortrait: document.querySelector("#pagePortrait"), pagePortrait: document.querySelector("#pagePortrait"),
pageLandscape: document.querySelector("#pageLandscape"), pageLandscape: document.querySelector("#pageLandscape"),
pageWidth: document.querySelector("#pageWidth"), pageWidth: document.querySelector("#pageWidth"),
pageRangeRow: document.querySelector("#pageRangeRow"),
pageRange: document.querySelector("#pageRange"),
pageHeight: document.querySelector("#pageHeight"), pageHeight: document.querySelector("#pageHeight"),
pageStatus: document.querySelector("#pageStatus"), pageStatus: document.querySelector("#pageStatus"),
pageCancel: document.querySelector("#pageCancel"), pageCancel: document.querySelector("#pageCancel"),
@@ -286,6 +290,10 @@ const el = {
pageListPanel: document.querySelector(".page-list-panel"), pageListPanel: document.querySelector(".page-list-panel"),
pageListTitle: document.querySelector("#pageListTitle"), pageListTitle: document.querySelector("#pageListTitle"),
navigationTabs: document.querySelector("#navigationTabs"), navigationTabs: document.querySelector("#navigationTabs"),
pageSelectionTools: document.querySelector("#pageSelectionTools"),
pageSelectionCount: document.querySelector("#pageSelectionCount"),
selectAllPages: document.querySelector("#selectAllPages"),
finishPageSelection: document.querySelector("#finishPageSelection"),
navigationContent: document.querySelector("#navigationContent"), navigationContent: document.querySelector("#navigationContent"),
pagesTab: document.querySelector("#pagesTab"), pagesTab: document.querySelector("#pagesTab"),
outlinesTab: document.querySelector("#outlinesTab"), outlinesTab: document.querySelector("#outlinesTab"),
@@ -716,7 +724,7 @@ for (const radio of [el.pagePortrait, el.pageLandscape]) {
el.pageForm.addEventListener("submit", async (event) => { el.pageForm.addEventListener("submit", async (event) => {
event.preventDefault(); event.preventDefault();
el.pageStatus.textContent = ""; el.pageStatus.textContent = "";
if (await changeDocument("ofdgoChangePage", null, "resize", state.pageIndex, Number(el.pageWidth.value), Number(el.pageHeight.value))) { if (await changeDocument("ofdgoBatchPages", null, "resize", selectedPageRange(), Number(el.pageWidth.value), Number(el.pageHeight.value))) {
el.pagePanel.close(); el.pagePanel.close();
} }
}); });
@@ -1130,18 +1138,27 @@ function openPagePanel() {
if (!state.editing || document.body.hasAttribute("aria-busy")) { if (!state.editing || document.body.hasAttribute("aria-busy")) {
return; return;
} }
const page = currentPageInfo(); const pages = selectedPageIndexes().map(index => state.doc.pages[index]);
el.pageWidth.value = String(page.width); if (!pages.length) return;
el.pageHeight.value = String(page.height); for (const [input, key] of [[el.pageWidth, "width"], [el.pageHeight, "height"]]) {
const same = pages.every(page => page[key] === pages[0][key]);
input.value = same ? String(pages[0][key]) : "";
input.placeholder = same ? "" : "不同";
}
el.pageRangeRow.hidden = pages.length === 1;
el.pageRange.textContent = `共${pages.length}页`;
el.pageRange.title = selectedPageRange();
el.pageStatus.textContent = ""; el.pageStatus.textContent = "";
updatePageDirection(); updatePageDirection();
el.pagePanel.showModal(); el.pagePanel.showModal();
} }
function updatePageDirection() { function updatePageDirection() {
const complete = el.pageWidth.value !== "" && el.pageHeight.value !== "";
el.pagePortrait.disabled = el.pageLandscape.disabled = !complete;
const landscape = Number(el.pageWidth.value) > Number(el.pageHeight.value); const landscape = Number(el.pageWidth.value) > Number(el.pageHeight.value);
el.pageLandscape.checked = landscape; el.pageLandscape.checked = complete && landscape;
el.pagePortrait.checked = !landscape; el.pagePortrait.checked = complete && !landscape;
} }
function warnUnsaved(event) { function warnUnsaved(event) {
@@ -2340,6 +2357,7 @@ async function openOFD(file) {
state.editing = false; state.editing = false;
state.pageSelection.clear(); state.pageSelection.clear();
state.pageSelectionAnchor = null; state.pageSelectionAnchor = null;
state.pageMultiSelect = false;
state.objectClipboard = null; state.objectClipboard = null;
state.styleClipboard = null; state.styleClipboard = null;
state.editorInfo = null; state.editorInfo = null;
@@ -2775,6 +2793,7 @@ async function openDocument(options = {}) {
if (options.resetScroll || !options.skipAutoFonts) { if (options.resetScroll || !options.skipAutoFonts) {
state.pageSelection.clear(); state.pageSelection.clear();
state.pageSelectionAnchor = null; state.pageSelectionAnchor = null;
state.pageMultiSelect = false;
} }
state.pageIndex = pageIndex; state.pageIndex = pageIndex;
state.scale = options.scale || 1; state.scale = options.scale || 1;
@@ -2978,8 +2997,12 @@ function openExportPanel() {
return; return;
} }
el.exportForm.reset(); el.exportForm.reset();
updateExportRange(); if (state.editing && state.pageSelection.size) {
el.exportSpecified.checked = true;
el.exportRange.value = selectedPageRange();
}
el.exportPanel.showModal(); el.exportPanel.showModal();
updateExportRange();
} }
async function updateExportRange() { async function updateExportRange() {
@@ -3980,6 +4003,7 @@ function showNavigation(selected) {
tab.setAttribute("aria-selected", String(active)); tab.setAttribute("aria-selected", String(active));
tab.tabIndex = active ? 0 : -1; tab.tabIndex = active ? 0 : -1;
} }
el.pageSelectionTools.hidden = !state.editing || !state.pageMultiSelect || selected !== el.pagesTab;
el.navigationContent.scrollTop = state.navigationScroll.get(selected) || 0; el.navigationContent.scrollTop = state.navigationScroll.get(selected) || 0;
} }
@@ -4261,19 +4285,20 @@ function createOutlineList(outlines, path = []) {
} }
function selectedPageIndexes() { function selectedPageIndexes() {
if (!state.pageSelection.size) return [state.pageIndex]; if (!state.pageSelection.size) return state.pageMultiSelect ? [] : [state.pageIndex];
return state.doc.pages.filter(page => state.pageSelection.has(page.id)).map(page => page.index); return state.doc.pages.filter(page => state.pageSelection.has(page.id)).map(page => page.index);
} }
function changeSelectedPages(action, direction = 0) { function changeSelectedPages(action, direction = 0) {
const indexes = selectedPageIndexes(); const indexes = selectedPageIndexes();
if (!indexes.length) return;
if (indexes.length === 1) return changeDocument("ofdgoChangePage", null, action, indexes[0], ...(direction ? [indexes[0] + direction] : [])); if (indexes.length === 1) return changeDocument("ofdgoChangePage", null, action, indexes[0], ...(direction ? [indexes[0] + direction] : []));
const at = direction < 0 ? indexes[0] - 1 : indexes.at(-1) + 2; const at = direction < 0 ? indexes[0] - 1 : indexes.at(-1) + 2;
return changeDocument("ofdgoBatchPages", null, action, selectedPageRange(), ...(direction ? [at] : [])); return changeDocument("ofdgoBatchPages", null, action, selectedPageRange(), ...(direction ? [at] : []));
} }
function selectedPageRange() { function selectedPageRange(indexes = selectedPageIndexes()) {
const pages = selectedPageIndexes().map(index => index + 1); const pages = indexes.map(index => index + 1);
const ranges = []; const ranges = [];
for (let i = 0; i < pages.length; i++) { for (let i = 0; i < pages.length; i++) {
const first = pages[i]; const first = pages[i];
@@ -4284,6 +4309,10 @@ function selectedPageRange() {
} }
function syncPageSelection() { function syncPageSelection() {
if (!state.editing) state.pageMultiSelect = false;
el.pageSelectionTools.hidden = !state.editing || !state.pageMultiSelect || el.pageList.hidden;
el.pageSelectionCount.textContent = `已选${state.pageSelection.size}页`;
el.selectAllPages.textContent = state.pageSelection.size === state.doc?.pageCount ? "清空" : "全选";
for (const button of el.pageList.children) { for (const button of el.pageList.children) {
if (state.editing) button.setAttribute("aria-pressed", String(state.pageSelection.has(state.doc.pages[Number(button.dataset.pageIndex)].id))); if (state.editing) button.setAttribute("aria-pressed", String(state.pageSelection.has(state.doc.pages[Number(button.dataset.pageIndex)].id)));
else button.removeAttribute("aria-pressed"); else button.removeAttribute("aria-pressed");
@@ -4292,8 +4321,12 @@ function syncPageSelection() {
function selectThumbnailPage(event, index) { function selectThumbnailPage(event, index) {
if (document.body.hasAttribute("aria-busy")) return; if (document.body.hasAttribute("aria-busy")) return;
if (event.pointerId === state.pageTouchPointer) {
state.pageTouchPointer = null;
return;
}
if (state.editing) { if (state.editing) {
const page = state.doc.pages[index], additive = event.ctrlKey || event.metaKey; const page = state.doc.pages[index], additive = event.ctrlKey || event.metaKey || state.pageMultiSelect;
if (!additive) state.pageSelection.clear(); if (!additive) state.pageSelection.clear();
if (event.shiftKey) { if (event.shiftKey) {
let anchor = state.doc.pages.findIndex(page => page.id === state.pageSelectionAnchor); let anchor = state.doc.pages.findIndex(page => page.id === state.pageSelectionAnchor);
@@ -4311,6 +4344,62 @@ function selectThumbnailPage(event, index) {
return renderPage(index); return renderPage(index);
} }
el.selectAllPages.addEventListener("click", () => {
if (document.body.hasAttribute("aria-busy")) return;
state.pageSelection = new Set(state.pageSelection.size === state.doc.pageCount ? [] : state.doc.pages.map(page => page.id));
syncPageSelection();
updateControls();
});
el.finishPageSelection.addEventListener("click", () => {
state.pageMultiSelect = false;
syncPageSelection();
updateControls();
});
let cancelPageTouch = null;
el.pageList.addEventListener("pointerdown", event => {
cancelPageTouch?.();
state.pageTouchPointer = null;
if (!state.editing || event.pointerType !== "touch" || !event.isPrimary || document.body.hasAttribute("aria-busy")) return;
const button = event.target.closest(".page-list-item");
if (!button || event.target.closest(".thumb-grip")) return;
const seq = state.openSeq, pointer = event.pointerId;
const timer = setTimeout(() => {
if (seq !== state.openSeq || !state.editing || document.body.hasAttribute("aria-busy")) return;
if (!state.pageMultiSelect) state.pageSelection.clear();
state.pageMultiSelect = true;
state.pageTouchPointer = pointer;
state.pageSelection.add(state.doc.pages[Number(button.dataset.pageIndex)].id);
syncPageSelection();
updateControls();
}, 450);
const finish = next => {
if (next.pointerId !== pointer) return;
cancelPageTouch?.();
};
cancelPageTouch = () => {
clearTimeout(timer);
el.pageList.removeEventListener("pointermove", move);
el.pageList.removeEventListener("pointerup", finish);
el.pageList.removeEventListener("pointercancel", finish);
el.pageList.removeEventListener("lostpointercapture", finish);
cancelPageTouch = null;
};
const move = next => {
if (Math.hypot(next.clientX - event.clientX, next.clientY - event.clientY) > 8) finish(next);
};
el.pageList.addEventListener("pointermove", move);
el.pageList.addEventListener("pointerup", finish);
el.pageList.addEventListener("pointercancel", finish);
el.pageList.addEventListener("lostpointercapture", finish);
});
el.pageList.addEventListener("contextmenu", event => {
if (state.pageMultiSelect) event.preventDefault();
});
el.pageList.addEventListener("keydown", event => { el.pageList.addEventListener("keydown", event => {
if (!state.editing || event.isComposing || document.body.hasAttribute("aria-busy")) return; if (!state.editing || event.isComposing || document.body.hasAttribute("aria-busy")) return;
if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "a") { if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "a") {
@@ -4318,6 +4407,7 @@ el.pageList.addEventListener("keydown", event => {
} else if (event.key === "Escape") { } else if (event.key === "Escape") {
state.pageSelection.clear(); state.pageSelection.clear();
state.pageSelectionAnchor = null; state.pageSelectionAnchor = null;
state.pageMultiSelect = false;
} else return; } else return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -4382,9 +4472,19 @@ function resetThumbnails() {
} }
} }
function pageDropIndex(from, target, after) { function pageDropIndex(indexes, target, after) {
const position = target + Number(after); const position = target + Number(after);
return position > from ? position - 1 : position; return position - indexes.filter(index => index < position).length;
}
function selectDragPages(index) {
if (!state.pageSelection.has(state.doc.pages[index].id)) {
state.pageSelection = new Set([state.doc.pages[index].id]);
state.pageSelectionAnchor = state.doc.pages[index].id;
syncPageSelection();
updateControls();
}
return selectedPageIndexes();
} }
function enablePageDrag(button, index) { function enablePageDrag(button, index) {
@@ -4399,16 +4499,21 @@ function enablePageDrag(button, index) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const seq = state.openSeq, pointer = event.pointerId; const seq = state.openSeq, pointer = event.pointerId;
let x = event.clientX, y = event.clientY, target = index, marker = null, frame = 0, dragging = false; const indexes = selectDragPages(index);
let x = event.clientX, y = event.clientY, position = null, marker = null, frame = 0, dragging = false;
const clearMarker = () => { marker?.classList.remove("drop-before", "drop-after"); marker = null; }; const clearMarker = () => { marker?.classList.remove("drop-before", "drop-after"); marker = null; };
const locate = () => { const locate = () => {
clearMarker(); clearMarker();
target = index; position = null;
const node = document.elementFromPoint(x, y)?.closest(".page-list-item"); const node = document.elementFromPoint(x, y)?.closest(".page-list-item");
if (!node || !el.pageList.contains(node)) return; if (!node || !el.pageList.contains(node)) return;
const rect = node.getBoundingClientRect(), after = y > rect.top + rect.height / 2; const rect = node.getBoundingClientRect(), after = y > rect.top + rect.height / 2;
target = pageDropIndex(index, Number(node.dataset.pageIndex), after); const target = Number(node.dataset.pageIndex), to = pageDropIndex(indexes, target, after);
if (target !== index) { marker = node; marker.classList.add(after ? "drop-after" : "drop-before"); } if (indexes.some((index, offset) => index !== to + offset)) {
position = target + Number(after);
marker = node;
marker.classList.add(after ? "drop-after" : "drop-before");
}
}; };
const tick = () => { const tick = () => {
if (seq !== state.openSeq || !state.editing) { finish(false); return; } if (seq !== state.openSeq || !state.editing) { finish(false); return; }
@@ -4436,8 +4541,8 @@ function enablePageDrag(button, index) {
clearMarker(); clearMarker();
button.classList.remove("reordering"); button.classList.remove("reordering");
if (grip.hasPointerCapture(pointer)) grip.releasePointerCapture(pointer); if (grip.hasPointerCapture(pointer)) grip.releasePointerCapture(pointer);
if (commit && dragging && target !== index && seq === state.openSeq && await canvasEditor.commitText() && await canvasEditor.commitCrop()) { if (commit && dragging && position !== null && seq === state.openSeq && await canvasEditor.commitText() && await canvasEditor.commitCrop()) {
await changeDocument("ofdgoChangePage", null, "move", index, target); await changeDocument("ofdgoBatchPages", null, "move", selectedPageRange(indexes), position);
} }
}; };
const up = next => { if (next.pointerId === pointer) finish(true); }; const up = next => { if (next.pointerId === pointer) finish(true); };
@@ -4452,9 +4557,11 @@ function enablePageDrag(button, index) {
button.addEventListener("keydown", async event => { button.addEventListener("keydown", async event => {
if (!event.altKey || !["ArrowUp", "ArrowDown"].includes(event.key) || document.body.hasAttribute("aria-busy")) return; if (!event.altKey || !["ArrowUp", "ArrowDown"].includes(event.key) || document.body.hasAttribute("aria-busy")) return;
event.preventDefault(); event.preventDefault();
const target = index + (event.key === "ArrowUp" ? -1 : 1); const indexes = selectDragPages(index), direction = event.key === "ArrowUp" ? -1 : 1;
const target = direction < 0 ? indexes[0] - 1 : indexes.at(-1) + 1;
if (target >= 0 && target < state.doc.pageCount && await canvasEditor.commitText() && await canvasEditor.commitCrop()) { if (target >= 0 && target < state.doc.pageCount && await canvasEditor.commitText() && await canvasEditor.commitCrop()) {
if (await changeDocument("ofdgoChangePage", null, "move", index, target)) el.pageList.querySelector(`[data-page-index="${target}"]`)?.focus(); const focus = (direction < 0 ? target : target - indexes.length + 1) + indexes.indexOf(index);
if (await changeSelectedPages("move", direction)) el.pageList.querySelector(`[data-page-index="${focus}"]`)?.focus();
} }
}); });
} }
@@ -5329,12 +5436,13 @@ function updateEditorTools() {
const pagesDisabled = !state.editing || !state.ready || state.exporting; const pagesDisabled = !state.editing || !state.ready || state.exporting;
el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert"); el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert");
el.saveButton.disabled = el.addPageButton.disabled = pagesDisabled; el.saveButton.disabled = el.addPageButton.disabled = pagesDisabled;
el.batchPagesButton.disabled = pagesDisabled;
const selectedPages = state.editing ? selectedPageIndexes() : [state.pageIndex]; const selectedPages = state.editing ? selectedPageIndexes() : [state.pageIndex];
el.copyPageButton.disabled = pagesDisabled || selectedPages.some(index => !pageCan("copy", index)); const selectionDisabled = pagesDisabled || !selectedPages.length;
el.pageSettingsButton.disabled = pagesDisabled || !pageCan("resize"); el.batchPagesButton.disabled = selectionDisabled;
el.deletePageButton.disabled = pagesDisabled || selectedPages.some(index => !pageCan("delete", index)) || selectedPages.length >= state.doc?.pageCount; el.copyPageButton.disabled = selectionDisabled || selectedPages.some(index => !pageCan("copy", index));
const moveDisabled = pagesDisabled || selectedPages.some(index => !pageCan("move", index)); el.pageSettingsButton.disabled = selectionDisabled || selectedPages.some(index => !pageCan("resize", index));
el.deletePageButton.disabled = selectionDisabled || selectedPages.some(index => !pageCan("delete", index)) || selectedPages.length >= state.doc?.pageCount;
const moveDisabled = selectionDisabled || selectedPages.some(index => !pageCan("move", index));
el.movePagePrevButton.disabled = moveDisabled || selectedPages[0] === 0; el.movePagePrevButton.disabled = moveDisabled || selectedPages[0] === 0;
el.movePageNextButton.disabled = moveDisabled || selectedPages.at(-1) === state.doc?.pageCount - 1; el.movePageNextButton.disabled = moveDisabled || selectedPages.at(-1) === state.doc?.pageCount - 1;
const enabled = state.editing && state.selectObjects && !state.panMode; const enabled = state.editing && state.selectObjects && !state.panMode;
+5 -2
View File
@@ -1075,8 +1075,8 @@ func changePage(args []js.Value) (any, error) {
return editorPageInfo{info, index}, nil return editorPageInfo{info, index}, nil
} }
// batchPages 按页码范围批量复制、删除或移动页面 // batchPages 按页码范围批量复制、删除、移动或调整页面尺寸
// 入参: args 操作、页码表达式及原页面序列中的插入位置 // 入参: args 操作、页码表达式及插入位置或宽高
// 返回: any 文档信息及目标页, error 错误信息 // 返回: any 文档信息及目标页, error 错误信息
func batchPages(args []js.Value) (any, error) { func batchPages(args []js.Value) (any, error) {
index := 0 index := 0
@@ -1113,6 +1113,9 @@ func batchPages(args []js.Value) (any, error) {
} }
} }
return currentEditor.MovePages(indexes, index) return currentEditor.MovePages(indexes, index)
case "resize":
index = indexes[0]
return currentEditor.ResizePages(indexes, args[2].Float(), args[3].Float())
default: default:
return fmt.Errorf("unsupported page action %q", args[0].String()) return fmt.Errorf("unsupported page action %q", args[0].String())
} }
+34 -8
View File
@@ -420,22 +420,48 @@ func (e *Editor) reorderPages(order []int) {
// 入参: index 页面索引, width 页面宽度, height 页面高度 // 入参: index 页面索引, width 页面宽度, height 页面高度
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) ResizePage(index int, width, height float64) error { func (e *Editor) ResizePage(index int, width, height float64) error {
page, err := e.page(index) return e.ResizePages([]int{index}, width, height)
if err != nil { }
// ResizePages 统一调整指定页面尺寸,不缩放或移动对象,一次操作计入一条撤销记录
// 入参: indexes 不重复的页面索引, width 页面宽度, height 页面高度
// 返回: error 错误信息
func (e *Editor) ResizePages(indexes []int, width, height float64) error {
if err := e.validatePageIndexes(indexes); err != nil {
return err return err
} }
if !finite(width) || !finite(height) || width <= 0 || height <= 0 { if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
return fmt.Errorf("page dimensions must be finite and positive") return fmt.Errorf("page dimensions must be finite and positive")
} }
before, after := page.Area.PhysicalBox, fmt.Sprintf("0 0 %s %s", ofdNumber(width), ofdNumber(height)) after := fmt.Sprintf("0 0 %s %s", ofdNumber(width), ofdNumber(height))
if before == after { before := make([]string, len(indexes))
changed := false
for i, index := range indexes {
page, err := e.page(index)
if err != nil {
return err
}
before[i] = page.Area.PhysicalBox
changed = changed || before[i] != after
}
if !changed {
return nil return nil
} }
page.Area.PhysicalBox = after indexes = slices.Clone(indexes)
if change := e.recordChange(); change != nil { resize := func(e *Editor) {
change.undo = func(e *Editor) { e.pages[index].Area.PhysicalBox = before } for _, index := range indexes {
change.redo = func(e *Editor) { e.pages[index].Area.PhysicalBox = after } e.pages[index].Area.PhysicalBox = after
} }
}
if change := e.recordChange(); change != nil {
change.undo = func(e *Editor) {
for i, index := range indexes {
e.pages[index].Area.PhysicalBox = before[i]
}
}
change.redo = resize
}
resize(e)
return nil return nil
} }
+24 -2
View File
@@ -88,6 +88,7 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
// WritePagesTo 按指定顺序将页面另存为独立OFD,不修改当前文档或撤销记录 // WritePagesTo 按指定顺序将页面另存为独立OFD,不修改当前文档或撤销记录
// 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转 // 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转
// 快照只准备所选页面,关联引用由页面导入流程统一筛选
// 签名凭据不代表另存文件验签有效;进度与取消复用OnWriteProgress,另含snapshot、ids和commit阶段 // 签名凭据不代表另存文件验签有效;进度与取消复用OnWriteProgress,另含snapshot、ids和commit阶段
// 入参: writer 输出流, indexes 从0开始的页面索引,不可重复或为空 // 入参: writer 输出流, indexes 从0开始的页面索引,不可重复或为空
// 返回: int64 已写入字节数, error 错误信息 // 返回: int64 已写入字节数, error 错误信息
@@ -95,11 +96,32 @@ func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (int64, error) {
if len(indexes) == 0 { if len(indexes) == 0 {
return 0, fmt.Errorf("no pages selected") return 0, fmt.Errorf("no pages selected")
} }
if err := e.validatePageIndexes(indexes); err != nil {
return 0, err
}
progress := editorProgress(e.OnWriteProgress) progress := editorProgress(e.OnWriteProgress)
if err := progress.report("snapshot", 0, 0); err != nil { if err := progress.report("snapshot", 0, 0); err != nil {
return 0, err return 0, err
} }
reader, err := e.reader(progress) snapshot := *e
snapshot.pages = make([]PageContent, len(indexes))
if e.source != nil {
source := *e.source
source.pages = make(map[string]*editorSourcePage, len(indexes))
for _, index := range indexes {
id := e.pages[index].ID
if page := e.source.pages[id]; page != nil {
source.pages[id] = page
}
}
snapshot.source = &source
}
positions := make([]int, len(indexes))
for i, index := range indexes {
snapshot.pages[i] = e.pages[index]
positions[i] = i
}
reader, err := snapshot.reader(progress)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -109,7 +131,7 @@ func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (int64, error) {
selected.Info = cloneEditorData(e.Info) selected.Info = cloneEditorData(e.Info)
selected.Info.DocID = id selected.Info.DocID = id
selected.OnWriteProgress = e.OnWriteProgress selected.OnWriteProgress = e.OnWriteProgress
if _, err := selected.ImportPagesWithOptions(reader, indexes, 0, PageImportOptions{Outlines: true, OnProgress: e.OnWriteProgress}); err != nil { if _, err := selected.ImportPagesWithOptions(reader, positions, 0, PageImportOptions{Outlines: true, OnProgress: e.OnWriteProgress}); err != nil {
return 0, err return 0, err
} }
return selected.WriteTo(writer) return selected.WriteTo(writer)