mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
This commit is contained in:
1 parent
13eced8dd8
commit
2cc6c121f8
6 files changed
+224
-39
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user