mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
0385e8c5fd
commit
c084682351
8 files changed
+389
-69
No files matched your search
+16
-9
@@ -332,6 +332,21 @@
|
||||
<div class="form-actions"><div id="paragraphStatus" class="form-status" role="status"></div><button id="paragraphCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="documentPanel" class="form-dialog document-dialog" aria-labelledby="documentTitle">
|
||||
<form id="documentForm">
|
||||
<h2 id="documentTitle" class="form-title">文档管理</h2>
|
||||
<div id="documentList" class="document-list" role="listbox" aria-label="文档列表"></div>
|
||||
<div id="documentActions" class="document-actions" role="group" aria-label="文档操作">
|
||||
<button id="documentAddButton" class="button" type="button" title="新增文档" aria-label="新增文档" aria-haspopup="dialog" aria-controls="createPanel">新增</button>
|
||||
<button id="infoButton" class="button" type="button" title="修改文档" aria-label="修改文档信息" aria-haspopup="dialog" aria-controls="infoPanel">修改</button>
|
||||
<button id="documentDeleteButton" class="button" type="button" title="删除文档" aria-label="删除文档">删除</button>
|
||||
<button id="documentUpButton" class="button" type="button" title="前移文档" aria-label="前移文档">前移</button>
|
||||
<button id="documentDownButton" class="button" type="button" title="后移文档" aria-label="后移文档">后移</button>
|
||||
<button id="documentClose" class="button" type="button">关闭</button>
|
||||
</div>
|
||||
<div id="documentStatus" class="form-status" role="status"></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="infoPanel" class="form-dialog" aria-label="文档信息">
|
||||
<form id="infoForm">
|
||||
<label class="form-row"><span>标题</span><input id="infoTitle" type="text" autofocus></label>
|
||||
@@ -594,19 +609,11 @@
|
||||
</section>
|
||||
<div class="panel-row">
|
||||
<div class="panel-title compact">文档</div>
|
||||
<div id="documentActions" class="panel-actions" hidden>
|
||||
<button id="documentAddButton" class="small-button" type="button" title="新建文档" aria-haspopup="dialog" aria-controls="createPanel">新建</button>
|
||||
<button id="infoButton" class="small-button" type="button" title="修改文档" aria-label="修改文档信息" aria-haspopup="dialog" aria-controls="infoPanel" hidden>修改</button>
|
||||
<button id="documentDeleteButton" class="small-button" type="button" title="删除文档">删除</button>
|
||||
</div>
|
||||
<button id="documentManageButton" class="small-button" type="button" title="文档管理" aria-label="文档管理" aria-haspopup="dialog" aria-controls="documentPanel" hidden>管理</button>
|
||||
</div>
|
||||
<div id="editNotice" class="font-permission-hint" role="status" hidden></div>
|
||||
<div id="documentRow" class="document-row" hidden>
|
||||
<select id="documentSelect" class="document-select" aria-label="选择文档" title="切换文档"></select>
|
||||
<div id="documentOrder" class="panel-actions" hidden>
|
||||
<button id="documentUpButton" class="small-button" type="button" title="前移文档" aria-label="前移文档">↑</button>
|
||||
<button id="documentDownButton" class="small-button" type="button" title="后移文档" aria-label="后移文档">↓</button>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="meta-list">
|
||||
<div>
|
||||
|
||||
+80
-10
@@ -1429,6 +1429,7 @@ body[data-hide-meta] .meta-panel {
|
||||
.viewer-panel,
|
||||
.search-results,
|
||||
.editor-toolbar,
|
||||
.document-list,
|
||||
.meta-panel {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--line-strong) transparent;
|
||||
@@ -1523,16 +1524,6 @@ body[data-hide-meta] .meta-panel {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.document-row .panel-actions {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.document-row .small-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.document-select {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
@@ -1542,6 +1533,81 @@ body[data-hide-meta] .meta-panel {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.document-dialog {
|
||||
width: 440px;
|
||||
}
|
||||
|
||||
.document-dialog .form-status:not(:empty) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.document-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.document-actions > .button {
|
||||
min-width: 0;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
|
||||
.document-list {
|
||||
max-height: 45dvh;
|
||||
min-height: 80px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--line);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.document-entry {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 4em;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.document-entry:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.document-entry:hover,
|
||||
.document-entry[aria-selected="true"] {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.document-entry[aria-selected="true"] {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.document-entry:focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.document-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.document-pages {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.document-pages {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.app-panel {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
@@ -3127,6 +3193,10 @@ audio.page-media {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.document-actions {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.tool-group + .tool-group::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
+156
-19
@@ -60,6 +60,9 @@ const state = {
|
||||
imageImporting: false,
|
||||
createInsert: false,
|
||||
createPackage: false,
|
||||
createDocumentAt: null,
|
||||
documentManagement: null,
|
||||
infoDocumentIndex: null,
|
||||
pageSelection: new Set(),
|
||||
pageSelectionAnchor: null,
|
||||
pageMultiSelect: false,
|
||||
@@ -235,8 +238,13 @@ const el = {
|
||||
infoButton: document.querySelector("#infoButton"),
|
||||
documentSelect: document.querySelector("#documentSelect"),
|
||||
documentActions: document.querySelector("#documentActions"),
|
||||
documentManageButton: document.querySelector("#documentManageButton"),
|
||||
documentPanel: document.querySelector("#documentPanel"),
|
||||
documentForm: document.querySelector("#documentForm"),
|
||||
documentList: document.querySelector("#documentList"),
|
||||
documentStatus: document.querySelector("#documentStatus"),
|
||||
documentClose: document.querySelector("#documentClose"),
|
||||
documentRow: document.querySelector("#documentRow"),
|
||||
documentOrder: document.querySelector("#documentOrder"),
|
||||
documentAddButton: document.querySelector("#documentAddButton"),
|
||||
documentDeleteButton: document.querySelector("#documentDeleteButton"),
|
||||
documentUpButton: document.querySelector("#documentUpButton"),
|
||||
@@ -679,30 +687,54 @@ editorClick(el.paragraphButton, () => {
|
||||
});
|
||||
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
|
||||
editorClick(el.documentAddButton, () => openCreatePanel(false, true));
|
||||
editorClick(el.documentManageButton, openDocumentManagement);
|
||||
el.documentClose.addEventListener("click", () => el.documentPanel.close());
|
||||
el.documentPanel.addEventListener("cancel", event => { if (el.documentForm.inert) event.preventDefault(); });
|
||||
el.documentPanel.addEventListener("close", () => {
|
||||
if (!el.documentPanel.open) {
|
||||
state.documentManagement = null;
|
||||
el.documentList.replaceChildren();
|
||||
el.documentStatus.textContent = "";
|
||||
}
|
||||
});
|
||||
el.documentList.addEventListener("keydown", event => {
|
||||
if (!state.documentManagement || !["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
const count = state.doc.documents.length, index = state.documentManagement.index;
|
||||
selectManagedDocument(event.key === "Home" ? 0 : event.key === "End" ? count - 1
|
||||
: Math.max(0, Math.min(count - 1, index + (event.key === "ArrowUp" ? -1 : 1))), true);
|
||||
});
|
||||
editorClick(el.documentDeleteButton, () => {
|
||||
if (window.confirm("删除当前文档?")) return changeDocument("ofdgoChangeDocument", null, "delete", state.doc.documentIndex || 0);
|
||||
const index = managedDocumentIndex(), entry = state.doc.documents[index];
|
||||
if (window.confirm(`删除文档“${entry.title || "未命名"}”?`)) return changeDocument("ofdgoChangeDocument", null, "delete", index);
|
||||
});
|
||||
editorClick(el.documentUpButton, () => {
|
||||
const index = state.doc.documentIndex || 0;
|
||||
const index = managedDocumentIndex();
|
||||
return changeDocument("ofdgoChangeDocument", null, "move", index, index - 1);
|
||||
});
|
||||
editorClick(el.documentDownButton, () => {
|
||||
const index = state.doc.documentIndex || 0;
|
||||
const index = managedDocumentIndex();
|
||||
return changeDocument("ofdgoChangeDocument", null, "move", index, index + 1);
|
||||
});
|
||||
editorClick(el.infoButton, () => {
|
||||
for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = state.doc[key.toLowerCase()] || "";
|
||||
state.infoDocumentIndex = managedDocumentIndex();
|
||||
const info = state.documentManagement?.entries?.[state.infoDocumentIndex] || state.doc;
|
||||
for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = info[key.toLowerCase()] || "";
|
||||
el.infoCustomRows.replaceChildren();
|
||||
for (const [index, field] of (state.doc.customData || []).entries()) addCustomDataRow(field, index);
|
||||
for (const [index, field] of (info.customData || []).entries()) addCustomDataRow(field, index);
|
||||
el.infoStatus.textContent = "";
|
||||
el.infoPanel.showModal();
|
||||
});
|
||||
el.infoCancel.addEventListener("click", () => el.infoPanel.close());
|
||||
el.infoPanel.addEventListener("cancel", event => { if (el.infoForm.inert) event.preventDefault(); });
|
||||
el.infoPanel.addEventListener("close", () => { if (!el.infoPanel.open) state.infoDocumentIndex = null; });
|
||||
el.infoCustomAdd.addEventListener("click", () => addCustomDataRow());
|
||||
el.infoForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
const fields = [...el.infoCustomRows.children].map(row => ({Name: row.children[0].value, Value: row.children[1].value, Source: row.sourceIndex}));
|
||||
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value, JSON.stringify(fields))) el.infoPanel.close();
|
||||
const args = [el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value, JSON.stringify(fields)];
|
||||
if (state.infoDocumentIndex !== null) args.push(state.infoDocumentIndex);
|
||||
if (await changeDocument("ofdgoUpdateInfo", null, ...args)) el.infoPanel.close();
|
||||
});
|
||||
|
||||
function addCustomDataRow(field = {Name: "", Value: ""}, sourceIndex) {
|
||||
@@ -1529,7 +1561,7 @@ function handleKeyDown(event) {
|
||||
}
|
||||
|
||||
function formDialogOpen() {
|
||||
return Boolean(sealPreview) || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
||||
return Boolean(sealPreview) || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open || el.documentPanel.open
|
||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||
}
|
||||
|
||||
@@ -1956,6 +1988,93 @@ function setEditorInfo(doc) {
|
||||
setDirty(state.packageDirty || state.encryptionDirty || doc.revision !== state.savedRevision);
|
||||
}
|
||||
|
||||
function managedDocumentIndex() {
|
||||
return state.documentManagement?.index ?? state.doc?.documentIndex ?? 0;
|
||||
}
|
||||
|
||||
function documentEntryKey(entry) {
|
||||
return JSON.stringify([entry?.root, entry?.id]);
|
||||
}
|
||||
|
||||
async function openDocumentManagement() {
|
||||
if (!state.editing || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return;
|
||||
const index = state.doc.documentIndex || 0;
|
||||
state.documentManagement = { index, key: documentEntryKey(state.doc.documents[index]), entries: null, loading: false, failed: false };
|
||||
renderDocumentManagement();
|
||||
el.documentPanel.showModal();
|
||||
await refreshDocumentManagement(true);
|
||||
}
|
||||
|
||||
async function refreshDocumentManagement(focus = false) {
|
||||
const manager = state.documentManagement, openSeq = state.openSeq;
|
||||
if (!manager || !el.documentPanel.open) return;
|
||||
manager.loading = true;
|
||||
manager.failed = false;
|
||||
el.documentStatus.textContent = "正在读取文档";
|
||||
updateDocumentControls();
|
||||
try {
|
||||
const entries = await callWASM("ofdgoPackageDocuments");
|
||||
if (state.documentManagement !== manager || !el.documentPanel.open) return;
|
||||
if (openSeq !== state.openSeq) return await refreshDocumentManagement(focus);
|
||||
const index = entries.findIndex(entry => documentEntryKey(entry) === manager.key);
|
||||
manager.index = index < 0 ? Math.min(manager.index, entries.length - 1) : index;
|
||||
manager.entries = entries;
|
||||
manager.key = documentEntryKey(entries[manager.index]);
|
||||
el.documentStatus.textContent = "";
|
||||
renderDocumentManagement();
|
||||
if (focus) el.documentList.children[manager.index]?.focus();
|
||||
} catch (error) {
|
||||
if (state.documentManagement === manager && openSeq === state.openSeq) {
|
||||
manager.failed = true;
|
||||
el.documentStatus.textContent = error.message;
|
||||
}
|
||||
} finally {
|
||||
if (state.documentManagement === manager) {
|
||||
manager.loading = false;
|
||||
updateDocumentControls();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderDocumentManagement() {
|
||||
const manager = state.documentManagement;
|
||||
if (!manager) return;
|
||||
const entries = manager.entries || state.doc.documents;
|
||||
el.documentList.replaceChildren(...entries.map(entry => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "document-entry";
|
||||
button.setAttribute("role", "option");
|
||||
button.setAttribute("aria-selected", String(entry.index === manager.index));
|
||||
button.tabIndex = entry.index === manager.index ? 0 : -1;
|
||||
const name = document.createElement("span");
|
||||
name.className = "document-name";
|
||||
name.textContent = `${entry.index + 1} · ${entry.title || "未命名"}`;
|
||||
button.title = name.textContent;
|
||||
const pages = document.createElement("span");
|
||||
pages.className = "document-pages";
|
||||
const count = entry.pageCount ?? (entry.index === state.doc.documentIndex ? state.doc.pageCount : null);
|
||||
pages.textContent = count === null ? "-" : `${count}页`;
|
||||
button.append(name, pages);
|
||||
button.addEventListener("click", () => selectManagedDocument(entry.index));
|
||||
return button;
|
||||
}));
|
||||
updateDocumentControls();
|
||||
}
|
||||
|
||||
function selectManagedDocument(index, focus = false) {
|
||||
const manager = state.documentManagement;
|
||||
if (!manager || manager.loading || document.body.hasAttribute("aria-busy") || index < 0 || index >= state.doc.documents.length) return;
|
||||
manager.index = index;
|
||||
manager.key = documentEntryKey(state.doc.documents[index]);
|
||||
for (const [i, button] of [...el.documentList.children].entries()) {
|
||||
button.setAttribute("aria-selected", String(i === index));
|
||||
button.tabIndex = i === index ? 0 : -1;
|
||||
}
|
||||
updateDocumentControls();
|
||||
if (focus) el.documentList.children[index]?.focus();
|
||||
}
|
||||
|
||||
async function switchDocument(index) {
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy") || state.exporting || formDialogOpen()) return;
|
||||
const previous = state.doc.documentIndex || 0;
|
||||
@@ -2087,9 +2206,10 @@ function openCreatePanel(insert = false, packageDocument = false) {
|
||||
el.createForm.reset();
|
||||
state.createInsert = insert === true;
|
||||
state.createPackage = packageDocument === true;
|
||||
state.createDocumentAt = state.createPackage ? managedDocumentIndex() + 1 : null;
|
||||
el.createMode.value = state.createInsert ? "images" : "blank";
|
||||
el.createSubmit.textContent = state.createInsert ? "插入" : "新建";
|
||||
el.createPanel.setAttribute("aria-label", state.createInsert ? "插入图片" : "新建文档");
|
||||
el.createSubmit.textContent = state.createInsert ? "插入" : state.createPackage ? "新增" : "新建";
|
||||
el.createPanel.setAttribute("aria-label", state.createInsert ? "插入图片" : state.createPackage ? "新增文档" : "新建文档");
|
||||
el.createCancel.disabled = false;
|
||||
updateCreateOptions();
|
||||
el.createStatus.textContent = "";
|
||||
@@ -2130,7 +2250,7 @@ async function createDocument(event) {
|
||||
event.preventDefault();
|
||||
if (state.createPackage) {
|
||||
if (await changeDocument("ofdgoChangeDocument", null, "add", el.createName.value.trim() || "未命名",
|
||||
Number(el.createWidth.value), Number(el.createHeight.value), (state.doc.documentIndex || 0) + 1)) el.createPanel.close();
|
||||
Number(el.createWidth.value), Number(el.createHeight.value), state.createDocumentAt)) el.createPanel.close();
|
||||
return;
|
||||
}
|
||||
if (state.createInsert) {
|
||||
@@ -2884,6 +3004,7 @@ async function changeDocument(name, item, ...args) {
|
||||
const restoring = name === "ofdgoUndo" || name === "ofdgoRedo";
|
||||
const { scrollLeft, scrollTop } = el.viewerPanel;
|
||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages";
|
||||
let refreshManagement = false;
|
||||
state.imageImporting = name === "ofdgoImportImages";
|
||||
if (importing) state.importing = true;
|
||||
setBusy(true, importing ? "正在导入页面" : "", null);
|
||||
@@ -2916,12 +3037,16 @@ async function changeDocument(name, item, ...args) {
|
||||
if (doc.revision === state.editorInfo?.revision) {
|
||||
return true;
|
||||
}
|
||||
refreshManagement = el.documentPanel.open && (name === "ofdgoChangeDocument" || name === "ofdgoUpdateInfo" || restoring);
|
||||
const previousDocument = state.doc.documents?.[state.doc.documentIndex || 0];
|
||||
const nextDocument = doc.documents?.[doc.documentIndex || 0];
|
||||
const documentChanged = previousDocument?.root !== nextDocument?.root || previousDocument?.id !== nextDocument?.id;
|
||||
const packageChanged = documentChanged || name === "ofdgoChangeDocument" || restoring
|
||||
&& JSON.stringify(doc.documents?.map(entry => [entry.root, entry.id])) !== JSON.stringify(state.doc.documents?.map(entry => [entry.root, entry.id]));
|
||||
if (packageChanged) {
|
||||
if (state.documentManagement && name === "ofdgoChangeDocument" && args[0] === "add") {
|
||||
state.documentManagement.key = documentEntryKey(nextDocument);
|
||||
}
|
||||
resetCompositeScope();
|
||||
state.objectClipboard = state.styleClipboard = null;
|
||||
state.pageSelection.clear();
|
||||
@@ -3010,6 +3135,7 @@ async function changeDocument(name, item, ...args) {
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
refreshManagement = false;
|
||||
if (openSeq === state.openSeq) {
|
||||
if (el.annotationCreate.open) {
|
||||
el.annotationCreateStatus.textContent = err.message;
|
||||
@@ -3037,6 +3163,8 @@ async function changeDocument(name, item, ...args) {
|
||||
el.createStatus.textContent = err.message;
|
||||
} else if (el.importPanel.open) {
|
||||
el.importStatus.textContent = err.message;
|
||||
} else if (el.documentPanel.open) {
|
||||
el.documentStatus.textContent = err.message;
|
||||
} else {
|
||||
showError(err, false);
|
||||
}
|
||||
@@ -3052,6 +3180,7 @@ async function changeDocument(name, item, ...args) {
|
||||
}
|
||||
if (openSeq === state.openSeq) {
|
||||
setBusy(false);
|
||||
if (refreshManagement) await refreshDocumentManagement();
|
||||
if (toolbarFocus?.isConnected && !toolbarFocus.disabled) toolbarFocus.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
@@ -3722,6 +3851,9 @@ async function openOFD(file, remote = null) {
|
||||
el.pagePanel.close();
|
||||
el.paragraphPanel.close();
|
||||
el.infoPanel.close();
|
||||
el.documentPanel.close();
|
||||
state.documentManagement = null;
|
||||
state.infoDocumentIndex = null;
|
||||
el.annotationNote.close();
|
||||
el.annotationCreate.close();
|
||||
el.objectPicker.close();
|
||||
@@ -8305,15 +8437,20 @@ function updateFitSpace() {
|
||||
}
|
||||
|
||||
function updateDocumentControls() {
|
||||
const count = state.doc?.documents?.length || 0, index = state.doc?.documentIndex || 0;
|
||||
const disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
|
||||
el.documentActions.hidden = el.documentOrder.hidden = !state.editing;
|
||||
const count = state.doc?.documents?.length || 0, index = managedDocumentIndex(), manager = state.documentManagement;
|
||||
const busy = document.body.hasAttribute("aria-busy");
|
||||
const disabled = !state.doc || !state.ready || state.exporting || busy;
|
||||
el.documentManageButton.hidden = !state.editing;
|
||||
el.documentManageButton.disabled = disabled;
|
||||
el.documentRow.hidden = !count || !state.editing && count < 2;
|
||||
el.documentSelect.disabled = disabled;
|
||||
el.documentAddButton.disabled = el.infoButton.disabled = disabled || !state.editing;
|
||||
el.documentDeleteButton.disabled = disabled || !state.editing || count <= 1;
|
||||
el.documentUpButton.disabled = disabled || !state.editing || index === 0;
|
||||
el.documentDownButton.disabled = disabled || !state.editing || index >= count - 1;
|
||||
const actionsDisabled = disabled || !state.editing || Boolean(manager?.loading || manager?.failed);
|
||||
el.documentList.inert = busy || Boolean(manager?.loading);
|
||||
el.documentAddButton.disabled = actionsDisabled;
|
||||
el.infoButton.disabled = actionsDisabled || Boolean(manager && !manager.entries);
|
||||
el.documentDeleteButton.disabled = actionsDisabled || count <= 1;
|
||||
el.documentUpButton.disabled = actionsDisabled || index === 0;
|
||||
el.documentDownButton.disabled = actionsDisabled || index >= count - 1;
|
||||
}
|
||||
|
||||
function updateControls() {
|
||||
@@ -8355,7 +8492,6 @@ function updateEditorTools() {
|
||||
el.editButton.title = state.editing ? "阅读" : "编辑";
|
||||
el.editButton.setAttribute("aria-label", el.editButton.title);
|
||||
el.editNotice.textContent = state.editorInfo?.editWarnings?.join(";") || "";
|
||||
el.infoButton.hidden = !state.editing;
|
||||
el.editNotice.hidden = !state.editing || !el.editNotice.textContent;
|
||||
el.editorTools.hidden = !state.editing;
|
||||
const pagesDisabled = !state.editing || !state.ready || state.exporting;
|
||||
@@ -8535,6 +8671,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
||||
el.pageForm.inert = busy;
|
||||
el.paragraphForm.inert = busy;
|
||||
el.infoForm.inert = busy;
|
||||
el.documentForm.inert = busy;
|
||||
el.attachmentForm.inert = busy;
|
||||
el.attachmentPanel.inert = busy;
|
||||
el.annotationForm.inert = busy;
|
||||
|
||||
+44
-4
@@ -186,6 +186,7 @@ func RunWASM() {
|
||||
registerCallback("ofdgoOpen", openDocument)
|
||||
registerCallback("ofdgoSelectDocument", selectDocument)
|
||||
registerCallback("ofdgoChangeDocument", changePackageDocument)
|
||||
registerCallback("ofdgoPackageDocuments", packageDocuments)
|
||||
registerAsyncCallback("ofdgoConvertPDF", convertPDFDocument)
|
||||
registerAsyncCallback("ofdgoConvertFile", convertFile)
|
||||
registerAsyncCallback("ofdgoPackFiles", packFiles)
|
||||
@@ -3285,11 +3286,21 @@ func createDocument(args []js.Value) (any, error) {
|
||||
}
|
||||
|
||||
// updateInfo 修改文档信息和自定义字段,保留其他元数据及创建程序标识
|
||||
// 入参: args 标题、作者、主题、可选自定义字段JSON
|
||||
// 入参: args 标题、作者、主题、可选自定义字段JSON及文档索引
|
||||
// 返回: any 编辑状态, error 错误信息
|
||||
func updateInfo(args []js.Value) (any, error) {
|
||||
return changeObjects(func() error {
|
||||
info := currentEditor.Info
|
||||
if len(args) < 3 || len(args) > 5 {
|
||||
return fmt.Errorf("invalid document info arguments")
|
||||
}
|
||||
index := currentEditor.DocumentIndex()
|
||||
if len(args) > 4 {
|
||||
index = args[4].Int()
|
||||
}
|
||||
info, err := currentEditor.DocumentInfo(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.Title, info.Author, info.Subject = args[0].String(), args[1].String(), args[2].String()
|
||||
if len(args) > 3 {
|
||||
var submitted []struct {
|
||||
@@ -3317,11 +3328,40 @@ func updateInfo(args []js.Value) (any, error) {
|
||||
info.CustomDatas = &ofdgo.CustomDatas{CustomData: fields}
|
||||
}
|
||||
}
|
||||
currentEditor.SetInfo(info)
|
||||
return nil
|
||||
return currentEditor.SetDocumentInfo(index, info)
|
||||
})
|
||||
}
|
||||
|
||||
// packageDocuments 按需读取管理列表的元数据和页数,不切换当前文档或解析页面内容
|
||||
// 入参: args 保留参数
|
||||
// 返回: any 文档列表, error 编辑状态错误
|
||||
func packageDocuments(args []js.Value) (any, error) {
|
||||
if currentSession == nil || currentEditor == nil {
|
||||
return nil, fmt.Errorf("no document is being edited")
|
||||
}
|
||||
entries := make([]DocumentEntry, currentEditor.DocumentCount())
|
||||
for i := range entries {
|
||||
info, err := currentEditor.DocumentInfo(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := &entries[i]
|
||||
body := currentSession.Reader.OFD.DocBody[i]
|
||||
entry.Index, entry.Root, entry.ID = i, body.DocRoot, info.DocID
|
||||
entry.Title, entry.Author, entry.Subject = info.Title, info.Author, info.Subject
|
||||
if info.CustomDatas != nil {
|
||||
entry.CustomData = info.CustomDatas.CustomData
|
||||
}
|
||||
if i == currentEditor.DocumentIndex() {
|
||||
count := currentEditor.PageCount()
|
||||
entry.PageCount = &count
|
||||
} else if count, err := currentSession.Reader.DocumentPageCount(i); err == nil {
|
||||
entry.PageCount = &count
|
||||
}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// changeAttachment 编辑附件,逐个读取文件并复用库层事务和历史
|
||||
// 入参: args 操作、附件标识、名称列表、文件读取回调
|
||||
// 返回: any 编辑状态, error 错误信息
|
||||
|
||||
@@ -98,12 +98,16 @@ type DocumentInfo struct {
|
||||
DetailsPending bool `json:"detailsPending,omitempty"`
|
||||
}
|
||||
|
||||
// DocumentEntry 包内文档入口,不预先解析其他文档的页面和资源
|
||||
// DocumentEntry 包内文档入口,管理列表按需补充元数据和页数
|
||||
type DocumentEntry struct {
|
||||
Index int `json:"index"`
|
||||
Title string `json:"title"`
|
||||
Root string `json:"root"`
|
||||
ID string `json:"id"`
|
||||
Index int `json:"index"`
|
||||
Title string `json:"title"`
|
||||
Root string `json:"root"`
|
||||
ID string `json:"id"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
CustomData []ofdgo.CustomData `json:"customData,omitempty"`
|
||||
PageCount *int `json:"pageCount,omitempty"`
|
||||
}
|
||||
|
||||
// DocumentDetails 不含页面数组的文档补充信息
|
||||
|
||||
+50
-19
@@ -23,6 +23,7 @@ import (
|
||||
"io"
|
||||
"maps"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
@@ -45,6 +46,50 @@ func (e *Editor) DocumentIndex() int {
|
||||
return e.source.reader.DocumentIndex()
|
||||
}
|
||||
|
||||
// DocumentInfo 获取指定文档信息的独立副本,不切换当前文档
|
||||
// 入参: index 文档索引,从0开始
|
||||
// 返回: DocInfo 文档信息, error 索引错误
|
||||
func (e *Editor) DocumentInfo(index int) (DocInfo, error) {
|
||||
if index < 0 || index >= e.DocumentCount() {
|
||||
return DocInfo{}, fmt.Errorf("document index %d out of range", index)
|
||||
}
|
||||
if index == e.DocumentIndex() {
|
||||
return cloneEditorData(e.Info), nil
|
||||
}
|
||||
info := cloneEditorData(e.source.reader.OFD.DocBody[index].DocInfo)
|
||||
if info.CustomDatas != nil {
|
||||
for i := range info.CustomDatas.CustomData {
|
||||
info.CustomDatas.CustomData[i].sourceIndex = i + 1
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// SetDocumentInfo 修改指定文档信息,保留扩展内容,不切换当前文档,可通过Undo撤销
|
||||
// 入参: index 文档索引,从0开始, info 文档信息
|
||||
// 返回: error 修改错误
|
||||
func (e *Editor) SetDocumentInfo(index int, info DocInfo) error {
|
||||
before, err := e.DocumentInfo(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reflect.DeepEqual(before, info) {
|
||||
return nil
|
||||
}
|
||||
if index == e.DocumentIndex() {
|
||||
e.SetInfo(info)
|
||||
return nil
|
||||
}
|
||||
return e.editDocuments(func(reader *Reader, data []byte, _ *editorXML) (int, error) {
|
||||
updated, err := editorUpdateInfoXML(data, NewEditor().Info.DocID, index, before, info)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reader.files[reader.fileNamesFold["ofd.xml"]] = updated
|
||||
return reader.DocumentIndex(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// AddDocument 在包内插入含一页的空白文档并选中,不修改输入包
|
||||
// 当前文档的修改保留,新文档使用独立目录和文档标识;可通过Undo撤销
|
||||
// 入参: title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量
|
||||
@@ -116,26 +161,12 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
|
||||
// 入参: index 文档索引,从0开始, title 标题,空值移除标题
|
||||
// 返回: error 修改错误
|
||||
func (e *Editor) RenameDocument(index int, title string) error {
|
||||
if index < 0 || index >= e.DocumentCount() {
|
||||
return fmt.Errorf("document index %d out of range", index)
|
||||
info, err := e.DocumentInfo(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if index == e.DocumentIndex() {
|
||||
info := e.Info
|
||||
info.Title = title
|
||||
e.SetInfo(info)
|
||||
return nil
|
||||
}
|
||||
if e.source.reader.OFD.DocBody[index].DocInfo.Title == title {
|
||||
return nil
|
||||
}
|
||||
return e.editDocuments(func(reader *Reader, data []byte, _ *editorXML) (int, error) {
|
||||
data, info, err := editorDocInfoXML(data, NewEditor().Info.DocID, index)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reader.files[reader.fileNamesFold["ofd.xml"]] = editorXMLSetText(data, info, [][2]string{{"Title", title}})
|
||||
return reader.DocumentIndex(), nil
|
||||
})
|
||||
info.Title = title
|
||||
return e.SetDocumentInfo(index, info)
|
||||
}
|
||||
|
||||
// MoveDocument 调整包内文档顺序,当前文档随条目移动,不重写内容,可通过Undo撤销
|
||||
|
||||
@@ -29,16 +29,23 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, info, err := editorDocInfoXML(data, e.source.fallbackDocID, e.source.reader.documentIndex)
|
||||
return editorUpdateInfoXML(data, e.source.fallbackDocID, e.source.reader.documentIndex, e.source.info, e.Info)
|
||||
}
|
||||
|
||||
// editorUpdateInfoXML 更新指定文档的描述和自定义字段,保留未修改内容
|
||||
// 入参: data 根索引XML, docID 缺少描述时使用的标识, index 文档索引, before 原信息, after 新信息
|
||||
// 返回: []byte 修改后的XML, error 解析错误
|
||||
func editorUpdateInfoXML(data []byte, docID string, index int, before, after DocInfo) ([]byte, error) {
|
||||
data, info, err := editorDocInfoXML(data, docID, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
before, after := e.source.info, e.Info
|
||||
var values [][2]string
|
||||
for _, field := range [][3]string{
|
||||
{"DocID", before.DocID, after.DocID}, {"Title", before.Title, after.Title}, {"Author", before.Author, after.Author},
|
||||
{"Subject", before.Subject, after.Subject}, {"Abstract", before.Abstract, after.Abstract},
|
||||
{"CreationDate", before.CreationDate, after.CreationDate}, {"ModDate", before.ModDate, after.ModDate},
|
||||
{"Creator", before.Creator, after.Creator}, {"CreatorVersion", before.CreatorVersion, after.CreatorVersion},
|
||||
} {
|
||||
if field[1] != field[2] {
|
||||
values = append(values, [2]string{field[0], field[2]})
|
||||
@@ -50,7 +57,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return editorCustomDatas(data, root.childAt("DocBody", e.source.reader.documentIndex).child("DocInfo"), after.CustomDatas)
|
||||
return editorCustomDatas(data, root.childAt("DocBody", index).child("DocInfo"), after.CustomDatas)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -83,6 +83,30 @@ func (r *Reader) DocumentIndex() int {
|
||||
return r.documentIndex
|
||||
}
|
||||
|
||||
// DocumentPageCount 获取指定文档页数,不切换当前文档或加载资源、注释和签名
|
||||
// 入参: index 文档索引,从0开始
|
||||
// 返回: int 页数, error 错误信息
|
||||
func (r *Reader) DocumentPageCount(index int) (int, error) {
|
||||
if index < 0 || index >= r.DocumentCount() {
|
||||
return 0, fmt.Errorf("document index %d out of range", index)
|
||||
}
|
||||
if index == r.documentIndex && r.doc != nil {
|
||||
return len(r.doc.Pages.Page), nil
|
||||
}
|
||||
data, err := r.readFile(r.OFD.DocBody[index].DocRoot)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var doc struct {
|
||||
XMLName xml.Name `xml:"Document"`
|
||||
Pages Pages `xml:"Pages"`
|
||||
}
|
||||
if err := xml.Unmarshal(data, &doc); err != nil {
|
||||
return 0, fmt.Errorf("failed to unmarshal document.xml: %w", err)
|
||||
}
|
||||
return len(doc.Pages.Page), nil
|
||||
}
|
||||
|
||||
// Document 创建指定文档的独立阅读器,不改变当前文档或混用资源缓存
|
||||
// 返回的阅读器不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器
|
||||
// 入参: index 文档索引,从0开始
|
||||
|
||||
Reference in new issue
Block a user