mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
5bc92dddd1
commit
89bbbba46e
4 files changed
+239
-3
No files matched your search
@@ -348,6 +348,23 @@
|
||||
<div id="documentStatus" class="form-status" role="status"></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="versionPanel" class="form-dialog document-dialog" aria-labelledby="versionTitle">
|
||||
<form id="versionForm">
|
||||
<h2 id="versionTitle" class="form-title">版本管理</h2>
|
||||
<div id="versionNotice" class="font-permission-hint" role="status" hidden></div>
|
||||
<div id="versionList" class="document-list" role="listbox" aria-label="版本列表"></div>
|
||||
<label class="form-row"><span>名称</span><input id="versionName" type="text" autocomplete="off" aria-label="版本名称"></label>
|
||||
<div class="document-actions" role="group" aria-label="版本操作">
|
||||
<button id="versionAdd" class="button" type="button" title="新增版本" aria-label="新增版本">新增</button>
|
||||
<button id="versionRename" class="button" type="button" title="修改名称" aria-label="修改名称">改名</button>
|
||||
<button id="versionDefault" class="button" type="button" title="默认版本" aria-label="默认版本">默认</button>
|
||||
<button id="versionDelete" class="button" type="button" title="删除版本" aria-label="删除版本">删除</button>
|
||||
<button id="versionSelect" class="button" type="button" title="切换版本" aria-label="切换版本">切换</button>
|
||||
<button id="versionClose" class="button" type="button">关闭</button>
|
||||
</div>
|
||||
<div id="versionStatus" 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>
|
||||
@@ -620,8 +637,11 @@
|
||||
</section>
|
||||
<div class="panel-row">
|
||||
<div class="panel-title compact">文档</div>
|
||||
<div class="panel-actions">
|
||||
<button id="versionManageButton" class="small-button" type="button" title="版本管理" aria-label="版本管理" aria-haspopup="dialog" aria-controls="versionPanel" hidden>版本</button>
|
||||
<button id="documentManageButton" class="small-button" type="button" title="文档管理" aria-label="文档管理" aria-haspopup="dialog" aria-controls="documentPanel" hidden>管理</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
+150
-2
@@ -64,6 +64,7 @@ const state = {
|
||||
createPackage: false,
|
||||
createDocumentAt: null,
|
||||
documentManagement: null,
|
||||
versionManagement: null,
|
||||
infoDocumentIndex: null,
|
||||
pageSelection: new Set(),
|
||||
pageSelectionAnchor: null,
|
||||
@@ -257,6 +258,19 @@ const el = {
|
||||
documentDeleteButton: document.querySelector("#documentDeleteButton"),
|
||||
documentUpButton: document.querySelector("#documentUpButton"),
|
||||
documentDownButton: document.querySelector("#documentDownButton"),
|
||||
versionManageButton: document.querySelector("#versionManageButton"),
|
||||
versionPanel: document.querySelector("#versionPanel"),
|
||||
versionForm: document.querySelector("#versionForm"),
|
||||
versionNotice: document.querySelector("#versionNotice"),
|
||||
versionList: document.querySelector("#versionList"),
|
||||
versionName: document.querySelector("#versionName"),
|
||||
versionStatus: document.querySelector("#versionStatus"),
|
||||
versionAdd: document.querySelector("#versionAdd"),
|
||||
versionRename: document.querySelector("#versionRename"),
|
||||
versionDefault: document.querySelector("#versionDefault"),
|
||||
versionDelete: document.querySelector("#versionDelete"),
|
||||
versionSelect: document.querySelector("#versionSelect"),
|
||||
versionClose: document.querySelector("#versionClose"),
|
||||
infoPanel: document.querySelector("#infoPanel"),
|
||||
infoForm: document.querySelector("#infoForm"),
|
||||
infoTitle: document.querySelector("#infoTitle"),
|
||||
@@ -696,6 +710,33 @@ editorClick(el.paragraphButton, () => {
|
||||
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
|
||||
editorClick(el.documentAddButton, () => openCreatePanel(false, true));
|
||||
editorClick(el.documentManageButton, openDocumentManagement);
|
||||
editorClick(el.versionManageButton, openVersionManagement);
|
||||
el.versionClose.addEventListener("click", () => el.versionPanel.close());
|
||||
el.versionPanel.addEventListener("cancel", event => { if (el.versionForm.inert) event.preventDefault(); });
|
||||
el.versionPanel.addEventListener("close", () => {
|
||||
if (!el.versionPanel.open) {
|
||||
state.versionManagement = null;
|
||||
el.versionList.replaceChildren();
|
||||
el.versionName.value = el.versionStatus.textContent = "";
|
||||
}
|
||||
});
|
||||
el.versionForm.addEventListener("submit", event => event.preventDefault());
|
||||
el.versionName.addEventListener("input", updateVersionControls);
|
||||
for (const [button, action] of [[el.versionAdd, "add"], [el.versionRename, "rename"], [el.versionDefault, "default"], [el.versionDelete, "delete"], [el.versionSelect, "select"]]) {
|
||||
button.addEventListener("click", () => changeVersion(action));
|
||||
}
|
||||
el.versionList.addEventListener("keydown", event => {
|
||||
const manager = state.versionManagement;
|
||||
if (!manager?.entries || manager.loading || document.body.hasAttribute("aria-busy")) return;
|
||||
let index = manager.entries.findIndex(entry => entry.id === manager.id);
|
||||
if (event.key === "ArrowUp") index--;
|
||||
else if (event.key === "ArrowDown") index++;
|
||||
else if (event.key === "Home") index = 0;
|
||||
else if (event.key === "End") index = manager.entries.length - 1;
|
||||
else return;
|
||||
event.preventDefault();
|
||||
selectManagedVersion(manager.entries[Math.max(0, Math.min(index, manager.entries.length - 1))].id, true);
|
||||
});
|
||||
el.documentClose.addEventListener("click", () => el.documentPanel.close());
|
||||
el.documentPanel.addEventListener("cancel", event => { if (el.documentForm.inert) event.preventDefault(); });
|
||||
el.documentPanel.addEventListener("close", () => {
|
||||
@@ -1610,7 +1651,7 @@ function handleKeyDown(event) {
|
||||
|
||||
function formDialogOpen() {
|
||||
return Boolean(sealPreview) || fontBindingElements.Panel.open || 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;
|
||||
|| el.versionPanel.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;
|
||||
}
|
||||
|
||||
function setObjectDimension(input, value) {
|
||||
@@ -2036,6 +2077,106 @@ function setEditorInfo(doc) {
|
||||
setDirty(state.packageDirty || state.encryptionDirty || doc.revision !== state.savedRevision);
|
||||
}
|
||||
|
||||
async function openVersionManagement() {
|
||||
if (!state.editing || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return;
|
||||
state.versionManagement = { id: state.doc.documentVersion || "", entries: null, loading: false, failed: false };
|
||||
el.versionList.replaceChildren();
|
||||
el.versionName.value = "";
|
||||
el.versionPanel.showModal();
|
||||
await refreshVersionManagement(true);
|
||||
}
|
||||
|
||||
async function refreshVersionManagement(focus = false) {
|
||||
const manager = state.versionManagement, openSeq = state.openSeq;
|
||||
if (!manager || !el.versionPanel.open) return;
|
||||
manager.loading = true;
|
||||
manager.failed = false;
|
||||
el.versionStatus.textContent = "正在读取版本";
|
||||
updateVersionControls();
|
||||
try {
|
||||
const entries = await callWASM("ofdgoDocumentVersions");
|
||||
if (state.versionManagement !== manager || !el.versionPanel.open) return;
|
||||
if (openSeq !== state.openSeq) return refreshVersionManagement(focus);
|
||||
manager.entries = entries;
|
||||
if (!entries.some(entry => entry.id === manager.id)) manager.id = state.doc.documentVersion || "";
|
||||
el.versionList.replaceChildren(...entries.map(entry => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "document-entry";
|
||||
button.setAttribute("role", "option");
|
||||
button.setAttribute("aria-current", String(entry.id === (state.doc.documentVersion || "")));
|
||||
const name = document.createElement("span"), marker = document.createElement("span");
|
||||
name.className = "document-name";
|
||||
name.textContent = entry.id ? entry.name || `版本 ${entry.index}` : "主文档";
|
||||
marker.className = "document-pages";
|
||||
marker.textContent = entry.default ? "默认" : "";
|
||||
button.title = name.textContent;
|
||||
button.append(name, marker);
|
||||
button.addEventListener("click", () => selectManagedVersion(entry.id));
|
||||
return button;
|
||||
}));
|
||||
manager.loading = false;
|
||||
el.versionStatus.textContent = "";
|
||||
selectManagedVersion(manager.id, focus);
|
||||
} catch (err) {
|
||||
if (state.versionManagement !== manager || !el.versionPanel.open) return;
|
||||
if (openSeq !== state.openSeq) return refreshVersionManagement(focus);
|
||||
manager.failed = true;
|
||||
el.versionStatus.textContent = err.message;
|
||||
} finally {
|
||||
if (state.versionManagement === manager && openSeq === state.openSeq) {
|
||||
manager.loading = false;
|
||||
updateVersionControls();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectManagedVersion(id, focus = false) {
|
||||
const manager = state.versionManagement;
|
||||
if (!manager || manager.loading || document.body.hasAttribute("aria-busy")) return;
|
||||
const index = manager.entries?.findIndex(entry => entry.id === id) ?? -1;
|
||||
if (index < 0) return;
|
||||
manager.id = id;
|
||||
el.versionName.value = manager.entries[index].name;
|
||||
Array.from(el.versionList.children).forEach((button, position) => {
|
||||
button.setAttribute("aria-selected", String(position === index));
|
||||
button.tabIndex = position === index ? 0 : -1;
|
||||
});
|
||||
updateVersionControls();
|
||||
if (focus) el.versionList.children[index]?.focus();
|
||||
}
|
||||
|
||||
async function changeVersion(action) {
|
||||
const manager = state.versionManagement;
|
||||
if (!state.editing || !manager?.entries || manager.loading || manager.failed || document.body.hasAttribute("aria-busy")) return;
|
||||
const entry = manager.entries.find(entry => entry.id === manager.id);
|
||||
if (!entry) return;
|
||||
if (action === "delete" && (!entry.id || !window.confirm(`删除版本“${entry.name || entry.index}”?`))) return;
|
||||
el.versionStatus.textContent = "正在处理版本";
|
||||
if (await changeDocument("ofdgoChangeVersion", null, action, entry.id, el.versionName.value.trim())) {
|
||||
if (state.versionManagement !== manager || !el.versionPanel.open) return;
|
||||
if (action === "add" || action === "select") manager.id = state.doc.documentVersion || "";
|
||||
await refreshVersionManagement(true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateVersionControls() {
|
||||
const manager = state.versionManagement, busy = document.body.hasAttribute("aria-busy");
|
||||
const disabled = !state.editing || !state.ready || state.exporting || busy;
|
||||
el.versionManageButton.hidden = !state.editing;
|
||||
el.versionManageButton.disabled = disabled;
|
||||
el.versionNotice.textContent = state.editorInfo?.editWarnings?.join(";") || "";
|
||||
el.versionNotice.hidden = !el.versionNotice.textContent;
|
||||
const entry = manager?.entries?.find(entry => entry.id === manager.id);
|
||||
const unavailable = disabled || !entry || Boolean(manager?.loading || manager?.failed);
|
||||
el.versionList.inert = busy || Boolean(manager?.loading);
|
||||
el.versionName.disabled = el.versionAdd.disabled = unavailable;
|
||||
el.versionRename.disabled = unavailable || !entry?.id || el.versionName.value.trim() === entry.name;
|
||||
el.versionDelete.disabled = unavailable || !entry?.id;
|
||||
el.versionDefault.disabled = unavailable || Boolean(entry?.default);
|
||||
el.versionSelect.disabled = unavailable || entry?.id === (state.doc?.documentVersion || "");
|
||||
}
|
||||
|
||||
function managedDocumentIndex() {
|
||||
return state.documentManagement?.index ?? state.doc?.documentIndex ?? 0;
|
||||
}
|
||||
@@ -3098,7 +3239,8 @@ async function changeDocument(name, item, ...args) {
|
||||
const previousDocument = state.doc.documents?.[state.doc.documentIndex || 0];
|
||||
const nextDocument = doc.documents?.[doc.documentIndex || 0];
|
||||
const sameDocument = previousDocument?.id && previousDocument.id === nextDocument?.id && state.doc.documentIndex === doc.documentIndex;
|
||||
const documentChanged = previousDocument?.id !== nextDocument?.id || previousDocument?.root !== nextDocument?.root
|
||||
const documentChanged = (state.doc.documentVersion || "") !== (doc.documentVersion || "")
|
||||
|| previousDocument?.id !== nextDocument?.id || previousDocument?.root !== nextDocument?.root
|
||||
&& !(sameDocument && (name === "ofdgoChangeFontBinding" || restoring));
|
||||
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]));
|
||||
@@ -3227,6 +3369,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.versionPanel.open) {
|
||||
el.versionStatus.textContent = err.message;
|
||||
} else if (el.documentPanel.open) {
|
||||
el.documentStatus.textContent = err.message;
|
||||
} else {
|
||||
@@ -3971,6 +4115,8 @@ async function openOFD(file, remote = null) {
|
||||
el.infoPanel.close();
|
||||
el.documentPanel.close();
|
||||
state.documentManagement = null;
|
||||
el.versionPanel.close();
|
||||
state.versionManagement = null;
|
||||
state.infoDocumentIndex = null;
|
||||
el.annotationNote.close();
|
||||
el.annotationCreate.close();
|
||||
@@ -8785,6 +8931,7 @@ function updateDocumentControls() {
|
||||
el.documentDeleteButton.disabled = actionsDisabled || count <= 1;
|
||||
el.documentUpButton.disabled = actionsDisabled || index === 0;
|
||||
el.documentDownButton.disabled = actionsDisabled || index >= count - 1;
|
||||
updateVersionControls();
|
||||
}
|
||||
|
||||
function updateControls() {
|
||||
@@ -9006,6 +9153,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
||||
el.paragraphForm.inert = busy;
|
||||
el.infoForm.inert = busy;
|
||||
el.documentForm.inert = busy;
|
||||
el.versionForm.inert = busy;
|
||||
el.attachmentForm.inert = busy;
|
||||
el.attachmentPanel.inert = busy;
|
||||
el.annotationForm.inert = busy;
|
||||
|
||||
+59
-1
@@ -186,6 +186,8 @@ func RunWASM() {
|
||||
registerAsyncCallback("ofdgoDetectFormat", detectDocumentFormat)
|
||||
registerCallback("ofdgoOpen", openDocument)
|
||||
registerCallback("ofdgoSelectDocument", selectDocument)
|
||||
registerCallback("ofdgoDocumentVersions", documentVersions)
|
||||
registerCallback("ofdgoChangeVersion", changeDocumentVersion)
|
||||
registerCallback("ofdgoChangeDocument", changePackageDocument)
|
||||
registerCallback("ofdgoPackageDocuments", packageDocuments)
|
||||
registerAsyncCallback("ofdgoConvertPDF", convertPDFDocument)
|
||||
@@ -3443,6 +3445,60 @@ func packageDocuments(args []js.Value) (any, error) {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// documentVersions 读取当前编辑文档的版本列表,不切换版本或解析页面
|
||||
// 入参: args 保留参数
|
||||
// 返回: any 版本列表, error 读取错误
|
||||
func documentVersions(args []js.Value) (any, error) {
|
||||
if currentEditor == nil {
|
||||
return nil, fmt.Errorf("no document is being edited")
|
||||
}
|
||||
versions, err := currentEditor.DocumentVersions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := make([]VersionEntry, 1, len(versions)+1)
|
||||
entries[0].Default = true
|
||||
for _, version := range versions {
|
||||
entries = append(entries, VersionEntry{ID: version.ID, Index: version.Index, Name: version.Name, Default: version.Current})
|
||||
if version.Current {
|
||||
entries[0].Default = false
|
||||
}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// changeDocumentVersion 调用库层版本操作,沿用编辑历史和预览更新流程
|
||||
// 入参: args 操作、版本标识及可选名称
|
||||
// 返回: any 文档信息, error 操作错误
|
||||
func changeDocumentVersion(args []js.Value) (any, error) {
|
||||
if len(args) != 3 {
|
||||
return nil, fmt.Errorf("invalid version arguments")
|
||||
}
|
||||
return changeAtomicObjects(func() error {
|
||||
id, name := args[1].String(), args[2].String()
|
||||
switch args[0].String() {
|
||||
case "add":
|
||||
return currentEditor.Transaction(func(editor *ofdgo.Editor) error {
|
||||
if err := editor.SelectVersion(id); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := editor.AddVersion(name)
|
||||
return err
|
||||
})
|
||||
case "rename":
|
||||
return currentEditor.RenameVersion(id, name)
|
||||
case "delete":
|
||||
return currentEditor.DeleteVersion(id)
|
||||
case "default":
|
||||
return currentEditor.SetDefaultVersion(id)
|
||||
case "select":
|
||||
return currentEditor.SelectVersion(id)
|
||||
default:
|
||||
return fmt.Errorf("invalid version operation")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// changeAttachment 编辑附件,逐个读取文件并复用库层事务和历史
|
||||
// 入参: args 操作、附件标识、名称列表、文件读取回调
|
||||
// 返回: any 编辑状态, error 错误信息
|
||||
@@ -3649,7 +3705,9 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
||||
if currentSession != nil {
|
||||
previous := currentSession.Reader
|
||||
oldBody, newBody := previous.OFD.DocBody[previous.DocumentIndex()], reader.OFD.DocBody[reader.DocumentIndex()]
|
||||
if oldBody.DocRoot != newBody.DocRoot || oldBody.DocInfo.DocID != newBody.DocInfo.DocID {
|
||||
oldVersion, _ := previous.DocumentVersionID()
|
||||
newVersion, _ := reader.DocumentVersionID()
|
||||
if oldBody.DocRoot != newBody.DocRoot || oldBody.DocInfo.DocID != newBody.DocInfo.DocID || oldVersion != newVersion {
|
||||
copiedObjects, copiedStyle = nil, nil
|
||||
clearImport()
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ type pageBoxInfo struct {
|
||||
// DocumentInfo 文档信息
|
||||
type DocumentInfo struct {
|
||||
DocumentIndex int `json:"documentIndex"`
|
||||
DocumentVersion string `json:"documentVersion"`
|
||||
Documents []DocumentEntry `json:"documents,omitempty"`
|
||||
Encryption EncryptionInfo `json:"encryption"`
|
||||
Version string `json:"version"`
|
||||
@@ -98,6 +99,14 @@ type DocumentInfo struct {
|
||||
DetailsPending bool `json:"detailsPending,omitempty"`
|
||||
}
|
||||
|
||||
// VersionEntry 版本管理列表中的入口,空ID表示主文档
|
||||
type VersionEntry struct {
|
||||
ID string `json:"id"`
|
||||
Index int32 `json:"index"`
|
||||
Name string `json:"name"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
// DocumentEntry 包内文档入口,管理列表按需补充元数据和页数
|
||||
type DocumentEntry struct {
|
||||
Index int `json:"index"`
|
||||
@@ -441,6 +450,7 @@ func (s *Session) Summary() DocumentInfo {
|
||||
Outlines: s.doc.OutlineInfos(),
|
||||
DetailsPending: true,
|
||||
}
|
||||
info.DocumentVersion, _ = s.Reader.DocumentVersionID()
|
||||
for index, body := range s.Reader.OFD.DocBody {
|
||||
info.Documents = append(info.Documents, DocumentEntry{Index: index, Title: body.DocInfo.Title, Root: body.DocRoot, ID: body.DocInfo.DocID})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user