feat(编辑交互): 优化文档配置与编辑交互
build.yaml / build (push) Successful in 50s

This commit is contained in:
xiaoqidun committed 2026-10-13 18:29:21 +08:00
1 parent 0afb368829
commit 6d782de471
6 files changed
+606 -76

No files matched your search

+9 -4
View File
@@ -207,7 +207,7 @@
<div class="form-row"><span>格式</span><div class="form-format"><button id="copyStyleButton" class="button" type="button" aria-label="复制格式">复制</button><button id="pasteStyleButton" class="button" type="button" aria-label="粘贴格式">粘贴</button></div></div> <div class="form-row"><span>格式</span><div class="form-format"><button id="copyStyleButton" class="button" type="button" aria-label="复制格式">复制</button><button id="pasteStyleButton" class="button" type="button" aria-label="粘贴格式">粘贴</button></div></div>
<details id="objectLinkFields" hidden> <details id="objectLinkFields" hidden>
<summary>链接</summary> <summary>链接</summary>
<div class="form-row"><label for="objectLinkKind">目标</label><select id="objectLinkKind"><option value="none">不设</option><option value="uri">网址</option><option value="page">页面</option><option value="keep">原有</option></select></div> <div class="form-row"><label for="objectLinkKind">目标</label><div class="form-unit object-link-target"><select id="objectLinkKind"><option value="none">不设</option><option value="uri">网址</option><option value="page">页面</option><option value="keep">原有</option></select><button id="objectActionsButton" class="button" type="button" title="管理动作" aria-label="管理动作">动作</button></div></div>
<div class="form-row" id="objectLinkAddressRow" hidden><label for="objectLinkAddress">地址</label><input id="objectLinkAddress" type="text"></div> <div class="form-row" id="objectLinkAddressRow" hidden><label for="objectLinkAddress">地址</label><input id="objectLinkAddress" type="text"></div>
<div class="form-row" id="objectLinkPageRow" hidden><label for="objectLinkPage">页码</label><input id="objectLinkPage" type="number" min="1" step="1"></div> <div class="form-row" id="objectLinkPageRow" hidden><label for="objectLinkPage">页码</label><input id="objectLinkPage" type="number" min="1" step="1"></div>
</details> </details>
@@ -401,10 +401,15 @@
</dialog> </dialog>
<dialog id="settingsPanel" class="form-dialog settings-dialog" aria-labelledby="settingsTitle"> <dialog id="settingsPanel" class="form-dialog settings-dialog" aria-labelledby="settingsTitle">
<form id="settingsForm"> <form id="settingsForm">
<h2 id="settingsTitle" class="form-title">文档配置</h2> <header class="settings-header">
<h2 id="settingsTitle" class="form-title">文档配置</h2>
<span id="settingsLocation" class="settings-location"></span>
</header>
<label class="form-row"><span>类型</span><select id="settingsKind"><option value="bookmarks">书签</option><option value="actions">动作</option><option value="templates">模板</option><option value="layers">图层</option><option value="extensions">扩展</option><option value="ColorSpace" hidden>色彩</option><option value="DrawParam" hidden>样式</option></select></label> <label class="form-row"><span>类型</span><select id="settingsKind"><option value="bookmarks">书签</option><option value="actions">动作</option><option value="templates">模板</option><option value="layers">图层</option><option value="extensions">扩展</option><option value="ColorSpace" hidden>色彩</option><option value="DrawParam" hidden>样式</option></select></label>
<div id="settingsContext"></div> <div id="settingsBody" class="settings-body">
<div id="settingsContent"></div> <div id="settingsContext"></div>
<div id="settingsContent"></div>
</div>
<div id="settingsStatus" class="form-status" role="status"></div> <div id="settingsStatus" class="form-status" role="status"></div>
<div id="settingsActions" class="form-actions"></div> <div id="settingsActions" class="form-actions"></div>
</form> </form>
+172 -10
View File
@@ -1354,6 +1354,11 @@ button:not(:disabled):hover {
color: var(--text-muted); color: var(--text-muted);
} }
#objectLinkFields .object-link-target {
min-width: 0;
grid-template-columns: minmax(0, 1fr) auto;
}
.text-font { .text-font {
display: grid; display: grid;
min-width: 0; min-width: 0;
@@ -1743,15 +1748,126 @@ body[data-hide-meta] .meta-panel {
.settings-dialog { .settings-dialog {
width: 560px; width: 560px;
overflow: hidden;
}
.settings-dialog > form {
display: flex;
flex-direction: column;
max-height: calc(100dvh - 26px);
padding: 0;
}
.settings-header {
display: flex;
flex: none;
align-items: baseline;
gap: 8px 16px;
padding: 10px 12px;
border-bottom: 1px solid var(--line);
background: #eef1f4;
}
.settings-header .form-title {
flex: none;
margin: 0;
padding: 0;
border: 0;
background: none;
}
.settings-location {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
min-width: 0;
margin-left: auto;
color: var(--text-muted);
font-size: 12px;
line-height: 1.5;
overflow: hidden;
overflow-wrap: anywhere;
}
.settings-location:empty,
.settings-location[hidden] {
display: none;
}
.settings-dialog > form > .form-row {
flex: none;
margin: 0;
padding: 12px;
}
.settings-body {
min-width: 0;
min-height: 0;
padding: 0 12px 12px;
overflow: auto;
overscroll-behavior: contain;
}
.settings-dialog .settings-section {
min-width: 0;
padding-block: 12px;
border-top: 1px solid var(--line);
}
.settings-dialog .settings-section:last-child {
padding-bottom: 0;
}
.settings-dialog .settings-section-title {
margin-bottom: 12px;
font-weight: 600;
line-height: 1.5;
overflow-wrap: anywhere;
}
.settings-dialog .settings-section-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 12px;
}
.settings-dialog .settings-section-actions > .button,
#settingsActions .button {
flex: 0 0 auto;
min-width: calc(2em + 22px);
padding-inline: 10px;
}
.settings-dialog .settings-section[hidden],
.settings-dialog .settings-section-actions[hidden],
.settings-dialog .settings-section-actions > [hidden] {
display: none;
} }
.settings-dialog .form-row { .settings-dialog .form-row {
grid-template-columns: 4em minmax(0, 1fr); grid-template-columns: 4em minmax(0, 1fr);
} }
.settings-pick {
grid-template-columns: minmax(0, 1fr) auto;
}
.settings-dialog details.settings-section {
margin-bottom: 0;
}
.settings-dialog details.settings-section:not([open]) > summary {
margin-bottom: 0;
}
.settings-dialog .form-row > label, .settings-dialog .form-row > label,
.settings-dialog .form-row > span:not(.form-unit) { .settings-dialog .form-row > span:not(.form-unit):not(.settings-readonly) {
white-space: nowrap; min-width: 0;
white-space: normal;
overflow-wrap: anywhere;
line-height: 1.5;
text-align-last: justify; text-align-last: justify;
} }
@@ -1765,13 +1881,18 @@ body[data-hide-meta] .meta-panel {
.settings-grid { grid-template-columns: minmax(0, 1fr); } .settings-grid { grid-template-columns: minmax(0, 1fr); }
} }
.settings-dialog .form-status:not(:empty) { #settingsStatus {
margin-bottom: 12px; flex: none;
}
#settingsStatus:not(:empty) {
padding: 8px 12px;
border-top: 1px solid var(--line);
} }
.settings-dialog .info-line { .settings-dialog .info-line {
grid-template-columns: 4em minmax(0, 1fr); grid-template-columns: 4em minmax(0, 1fr);
align-items: center; align-items: start;
gap: 12px; gap: 12px;
min-height: var(--control-height); min-height: var(--control-height);
margin-bottom: 12px; margin-bottom: 12px;
@@ -1779,6 +1900,9 @@ body[data-hide-meta] .meta-panel {
} }
.settings-dialog .info-label { .settings-dialog .info-label {
min-width: 0;
white-space: normal;
overflow-wrap: anywhere;
text-align-last: justify; text-align-last: justify;
} }
@@ -1786,18 +1910,47 @@ body[data-hide-meta] .meta-panel {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.settings-detail { .settings-dialog .settings-readonly {
display: block;
min-width: 0;
min-height: var(--control-height);
padding-block: 4px;
color: inherit;
font: inherit;
line-height: 1.5;
text-align: left;
text-align-last: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
cursor: text;
-webkit-user-select: text;
user-select: text;
}
.settings-dialog .form-row:has(.settings-readonly) {
align-items: baseline;
}
.settings-dialog .settings-detail {
min-width: 0;
padding-block: 10px; padding-block: 10px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.settings-detail pre { .settings-dialog .settings-detail pre {
max-width: 100%;
margin-block: 8px 0;
white-space: pre-wrap; white-space: pre-wrap;
font: inherit; font: inherit;
line-height: 1.5;
} }
.settings-binding { .settings-dialog .settings-detail .info-label {
text-align-last: auto;
}
.settings-dialog .settings-binding {
margin-block: 12px; margin-block: 12px;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
@@ -1805,11 +1958,20 @@ body[data-hide-meta] .meta-panel {
#settingsActions { #settingsActions {
display: flex; display: flex;
flex: none;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px;
padding: 12px;
border-top: 1px solid var(--line);
} }
#settingsActions > .button { #settingsActions .settings-section-actions:not([hidden]) {
flex: 0 0 auto; display: contents;
}
#settingsStatus:not(:empty) + #settingsActions {
padding-top: 0;
border-top: 0;
} }
#settingsActions .settings-close { #settingsActions .settings-close {
+354 -57
View File
@@ -26,7 +26,7 @@ const fontBindingElements = Object.fromEntries(["Panel", "Form", "Title", "Name"
const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "Actions", "Add", "Preview", "Export", "Rename", "Replace", "Delete", "Locate", "Close", "Binding", "BindingActions", "Page", "Object", "Pick", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)])); const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "Actions", "Add", "Preview", "Export", "Rename", "Replace", "Delete", "Locate", "Close", "Binding", "BindingActions", "Page", "Object", "Pick", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)]));
const resourceManager = { items: [], selected: null, targets: [], pick: null, busy: false, sequence: 0, url: null, font: null, fileAction: null }; const resourceManager = { items: [], selected: null, targets: [], pick: null, busy: false, sequence: 0, url: null, font: null, fileAction: null };
const settingsElements = Object.fromEntries(["Button", "Panel", "Form", "Title", "Kind", "Context", "Content", "Actions", "Status"].map(name => [name, document.querySelector(`#settings${name}`)])); const settingsElements = Object.fromEntries(["Button", "Panel", "Form", "Title", "Kind", "Context", "Content", "Actions", "Status"].map(name => [name, document.querySelector(`#settings${name}`)]));
const settings = { sequence: 0, busy: false, page: 0, scope: "document", object: "", path: [], selected: -1, data: null, actions: [] }; const settings = { sequence: 0, busy: false, page: 0, scope: "document", object: "", path: [], selected: -1, data: null, actions: [], drafts: new Map(), view: "", baseline: {}, suspended: false };
let wasmPromise = null; let wasmPromise = null;
let remoteDownload = null; let remoteDownload = null;
@@ -1499,10 +1499,22 @@ el.annotationsFilter.addEventListener("input", renderAnnotationSummary);
el.annotationsKind.addEventListener("change", renderAnnotationSummary); el.annotationsKind.addEventListener("change", renderAnnotationSummary);
el.versionExport.addEventListener("click", exportManagedVersion); el.versionExport.addEventListener("click", exportManagedVersion);
editorClick(settingsElements.Button, openDocumentSettings); editorClick(settingsElements.Button, openDocumentSettings);
settingsElements.Kind.addEventListener("change", () => { settings.selected = -1; loadDocumentSettings(); }); settingsElements.Kind.addEventListener("change", () => { rememberSettingsDraft(); settings.selected = -1; loadDocumentSettings(); });
settingsElements.Form.addEventListener("submit", event => event.preventDefault()); settingsElements.Form.addEventListener("submit", event => event.preventDefault());
settingsElements.Panel.addEventListener("cancel", event => { if (settings.busy) event.preventDefault(); }); settingsElements.Panel.addEventListener("cancel", event => { event.preventDefault(); closeDocumentSettings(); });
settingsElements.Panel.addEventListener("close", () => { settings.sequence++; }); settingsElements.Panel.addEventListener("close", () => { if (!settings.suspended) { settings.sequence++; settings.drafts.clear(); settings.view = ""; } });
document.querySelector("#objectActionsButton").addEventListener("click", async () => {
const item = canvasEditor.selected;
if (!item || item.scoped || item.items) return;
const original = state.objectLinkOriginal;
if ((el.objectLinkKind.value !== original.kind || el.objectLinkAddress.value !== original.address || el.objectLinkPage.value !== original.page)
&& !window.confirm("链接尚未应用,放弃链接更改?")) return;
syncObjectLink(item);
settings.scope = "object";
settingsElements.Kind.value = "actions";
await openDocumentSettings();
settingsElements.Kind.disabled = true;
});
el.outlinesTab.addEventListener("click", () => showNavigation(el.outlinesTab)); el.outlinesTab.addEventListener("click", () => showNavigation(el.outlinesTab));
el.searchTab.addEventListener("click", focusSearch); el.searchTab.addEventListener("click", focusSearch);
el.navigationTabs.addEventListener("keydown", (event) => { el.navigationTabs.addEventListener("keydown", (event) => {
@@ -1785,7 +1797,7 @@ function handleKeyDown(event) {
} }
function formDialogOpen() { function formDialogOpen() {
return Boolean(sealPreview || resourceManager.pick) || 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 return Boolean(sealPreview || resourceManager.pick) || settingsElements.Panel.open || 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
|| resourceElements.Panel.open || el.structureDialog.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; || resourceElements.Panel.open || el.structureDialog.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;
} }
@@ -1825,6 +1837,18 @@ function canCopyStyle(item) {
return item.type === "ImageObject" && canEditObject(item, "transform"); return item.type === "ImageObject" && canEditObject(item, "transform");
} }
function syncObjectLink(item) {
const link = item.link ? JSON.parse(item.link) : null;
const targetPage = link?.Goto?.Dest ? pageIndexByID(state.doc.pages, link.Goto.Dest.PageID) : -1;
el.objectLinkKind.value = item.linkCount > 1 ? "keep" : link?.URI ? "uri" : targetPage >= 0 ? "page" : link ? "keep" : "none";
el.objectLinkKind.disabled = item.linkCount > 1;
el.objectLinkAddress.value = link?.URI?.URI || "";
el.objectLinkPage.value = String(targetPage >= 0 ? targetPage + 1 : 1);
el.objectLinkPage.max = String(state.doc?.pages.length || 1);
state.objectLinkOriginal = {kind: el.objectLinkKind.value, address: el.objectLinkAddress.value, page: el.objectLinkPage.value, link};
updateObjectLinkFields();
}
function openObjectStyle() { function openObjectStyle() {
const items = canvasEditor.items(); const items = canvasEditor.items();
if (!items.length) return; if (!items.length) return;
@@ -1835,15 +1859,7 @@ function openObjectStyle() {
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item)); el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null; const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
el.objectLinkFields.hidden = items.length !== 1 || items[0].scoped; el.objectLinkFields.hidden = items.length !== 1 || items[0].scoped;
const link = items[0].link ? JSON.parse(items[0].link) : null; syncObjectLink(items[0]);
const targetPage = link?.Goto?.Dest ? pageIndexByID(state.doc.pages, link.Goto.Dest.PageID) : -1;
el.objectLinkKind.value = items[0].linkCount > 1 ? "keep" : link?.URI ? "uri" : targetPage >= 0 ? "page" : link ? "keep" : "none";
el.objectLinkKind.disabled = items[0].linkCount > 1;
el.objectLinkAddress.value = link?.URI?.URI || "";
el.objectLinkPage.value = String(targetPage >= 0 ? targetPage + 1 : 1);
el.objectLinkPage.max = String(state.doc?.pages.length || 1);
state.objectLinkOriginal = { kind: el.objectLinkKind.value, address: el.objectLinkAddress.value, page: el.objectLinkPage.value, link };
updateObjectLinkFields();
el.objectBorderFields.hidden = items.length !== 1 || items[0].scoped || items[0].type !== "ImageObject"; el.objectBorderFields.hidden = items.length !== 1 || items[0].scoped || items[0].type !== "ImageObject";
const border = items[0].borderStyle ? JSON.parse(items[0].borderStyle) : null; const border = items[0].borderStyle ? JSON.parse(items[0].borderStyle) : null;
el.borderEnabled.checked = Boolean(border); el.borderEnabled.checked = Boolean(border);
@@ -5161,24 +5177,111 @@ function settingsSelect(container, label, id, choices, value, change) {
field.setAttribute("aria-label", label); field.setAttribute("aria-label", label);
field.replaceChildren(...choices.map(([value, text]) => new Option(text, String(value)))); field.replaceChildren(...choices.map(([value, text]) => new Option(text, String(value))));
if (value != null && choices.some(([key]) => String(key) === String(value))) field.value = String(value); if (value != null && choices.some(([key]) => String(key) === String(value))) field.value = String(value);
if (change) field.addEventListener("change", () => change(field.value)); if (change) field.addEventListener("change", () => { rememberSettingsDraft(); change(field.value); });
row.append(title, field); row.append(title, field);
container.append(row); container.append(row);
return field; return field;
} }
function settingsButton(label, action, disabled = false, id = "") { function settingsButton(label, action, disabled = false, id = "", container = settingsElements.Actions, reason = "") {
const button = document.createElement("button"); const button = document.createElement("button");
button.type = "button"; button.type = "button";
button.className = "button"; button.className = "button";
button.textContent = label; button.textContent = label;
button.disabled = disabled; button.disabled = disabled;
if (disabled && reason) button.title = reason;
if (id) button.id = `settings${id}`; if (id) button.id = `settings${id}`;
button.addEventListener("click", action); button.addEventListener("click", async () => {
settingsElements.Actions.append(button); try { await action(); }
catch (error) { settingsElements.Status.textContent = error.message; }
});
container.append(button);
return button; return button;
} }
function settingsSection(label) {
const section = document.createElement("section"), title = document.createElement("div"), actions = document.createElement("div");
section.className = "settings-section";
title.className = "settings-section-title";
title.textContent = label;
actions.className = "settings-section-actions";
section.append(title);
settingsElements.Content.append(section);
return {section, actions};
}
function settingsSnapshot() {
return Object.fromEntries([...settingsElements.Content.querySelectorAll("input,select,textarea")].filter(field => field.id).map(field => [field.id,
field.type === "checkbox" ? field.checked : field.type === "file" ? [...field.files] : field.multiple ? [...field.selectedOptions].map(option => option.value) : field.value]));
}
function rememberSettingsDraft() {
if (!state.editing || !settings.view || settings.busy) return;
const values = settingsSnapshot();
const changed = Object.keys(values).some(id => {
if (["settingsTemplateReference", "settingsLayerObjects", "settingsTargetPage", "settingsTargetObject"].includes(id)) return false;
const value = values[id], before = settings.baseline[id];
return Array.isArray(value) ? value.length !== (before?.length || 0) || value.some((entry, index) => entry !== before?.[index]) : value !== before;
});
if (!changed) { settings.drafts.delete(settings.view); return; }
settings.drafts.set(settings.view, {values, scroll: document.querySelector("#settingsBody")?.scrollTop || 0,
expanded: [...settingsElements.Content.querySelectorAll("details")].map(node => node.open)});
}
function restoreSettingsDraft() {
const kind = settingsElements.Kind.value;
const entry = kind === "bookmarks" ? settings.data.bookmarks?.[settings.selected]?.Name
: kind === "templates" ? settings.data.page.templates?.[settings.selected]?.ID
: kind === "layers" ? settings.data.page.layers?.[settings.selected]?.ID : settings.selected;
settings.view = JSON.stringify([kind, settings.resource?.id || "", kind === "actions" ? [settings.scope, settings.page, settings.object, settings.path] : ["templates", "layers"].includes(kind) ? settings.page : "", entry ?? -1]);
settings.baseline = settingsSnapshot();
const draft = settings.drafts.get(settings.view);
if (!draft) return;
if (kind === "actions" && draft.values.settingsActionType) {
renderActionFields(draft.values.settingsActionType, settings.actions[settings.selected] || {});
}
for (const [id, value] of Object.entries(draft.values)) {
const field = document.getElementById(id);
if (!field || !settingsElements.Content.contains(field)) continue;
if (field.type === "checkbox") field.checked = value;
else if (field.type === "file") {
const transfer = new DataTransfer();
for (const file of value) transfer.items.add(file);
field.files = transfer.files;
} else if (field.multiple) for (const option of field.options) option.selected = value.includes(option.value);
else if (field.tagName !== "SELECT" || [...field.options].some(option => option.value === value)) field.value = value;
}
if (document.querySelector("#settingsDestType")) updateDestinationFields();
for (const key of ["FillColor", "StrokeColor"]) {
const field = document.querySelector(`#settings${key}`);
if (field) field.closest("label").hidden = settingsValue(key + "Mode") !== "solid";
}
const body = document.querySelector("#settingsBody");
for (const [index, node] of [...settingsElements.Content.querySelectorAll("details")].entries()) node.open = draft.expanded?.[index] || false;
if (body) body.scrollTop = draft.scroll;
}
function closeDocumentSettings() {
if (settings.busy) return;
rememberSettingsDraft();
if (settings.drafts.size && !window.confirm("配置尚未应用,放弃更改?")) return;
settingsElements.Panel.close();
}
function renderReadonlySettings() {
for (const field of settingsElements.Content.querySelectorAll("input,select,textarea")) {
field.disabled = true;
if (field.id === "settingsTemplateReference") { field.disabled = false; continue; }
const row = field.closest(".form-row");
if (!row || row.hidden || field.type === "file") { if (field.type === "file" && row) row.hidden = true; continue; }
const value = document.createElement("span");
value.className = "settings-readonly";
value.textContent = field.tagName === "SELECT" ? field.selectedOptions[0]?.textContent || "—" : field.type === "checkbox" ? field.checked ? "是" : "否" : field.value || field.placeholder || "—";
field.hidden = true;
field.after(value);
}
}
function settingsValue(id) { function settingsValue(id) {
const field = document.querySelector(`#settings${id}`); const field = document.querySelector(`#settings${id}`);
return field?.type === "checkbox" ? field.checked : field?.value ?? ""; return field?.type === "checkbox" ? field.checked : field?.value ?? "";
@@ -5186,10 +5289,13 @@ function settingsValue(id) {
async function openDocumentSettings() { async function openDocumentSettings() {
if (!state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return; if (!state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return;
if (resourceManager.pick?.settings) { finishResourcePick(); return; }
settings.page = state.pageIndex; settings.page = state.pageIndex;
settings.object = canvasEditor.selected?.index === settings.page && !canvasEditor.selected.scoped ? canvasEditor.selected.id : ""; settings.object = canvasEditor.selected?.index === settings.page && !canvasEditor.selected.scoped ? canvasEditor.selected.id : "";
settings.path = state.outlineSelection || []; settings.path = state.outlineSelection || [];
settings.selected = -1; settings.selected = -1;
settings.drafts.clear();
settings.view = "";
settings.resource = null; settings.resource = null;
settingsElements.Kind.disabled = false; settingsElements.Kind.disabled = false;
for (const option of settingsElements.Kind.options) option.hidden = option.value === "actions" && !state.editing || ["ColorSpace", "DrawParam"].includes(option.value); for (const option of settingsElements.Kind.options) option.hidden = option.value === "actions" && !state.editing || ["ColorSpace", "DrawParam"].includes(option.value);
@@ -5201,6 +5307,9 @@ async function openDocumentSettings() {
async function openResourceSettings(kind, id) { async function openResourceSettings(kind, id) {
if (!state.doc || !state.ready || document.body.hasAttribute("aria-busy")) return; if (!state.doc || !state.ready || document.body.hasAttribute("aria-busy")) return;
settings.resource = {kind, id}; settings.resource = {kind, id};
settings.selected = -1;
settings.drafts.clear();
settings.view = "";
const page = Number(resourceElements.Page.value); const page = Number(resourceElements.Page.value);
settings.page = resourceElements.Page.value !== "" && Number.isInteger(page) && page >= 0 && page < state.doc.pages.length ? page : state.pageIndex; settings.page = resourceElements.Page.value !== "" && Number.isInteger(page) && page >= 0 && page < state.doc.pages.length ? page : state.pageIndex;
settings.object = resourceElements.Object.value || ""; settings.object = resourceElements.Object.value || "";
@@ -5233,7 +5342,7 @@ async function loadDocumentSettings() {
data.actions = (settings.scope === "object" && !settings.object || settings.scope === "outline" && !settings.path.length) ? [] : await callWASM("ofdgoActionList", settings.scope, settings.page, settings.object, settings.path) || []; data.actions = (settings.scope === "object" && !settings.object || settings.scope === "outline" && !settings.path.length) ? [] : await callWASM("ofdgoActionList", settings.scope, settings.page, settings.object, settings.path) || [];
} }
if (kind === "templates" || kind === "layers") { if (kind === "templates" || kind === "layers") {
data.page = await callWASM("ofdgoPageSettings", settings.page); data.page = await callWASM("ofdgoPageSettings", settings.page, kind);
if (kind === "layers" && state.editing) { if (kind === "layers" && state.editing) {
const page = await loadPageData(settings.page); const page = await loadPageData(settings.page);
data.objects = (page.objects || []).filter(item => item.type !== "Annotation" && !item.scoped); data.objects = (page.objects || []).filter(item => item.type !== "Annotation" && !item.scoped);
@@ -5255,7 +5364,7 @@ async function loadDocumentSettings() {
if (sequence === settings.sequence && openSeq === state.openSeq) { if (sequence === settings.sequence && openSeq === state.openSeq) {
settingsElements.Content.replaceChildren(); settingsElements.Content.replaceChildren();
settingsElements.Actions.replaceChildren(); settingsElements.Actions.replaceChildren();
settingsButton("关闭", () => settingsElements.Panel.close()); settingsButton("关闭", closeDocumentSettings);
settingsElements.Status.textContent = error.message; settingsElements.Status.textContent = error.message;
} }
} finally { } finally {
@@ -5268,6 +5377,12 @@ function renderDocumentSettings() {
settingsElements.Content.replaceChildren(); settingsElements.Content.replaceChildren();
settingsElements.Actions.replaceChildren(); settingsElements.Actions.replaceChildren();
const kind = settingsElements.Kind.value; const kind = settingsElements.Kind.value;
const location = document.querySelector("#settingsLocation"), doc = state.doc;
if (location) {
const current = doc?.documents?.find(entry => entry.index === doc.documentIndex) || doc?.documents?.[doc?.documentIndex || 0];
location.textContent = `${current?.title || current?.name || `文档${(doc?.documentIndex || 0) + 1}`}${doc?.documentVersion ? ` · 版本${doc.documentVersion}` : ""}`;
location.hidden = !(doc?.documents?.length > 1 || doc?.documentVersion);
}
settingsElements.Title.textContent = {ColorSpace: "色彩设置", DrawParam: "样式设置"}[kind] || "文档配置"; settingsElements.Title.textContent = {ColorSpace: "色彩设置", DrawParam: "样式设置"}[kind] || "文档配置";
if (kind === "actions") renderActionSettings(); if (kind === "actions") renderActionSettings();
else if (kind === "bookmarks") renderBookmarkSettings(); else if (kind === "bookmarks") renderBookmarkSettings();
@@ -5275,7 +5390,15 @@ function renderDocumentSettings() {
else if (kind === "templates") renderTemplateSettings(); else if (kind === "templates") renderTemplateSettings();
else if (kind === "layers") renderLayerSettings(); else if (kind === "layers") renderLayerSettings();
else if (kind === "ColorSpace" || kind === "DrawParam") renderStyleResourceSettings(kind); else if (kind === "ColorSpace" || kind === "DrawParam") renderStyleResourceSettings(kind);
settingsButton("关闭", () => settingsElements.Panel.close(), false, "Close").classList.add("settings-close"); if (!state.editing) renderReadonlySettings();
restoreSettingsDraft();
const move = document.querySelector("#settingsMoveObjects");
if (move) move.disabled = !document.querySelector("#settingsLayerObjects").selectedOptions.length;
const target = document.querySelector("#settingsTargetObject");
if (target) target.dispatchEvent(new Event("change"));
const lastActions = settingsElements.Content.querySelector(".settings-section:last-child > .settings-section-actions");
if (lastActions) settingsElements.Actions.append(lastActions);
settingsButton("关闭", closeDocumentSettings, false, "Close").classList.add("settings-close");
} }
function settingsPages(container, id, value, change) { function settingsPages(container, id, value, change) {
@@ -5306,8 +5429,12 @@ function renderActionSettings() {
const context = settingsElements.Context, content = settingsElements.Content; const context = settingsElements.Context, content = settingsElements.Content;
settingsSelect(context, "归属", "Scope", [["document", "文档"], ["page", "页面"], ["object", "图元"], ["outline", "目录"]], settings.scope, value => { settings.scope = value; settings.selected = -1; loadDocumentSettings(); }); settingsSelect(context, "归属", "Scope", [["document", "文档"], ["page", "页面"], ["object", "图元"], ["outline", "目录"]], settings.scope, value => { settings.scope = value; settings.selected = -1; loadDocumentSettings(); });
if (settings.scope === "page" || settings.scope === "object") settingsPages(context, "Page", settings.page, value => { settings.page = Number(value); settings.selected = -1; loadDocumentSettings(); }); if (settings.scope === "page" || settings.scope === "object") settingsPages(context, "Page", settings.page, value => { settings.page = Number(value); settings.selected = -1; loadDocumentSettings(); });
if (settings.scope === "object") settingsSelect(context, "图元", "Object", (settings.data.objects || []).map(item => [item.id, `${item.id} · ${item.text || ({TextObject: "文字", PathObject: "图形", ImageObject: "图片"}[item.type] || "组合")}`]), settings.object, value => { settings.object = value; settings.selected = -1; loadDocumentSettings(); }); if (settings.scope === "object") {
const objects = settingsSelect(context, "图元", "Object", (settings.data.objects || []).map(item => [item.id, `${item.id} · ${item.text || ({TextObject: "文字", PathObject: "图形", ImageObject: "图片"}[item.type] || "组合")}`]), settings.object, value => { settings.object = value; settings.selected = -1; loadDocumentSettings(); });
if (!el.objectStylePanel.open) settingsPickControl(objects, "action");
}
if (settings.scope === "outline") settingsSelect(context, "目录", "Outline", outlineSettingsChoices(), JSON.stringify(settings.path), value => { settings.path = JSON.parse(value); settings.selected = -1; loadDocumentSettings(); }); if (settings.scope === "outline") settingsSelect(context, "目录", "Outline", outlineSettingsChoices(), JSON.stringify(settings.path), value => { settings.path = JSON.parse(value); settings.selected = -1; loadDocumentSettings(); });
if (el.objectStylePanel.open) for (const field of context.querySelectorAll("select")) field.disabled = true;
if (settings.selected >= settings.actions.length) settings.selected = -1; if (settings.selected >= settings.actions.length) settings.selected = -1;
settingsSelect(context, "动作", "Entry", [[-1, "新增"], ...settings.actions.map((item, index) => [index, `${index + 1} · ${actionSettingsLabel(item)}`])], settings.selected, value => { settings.selected = Number(value); renderDocumentSettings(); }); settingsSelect(context, "动作", "Entry", [[-1, "新增"], ...settings.actions.map((item, index) => [index, `${index + 1} · ${actionSettingsLabel(item)}`])], settings.selected, value => { settings.selected = Number(value); renderDocumentSettings(); });
const action = settings.actions[settings.selected] || {}; const action = settings.actions[settings.selected] || {};
@@ -5379,21 +5506,56 @@ function destinationFromSettings() {
async function commitSettings(name, ...args) { async function commitSettings(name, ...args) {
if (!state.editing || settings.busy || document.body.hasAttribute("aria-busy")) return false; if (!state.editing || settings.busy || document.body.hasAttribute("aria-busy")) return false;
rememberSettingsDraft();
const pending = settings.drafts.get(settings.view), baseline = settings.baseline;
settings.busy = true; settings.busy = true;
settingsElements.Form.inert = true; settingsElements.Form.inert = true;
settingsElements.Status.textContent = "正在保存修改"; settingsElements.Status.textContent = "正在保存修改";
const view = settings.view, focus = document.activeElement?.id, body = document.querySelector("#settingsBody"), scroll = body?.scrollTop || 0;
try { try {
if (!await changeDocument(name, null, ...args)) { if (!await changeDocument(name, null, ...args)) {
if (settingsElements.Status.textContent === "正在保存修改") settingsElements.Status.textContent = el.statusText.textContent; if (settingsElements.Status.textContent === "正在保存修改") settingsElements.Status.textContent = el.statusText.textContent;
return false; return false;
} }
settings.drafts.delete(view);
if (pending) {
const fields = settingsCommittedFields(name, args, Object.keys(pending.values));
const values = Object.fromEntries(Object.entries(pending.values).filter(([id, value]) => !fields.includes(id) && JSON.stringify(value) !== JSON.stringify(baseline[id])));
if (Object.keys(values).length) settings.drafts.set(view, {...pending, values});
}
await loadDocumentSettings(); await loadDocumentSettings();
if (name === "ofdgoChangeActions" && el.objectStylePanel.open && settings.scope === "object") {
const item = canvasEditor.pageItems(settings.page).find(item => item.id === settings.object && !item.scoped);
if (item) { canvasEditor.select(item); syncObjectLink(item); }
}
if (body) body.scrollTop = scroll;
document.getElementById(focus)?.focus({preventScroll: true});
return true; return true;
} finally { settings.busy = false; settingsElements.Form.inert = false; } } finally { settings.busy = false; settingsElements.Form.inert = false; }
} }
function settingsCommittedFields(name, args, fields) {
const binding = ["settingsTargetPage", "settingsTargetObject", "settingsPaintTarget", "settingsPaintMode", "settingsPaintValue"];
if (name === "ofdgoChangeTemplate") {
if (args[0] === "update") return ["settingsName", "settingsTemplateOrder"];
if (args[0] === "replace") return ["settingsSourcePage"];
if (["bind", "order", "unbind"].includes(args[0])) return ["settingsBindingOrder"];
if (args[0] === "move") return [];
}
if (name === "ofdgoChangeLayer") {
if (args[0] === "update") return ["settingsLayerType", "settingsDrawParam"];
if (args[0] === "objects") return ["settingsLayerObjects"];
if (args[0] === "move") return [];
}
if (name === "ofdgoChangeStyleResource") return fields.filter(id => !binding.includes(id));
if (name === "ofdgoApplyStyleResource") return binding;
return fields;
}
async function writeActionSettings(operation) { async function writeActionSettings(operation) {
if (!settingsElements.Form.reportValidity()) return; if (operation === "save" && !settingsElements.Form.reportValidity()) return;
rememberSettingsDraft();
const drafts = new Map(settings.drafts), view = settings.view;
const actions = structuredClone(settings.actions), index = settings.selected; const actions = structuredClone(settings.actions), index = settings.selected;
if (operation === "save") { if (operation === "save") {
const original = actions[index] || {}; const original = actions[index] || {};
@@ -5413,11 +5575,27 @@ async function writeActionSettings(operation) {
[actions[index], actions[next]] = [actions[next], actions[index]]; [actions[index], actions[next]] = [actions[next], actions[index]];
settings.selected = next; settings.selected = next;
} }
if (!await commitSettings("ofdgoChangeActions", settings.scope, settings.page, settings.object, settings.path, JSON.stringify(actions))) settings.selected = index; if (!await commitSettings("ofdgoChangeActions", settings.scope, settings.page, settings.object, settings.path, JSON.stringify(actions))) { settings.selected = index; return; }
if (operation !== "save") {
const context = JSON.parse(view).slice(0, -1), prefix = JSON.stringify(context);
for (const key of settings.drafts.keys()) if (JSON.stringify(JSON.parse(key).slice(0, -1)) === prefix) settings.drafts.delete(key);
for (const [key, draft] of drafts) {
const parts = JSON.parse(key);
if (JSON.stringify(parts.slice(0, -1)) !== prefix) continue;
let position = parts.at(-1);
if (operation === "delete") {
if (position === index) continue;
if (position > index) position--;
} else if (position === index) position = settings.selected;
else if (position === settings.selected) position = index;
settings.drafts.set(JSON.stringify([...context, position]), draft);
}
renderDocumentSettings();
}
} }
function renderStyleResourceSettings(kind) { function renderStyleResourceSettings(kind) {
const content = settingsElements.Content, value = settings.data.resource || {}; const properties = settingsSection("属性"), content = properties.section, value = settings.data.resource || {};
const existing = Boolean(settings.resource.id); const existing = Boolean(settings.resource.id);
if (kind === "ColorSpace") { if (kind === "ColorSpace") {
settingsSelect(content, "模型", "ColorType", [["RGB", "RGB"], ["GRAY", "GRAY"], ["CMYK", "CMYK"]], value.Type || "RGB"); settingsSelect(content, "模型", "ColorType", [["RGB", "RGB"], ["GRAY", "GRAY"], ["CMYK", "CMYK"]], value.Type || "RGB");
@@ -5453,20 +5631,22 @@ function renderStyleResourceSettings(kind) {
} }
content.querySelectorAll("input,select,textarea").forEach(field => { field.disabled = !state.editing; }); content.querySelectorAll("input,select,textarea").forEach(field => { field.disabled = !state.editing; });
if (!state.editing) return; if (!state.editing) return;
settingsButton(existing ? "修改" : "添加", () => writeStyleResource(kind), false, "Save"); settingsButton(existing ? "修改" : "添加", () => writeStyleResource(kind), false, "Save", properties.actions);
content.append(properties.actions);
if (!existing) return; if (!existing) return;
const binding = document.createElement("div"); const group = settingsSection("绑定"), binding = group.section;
binding.className = "settings-binding";
content.append(binding);
settingsPages(binding, "TargetPage", settings.page, value => { settings.page = Number(value); loadSettingsObjects(); }); settingsPages(binding, "TargetPage", settings.page, value => { settings.page = Number(value); loadSettingsObjects(); });
settingsSelect(binding, "图元", "TargetObject", [["", "选择图元"], ...(settings.data.objects || []).map(item => [item.id, `${item.id} · ${item.text || "图元"}`])], settings.object); const objects = settingsSelect(binding, "图元", "TargetObject", [["", "选择图元"], ...(settings.data.objects || []).map(item => [item.id, `${item.id} · ${item.text || "图元"}`])], settings.object);
settingsPickControl(objects, "style");
if (kind === "ColorSpace") { if (kind === "ColorSpace") {
settingsSelect(binding, "应用", "PaintTarget", [["fill", "填充"], ["stroke", "描边"]], "fill"); settingsSelect(binding, "应用", "PaintTarget", [["fill", "填充"], ["stroke", "描边"]], "fill");
settingsSelect(binding, "方式", "PaintMode", [["value", "分量"], ["index", "索引"]], "value"); settingsSelect(binding, "方式", "PaintMode", [["value", "分量"], ["index", "索引"]], "value");
settingsField(binding, "颜色值", "PaintValue", value.Type === "CMYK" ? "0 0 0 255" : value.Type === "GRAY" ? "0" : "0 0 0"); settingsField(binding, "颜色值", "PaintValue", value.Type === "CMYK" ? `0 0 0 ${2 ** (value.BitsPerComponent || 8) - 1}` : value.Type === "GRAY" ? "0" : "0 0 0");
} }
settingsButton("应用", () => applyStyleResource(kind), false, "Apply"); const apply = settingsButton("应用", () => applyStyleResource(kind), !objects.value, "Apply", group.actions);
if (kind === "DrawParam") settingsButton("解绑", () => applyStyleResource(kind, true), false, "Unbind"); const unbind = kind === "DrawParam" ? settingsButton("解绑", () => applyStyleResource(kind, true), !objects.value, "Unbind", group.actions) : null;
objects.addEventListener("change", () => { apply.disabled = !objects.value; if (unbind) unbind.disabled = !objects.value; });
binding.append(group.actions);
} }
async function loadSettingsObjects() { async function loadSettingsObjects() {
@@ -5478,7 +5658,7 @@ async function loadSettingsObjects() {
if (sequence !== settings.sequence || page !== settings.page || openSeq !== state.openSeq) return; if (sequence !== settings.sequence || page !== settings.page || openSeq !== state.openSeq) return;
field.replaceChildren(new Option("选择图元", ""), ...(data.objects || []).filter(item => ["TextObject", "PathObject", "CompositeObject", "CompositeGraphicUnit"].includes(item.type) && !item.scoped).map(item => new Option(`${item.id} · ${item.text || "图元"}`, item.id))); field.replaceChildren(new Option("选择图元", ""), ...(data.objects || []).filter(item => ["TextObject", "PathObject", "CompositeObject", "CompositeGraphicUnit"].includes(item.type) && !item.scoped).map(item => new Option(`${item.id} · ${item.text || "图元"}`, item.id)));
} catch (error) { if (sequence === settings.sequence) settingsElements.Status.textContent = error.message; } } catch (error) { if (sequence === settings.sequence) settingsElements.Status.textContent = error.message; }
finally { if (sequence === settings.sequence) field.disabled = false; } finally { if (sequence === settings.sequence) { field.disabled = false; field.dispatchEvent(new Event("change")); } }
} }
async function writeStyleResource(kind) { async function writeStyleResource(kind) {
@@ -5530,7 +5710,7 @@ async function applyStyleResource(kind, clear = false) {
} }
function renderLayerSettings() { function renderLayerSettings() {
const entries = settings.data.page.layers || [], content = settingsElements.Content; const entries = settings.data.page.layers || [];
settingsPages(settingsElements.Context, "Page", settings.page, value => { settings.page = Number(value); settings.selected = -1; loadDocumentSettings(); }); settingsPages(settingsElements.Context, "Page", settings.page, value => { settings.page = Number(value); settings.selected = -1; loadDocumentSettings(); });
if (settings.selected >= entries.length) settings.selected = -1; if (settings.selected >= entries.length) settings.selected = -1;
if (!state.editing && settings.selected < 0 && entries.length) settings.selected = 0; if (!state.editing && settings.selected < 0 && entries.length) settings.selected = 0;
@@ -5540,30 +5720,37 @@ function renderLayerSettings() {
settingsSelect(settingsElements.Context, "图层", "Entry", options, settings.selected, value => { settings.selected = Number(value); renderDocumentSettings(); }); settingsSelect(settingsElements.Context, "图层", "Entry", options, settings.selected, value => { settings.selected = Number(value); renderDocumentSettings(); });
const entry = entries[settings.selected]; const entry = entries[settings.selected];
if (!entry && !state.editing) { settingsElements.Status.textContent = "暂无图层"; return; } if (!entry && !state.editing) { settingsElements.Status.textContent = "暂无图层"; return; }
const properties = settingsSection("属性"), content = properties.section;
settingsSelect(content, "类型", "LayerType", Object.entries(types), entry?.Type || "Body"); settingsSelect(content, "类型", "LayerType", Object.entries(types), entry?.Type || "Body");
settingsSelect(content, "样式", "DrawParam", [["", "无"], ...settings.data.resources.filter(item => item.kind === "DrawParam").map(item => [item.id, item.name])], referenceIDKey(entry?.DrawParam)); settingsSelect(content, "样式", "DrawParam", [["", "无"], ...settings.data.resources.filter(item => item.kind === "DrawParam").map(item => [item.id, item.name])], referenceIDKey(entry?.DrawParam));
content.querySelectorAll("select").forEach(field => { field.disabled = !state.editing; }); content.querySelectorAll("select").forEach(field => { field.disabled = !state.editing; });
if (state.editing) settingsButton("定位", () => locateSettingsObjects(entry?.ObjectIDs || []), !entry?.ObjectCount, "LocateLayer", properties.actions, "图层没有可定位的图元");
content.append(properties.actions);
if (!state.editing) return; if (!state.editing) return;
const write = async (operation, order = 0, objects = []) => { const write = async (operation, order = 0, objects = []) => {
const selected = settings.selected; const selected = settings.selected;
if (!entry && operation === "add") settings.selected = entries.length; if (!entry && operation === "add") settings.selected = entries.length;
if (!await commitSettings("ofdgoChangeLayer", operation, settings.page, entry?.ID || "", settingsValue("LayerType"), settingsValue("DrawParam"), order, objects)) settings.selected = selected; if (!await commitSettings("ofdgoChangeLayer", operation, settings.page, entry?.ID || "", settingsValue("LayerType"), settingsValue("DrawParam"), order, objects)) settings.selected = selected;
}; };
settingsButton(entry ? "修改" : "添加", () => write(entry ? "update" : "add"), false, "Save"); settingsButton(entry ? "修改" : "添加", () => write(entry ? "update" : "add"), false, "Save", properties.actions);
settingsButton("删除", () => write("delete"), !entry || entry.ObjectCount > 0, "Delete"); settingsButton("删除", () => write("delete"), !entry || entry.ObjectCount > 0, "Delete", properties.actions, entry?.ObjectCount ? "图层仍有图元" : "未选择图层");
settingsButton("上移", () => write("move", entry.Order - 1), !entry || entry.Order === 0, "Up"); settingsButton("上移", () => write("move", entry.Order - 1), !entry || entry.Order === 0, "Up", properties.actions);
settingsButton("下移", () => write("move", entry.Order + 1), !entry || entry.Order >= entries.filter(item => item.Type === entry.Type).length - 1, "Down"); settingsButton("下移", () => write("move", entry.Order + 1), !entry || entry.Order >= entries.filter(item => item.Type === entry.Type).length - 1, "Down", properties.actions);
if (entry) { if (entry) {
const objects = settingsSelect(content, "图元", "LayerObjects", (settings.data.objects || []).map(item => [item.id, `${item.id} · ${item.text || "图元"}`]), null); const binding = settingsSection("图元");
const objects = settingsSelect(binding.section, "图元", "LayerObjects", (settings.data.objects || []).filter(item => !entry.ObjectIDs?.includes(item.id)).map(item => [item.id, `${item.id} · ${item.text || "图元"}`]), null);
objects.multiple = true; objects.multiple = true;
objects.size = Math.min(4, Math.max(2, objects.options.length)); objects.size = Math.min(4, Math.max(2, objects.options.length));
for (const option of objects.options) option.selected = canvasEditor.items().some(item => item.index === settings.page && item.id === option.value && !item.scoped); for (const option of objects.options) option.selected = canvasEditor.items().some(item => item.index === settings.page && item.id === option.value && !item.scoped);
settingsButton("移入", () => write("objects", 0, [...objects.selectedOptions].map(option => option.value)), !objects.options.length, "MoveObjects"); settingsButton("选取", () => beginSettingsPick("layer"), Boolean(state.composite), "PickLayer", binding.actions);
const move = settingsButton("移入", () => write("objects", 0, [...objects.selectedOptions].map(option => option.value)), !objects.selectedOptions.length, "MoveObjects", binding.actions);
objects.addEventListener("change", () => { move.disabled = !objects.selectedOptions.length; });
binding.section.append(binding.actions);
} }
} }
function renderTemplateSettings() { function renderTemplateSettings() {
const entries = settings.data.page.templates || [], bindings = settings.data.page.bindings || [], content = settingsElements.Content; const entries = settings.data.page.templates || [], bindings = settings.data.page.bindings || [];
settingsPages(settingsElements.Context, "Page", settings.page, value => { settings.page = Number(value); loadDocumentSettings(); }); settingsPages(settingsElements.Context, "Page", settings.page, value => { settings.page = Number(value); loadDocumentSettings(); });
if (settings.selected >= entries.length) settings.selected = -1; if (settings.selected >= entries.length) settings.selected = -1;
if (!state.editing && settings.selected < 0 && entries.length) settings.selected = 0; if (!state.editing && settings.selected < 0 && entries.length) settings.selected = 0;
@@ -5572,17 +5759,31 @@ function renderTemplateSettings() {
settingsSelect(settingsElements.Context, "模板", "Entry", options, settings.selected, value => { settings.selected = Number(value); renderDocumentSettings(); }); settingsSelect(settingsElements.Context, "模板", "Entry", options, settings.selected, value => { settings.selected = Number(value); renderDocumentSettings(); });
const entry = entries[settings.selected]; const entry = entries[settings.selected];
if (!entry && !state.editing) { settingsElements.Status.textContent = "暂无模板"; return; } if (!entry && !state.editing) { settingsElements.Status.textContent = "暂无模板"; return; }
const properties = settingsSection("属性"), content = properties.section;
settingsField(content, "名称", "Name", entry?.Name); settingsField(content, "名称", "Name", entry?.Name);
settingsSelect(content, "层次", "TemplateOrder", [["Background", "背景"], ["Foreground", "前景"]], entry?.ZOrder || "Background"); settingsSelect(content, "层次", "TemplateOrder", [["Background", "背景"], ["Foreground", "前景"]], entry?.ZOrder || "Background");
if (entry) appendInfoLine(content, "文件", entry.BaseLoc); if (entry) appendInfoLine(content, "文件", entry.BaseLoc);
content.querySelectorAll("input,select").forEach(field => { field.disabled = !state.editing; }); content.querySelectorAll("input,select").forEach(field => { field.disabled = !state.editing; });
const bound = entry ? bindings.findIndex(binding => referenceIDKey(binding.TemplateID) === referenceIDKey(entry.ID)) : -1; const bound = entry ? bindings.findIndex(binding => referenceIDKey(binding.TemplateID) === referenceIDKey(entry.ID)) : -1;
if (entry && bound >= 0) { const references = (settings.data.page.templateReferences || []).filter(reference => referenceIDKey(reference.TemplateID) === referenceIDKey(entry?.ID));
const order = bindings[bound].ZOrder; if (references.length) {
if (!state.editing) appendInfoLine(content, "绑定层次", {Background: "背景", Foreground: "前景"}[order] || "默认"); const reference = settingsSelect(content, "引用", "TemplateReference", references.map((ref, index) => [index, `第${ref.PageIndex + 1}页`]), 0);
settingsButton("定位", () => locateSettingsPage(references[Number(reference.value)].PageIndex), false, "LocateTemplate", properties.actions);
} }
if (!state.editing) return; content.append(properties.actions);
settingsPages(content, "SourcePage", settings.page); if (!state.editing) {
if (bound >= 0) appendInfoLine(content, "绑定层次", {Background: "背景", Foreground: "前景"}[bindings[bound].ZOrder] || "默认");
return;
}
const source = entry ? document.createElement("details") : content;
if (entry) {
source.className = "settings-section";
const title = document.createElement("summary");
title.textContent = "内容";
source.append(title);
settingsElements.Content.append(source);
}
settingsPages(source, "SourcePage", settings.page);
document.querySelector("#settingsSourcePage").closest("label").firstElementChild.textContent = "来源页"; document.querySelector("#settingsSourcePage").closest("label").firstElementChild.textContent = "来源页";
const write = async (operation, order = 0) => { const write = async (operation, order = 0) => {
if (operation === "replace" && !window.confirm("替换模板内容?当前文档中引用此模板的页面将同步更新。")) return; if (operation === "replace" && !window.confirm("替换模板内容?当前文档中引用此模板的页面将同步更新。")) return;
@@ -5590,15 +5791,18 @@ function renderTemplateSettings() {
if (operation === "add") settings.selected = entries.length; if (operation === "add") settings.selected = entries.length;
if (!await commitSettings("ofdgoChangeTemplate", operation, ["add", "replace"].includes(operation) ? Number(settingsValue("SourcePage")) : settings.page, entry?.ID || "", settingsValue("Name"), ["bind", "order"].includes(operation) ? settingsValue("BindingOrder") : settingsValue("TemplateOrder"), order)) settings.selected = selected; if (!await commitSettings("ofdgoChangeTemplate", operation, ["add", "replace"].includes(operation) ? Number(settingsValue("SourcePage")) : settings.page, entry?.ID || "", settingsValue("Name"), ["bind", "order"].includes(operation) ? settingsValue("BindingOrder") : settingsValue("TemplateOrder"), order)) settings.selected = selected;
}; };
settingsButton(entry ? "修改" : "添加", () => write(entry ? "update" : "add"), false, "Save"); settingsButton(entry ? "修改" : "添加", () => write(entry ? "update" : "add"), false, "Save", properties.actions);
settingsButton("替换", () => write("replace"), !entry, "Replace"); if (!entry) content.append(properties.actions);
settingsButton("删除", () => write("delete"), !entry || bound >= 0, "Delete"); if (entry) settingsButton("替换", () => write("replace"), false, "Replace", source);
settingsButton("删除", () => write("delete"), !entry || references.length > 0, "Delete", properties.actions, references.length ? "模板仍被页面引用" : "未选择模板");
if (entry) { if (entry) {
settingsSelect(content, "绑定层次", "BindingOrder", [["", "默认"], ["Background", "背景"], ["Foreground", "前景"]], bound < 0 ? "" : bindings[bound].ZOrder || ""); const binding = settingsSection("绑定");
settingsButton(bound < 0 ? "绑定" : "应用", () => write(bound < 0 ? "bind" : "order"), false, "Bind"); settingsSelect(binding.section, "层次", "BindingOrder", [["", "默认"], ["Background", "背景"], ["Foreground", "前景"]], bound < 0 ? "" : bindings[bound].ZOrder || "");
settingsButton("解绑", () => write("unbind"), bound < 0, "Unbind"); settingsButton(bound < 0 ? "绑定" : "应用", () => write(bound < 0 ? "bind" : "order"), false, "Bind", binding.actions);
settingsButton("上移", () => write("move", bound - 1), bound <= 0, "Up"); settingsButton("解绑", () => write("unbind"), bound < 0, "Unbind", binding.actions, "当前页未绑定此模板");
settingsButton("下移", () => write("move", bound + 1), bound < 0 || bound >= bindings.length - 1, "Down"); settingsButton("上移", () => write("move", bound - 1), bound <= 0, "Up", binding.actions);
settingsButton("下移", () => write("move", bound + 1), bound < 0 || bound >= bindings.length - 1, "Down", binding.actions);
binding.section.append(binding.actions);
} }
} }
@@ -5650,6 +5854,85 @@ function renderExtensionSettings() {
} }
} }
function settingsPickControl(field, kind) {
const wrapper = document.createElement("span");
wrapper.className = "form-unit settings-pick";
field.replaceWith(wrapper);
wrapper.append(field);
settingsButton("选取", () => beginSettingsPick(kind), Boolean(state.composite), "PickObject", wrapper);
}
async function beginSettingsPick(kind) {
if (!state.editing || settings.busy || state.composite) return;
rememberSettingsDraft();
const page = kind === "style" ? Number(settingsValue("TargetPage")) : settings.page, sequence = state.openSeq;
if (!await renderPage(page, {fit: false}) || sequence !== state.openSeq || !settingsElements.Panel.open) return;
suspendSettings(kind);
}
function suspendSettings(kind) {
settings.suspended = true;
const parents = [resourceElements.Panel, el.objectStylePanel].filter(panel => panel.open);
resourceManager.pick = {sequence: state.openSeq, status: el.statusText.textContent, settings: kind, parents};
settingsElements.Panel.close();
for (const panel of parents) panel.close();
canvasEditor.enabled = false;
el.viewerPanel.classList.add("resource-picking");
setBusy(false);
setStatus(kind === "locate" ? "定位" : "选取图元");
el.viewerPanel.focus({preventScroll: true});
}
async function pickSettingsObject(event, picking) {
if (picking.settings === "locate") { finishResourcePick(); return; }
const available = canvasEditor.itemsAt(event).filter(item => !item.scoped && !item.draft && item.type !== "Annotation"
&& (picking.settings !== "style" || ["TextObject", "PathObject", "CompositeObject", "CompositeGraphicUnit"].includes(item.type))
&& (picking.settings !== "layer" || item.index === settings.page && !settings.data.page.layers[settings.selected]?.ObjectIDs?.includes(item.id)));
if (!available.length) return;
const choose = async item => {
if (resourceManager.pick !== picking || picking.sequence !== state.openSeq) return;
finishResourcePick();
if (picking.settings === "action") {
settings.page = item.index;
settings.object = item.id;
settings.selected = -1;
await loadDocumentSettings();
} else if (picking.settings === "style") {
settings.page = item.index;
settings.object = item.id;
document.querySelector("#settingsTargetPage").value = String(item.index);
await loadSettingsObjects();
document.querySelector("#settingsTargetObject").value = item.id;
document.querySelector("#settingsTargetObject").dispatchEvent(new Event("change"));
} else {
const field = document.querySelector("#settingsLayerObjects");
for (const option of field.options) if (option.value === item.id) option.selected = true;
field.dispatchEvent(new Event("change"));
}
};
if (available.length === 1) await choose(available[0]);
else resourceChoices(available, item => `${item.id} · ${(item.text || "图元").slice(0, 50)}`, choose);
}
async function locateSettingsObjects(ids) {
if (settings.busy) return;
const sequence = settings.sequence, data = await loadPageData(settings.page);
if (sequence !== settings.sequence || !settingsElements.Panel.open) return;
const objects = (data.objects || []).filter(item => ids.includes(item.id) && !item.scoped);
if (!objects.length) { settingsElements.Status.textContent = "暂无可见位置"; return; }
const box = selectionBounds(objects);
rememberSettingsDraft();
suspendSettings("locate");
await focusPageRegion({page: settings.page + 1, ...box}, "图层", "center");
}
async function locateSettingsPage(page) {
rememberSettingsDraft();
suspendSettings("locate");
const area = state.doc.pages[page];
await focusPageRegion({page: page + 1, x: 0, y: 0, width: area.width, height: area.height}, "模板", "center");
}
async function openResourceManagement() { async function openResourceManagement() {
if (!state.doc || !state.ready || document.body.hasAttribute("aria-busy")) return; if (!state.doc || !state.ready || document.body.hasAttribute("aria-busy")) return;
if (resourceManager.pick) { finishResourcePick(); return; } if (resourceManager.pick) { finishResourcePick(); return; }
@@ -5852,11 +6135,24 @@ function finishResourcePick(reopen = true) {
if (!picking) return; if (!picking) return;
resourceManager.pick = null; resourceManager.pick = null;
el.viewerPanel.classList.remove("resource-picking"); el.viewerPanel.classList.remove("resource-picking");
if (picking.settings && (!reopen || picking.sequence !== state.openSeq)) {
settings.suspended = false;
settings.drafts.clear();
settings.view = "";
settings.sequence++;
}
if (picking.sequence === state.openSeq) { if (picking.sequence === state.openSeq) {
updateEditorTools(); updateEditorTools();
setBusy(document.body.hasAttribute("aria-busy")); setBusy(document.body.hasAttribute("aria-busy"));
setStatus(picking.status); setStatus(picking.status);
if (reopen && !resourceElements.Panel.open) { if (picking.settings) {
if (reopen) {
for (const panel of picking.parents) if (!panel.open) panel.showModal();
settingsElements.Panel.showModal();
(document.querySelector("#settingsPickObject") || document.querySelector("#settingsPickLayer"))?.focus();
}
settings.suspended = false;
} else if (reopen && !resourceElements.Panel.open) {
resourceElements.Panel.showModal(); resourceElements.Panel.showModal();
resourceElements.Pick.focus(); resourceElements.Pick.focus();
} }
@@ -5869,6 +6165,7 @@ function pickResourceObject(event) {
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
if (resourceManager.busy || document.body.hasAttribute("aria-busy")) return; if (resourceManager.busy || document.body.hasAttribute("aria-busy")) return;
if (picking.settings) { pickSettingsObject(event, picking).catch(error => { finishResourcePick(); settingsElements.Status.textContent = error.message; }); return; }
const items = canvasEditor.itemsAt(event).filter(item => !item.scoped && !item.draft); const items = canvasEditor.itemsAt(event).filter(item => !item.scoped && !item.draft);
if (!items.length) { setStatus("未选中图元"); return; } if (!items.length) { setStatus("未选中图元"); return; }
resourceTask(async () => { resourceTask(async () => {
@@ -5882,7 +6179,7 @@ function pickResourceObject(event) {
finishResourcePick(); finishResourcePick();
resourceElements.Page.value = String(item.index); resourceElements.Page.value = String(item.index);
await loadResourceTargets(item.id); await loadResourceTargets(item.id);
resourceElements.Status.textContent = "已选取"; resourceElements.Status.textContent = "";
}; };
if (available.length === 1) await choose(available[0]); if (available.length === 1) await choose(available[0]);
else resourceChoices(available, item => `${item.id} · ${(item.text || "图元").slice(0, 50)}`, item => resourceTask(() => choose(item))); else resourceChoices(available, item => `${item.id} · ${(item.text || "图元").slice(0, 50)}`, item => resourceTask(() => choose(item)));
+18 -4
View File
@@ -3830,13 +3830,20 @@ func applyStyleResource(args []js.Value) (any, error) {
}) })
} }
// pageSettings 获取当前页的图层、模板定义和绑定信息,不修改文档 // pageSettings 获取当前页设置,图层视图跳过全部普通页模板引用查询,不修改文档
// 入参: args 页面索引 // 入参: args 页面索引及可选类型layers或templates,省略类型时返回全量数据
// 返回: any 页面设置, error 读取错误 // 返回: any 页面设置, error 读取错误
func pageSettings(args []js.Value) (any, error) { func pageSettings(args []js.Value) (any, error) {
if currentSession == nil || len(args) != 1 { if currentSession == nil || len(args) < 1 || len(args) > 2 {
return nil, fmt.Errorf("invalid page settings") return nil, fmt.Errorf("invalid page settings")
} }
kind := "templates"
if len(args) == 2 {
kind = args[1].String()
if kind != "layers" && kind != "templates" {
return nil, fmt.Errorf("invalid page settings type %q", kind)
}
}
reader := currentSession.Reader reader := currentSession.Reader
layers, err := reader.Layers(args[0].Int()) layers, err := reader.Layers(args[0].Int())
if err != nil { if err != nil {
@@ -3850,7 +3857,14 @@ func pageSettings(args []js.Value) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return map[string]any{"layers": layers, "templates": templates, "bindings": page.Template}, nil references := []ofdgo.TemplateReference{}
if kind == "templates" {
references, err = reader.TemplateReferences()
if err != nil {
return nil, err
}
}
return map[string]any{"layers": layers, "templates": templates, "bindings": page.Template, "templateReferences": references}, nil
} }
// changeLayer 调用图层管理接口,组合修改使用同一次事务 // changeLayer 调用图层管理接口,组合修改使用同一次事务
+7 -1
View File
@@ -24,12 +24,14 @@ import (
) )
// LayerInfo 描述普通页面图层,Order为同类型零基顺序,ObjectCount包含页块中的图元。 // LayerInfo 描述普通页面图层,Order为同类型零基顺序,ObjectCount包含页块中的图元。
// ObjectIDs按层内文件顺序列出图元标识,包含PageBlock成员但不展开复合图元,空层返回空切片。
type LayerInfo struct { type LayerInfo struct {
ID string ID string
Type string Type string
DrawParam string DrawParam string
Order int Order int
ObjectCount int ObjectCount int
ObjectIDs []string
} }
// Layers 按文件顺序返回普通页面图层,省略的类型规范化为Body,不包含模板图层。 // Layers 按文件顺序返回普通页面图层,省略的类型规范化为Body,不包含模板图层。
@@ -62,7 +64,11 @@ func editorLayerInfos(p *PageContent) []LayerInfo {
orders := make(map[string]int) orders := make(map[string]int)
for _, layer := range p.Content.Layer { for _, layer := range p.Content.Layer {
kind := editorLayerType(layer.Type) kind := editorLayerType(layer.Type)
result = append(result, LayerInfo{layer.ID, kind, layer.DrawParam, orders[kind], len(layer.Objects)}) ids := make([]string, len(layer.Objects))
for i, object := range layer.Objects {
ids[i] = editorObjectID(object)
}
result = append(result, LayerInfo{ID: layer.ID, Type: kind, DrawParam: layer.DrawParam, Order: orders[kind], ObjectCount: len(ids), ObjectIDs: ids})
orders[kind]++ orders[kind]++
} }
return result return result
+46
View File
@@ -21,6 +21,14 @@ import (
"path" "path"
) )
// TemplateReference 普通页面的模板引用,保留原始模板标识及绑定层次。
// PageIndex为当前文档的零基页面索引,PageID为页面标识;ZOrder为空表示沿用模板默认层次。
type TemplateReference struct {
PageIndex int
PageID string
Template
}
// Templates 返回当前文档及所选版本的模板定义副本,省略的ZOrder返回Background。 // Templates 返回当前文档及所选版本的模板定义副本,省略的ZOrder返回Background。
// 返回: []TemplatePage 独立模板清单, error 文档读取错误 // 返回: []TemplatePage 独立模板清单, error 文档读取错误
func (e *Editor) Templates() ([]TemplatePage, error) { func (e *Editor) Templates() ([]TemplatePage, error) {
@@ -59,6 +67,44 @@ func (e *Editor) PageTemplates(page int) ([]Template, error) {
return append([]Template{}, p.Template...), nil return append([]Template{}, p.Template...), nil
} }
// TemplateReferences 返回当前文档及所选版本全部普通页面的模板引用副本,不包含模板页自身引用。
// 清单按页面索引及页内引用文件顺序排列,无引用时返回空切片,读取失败时不返回部分结果。
// 返回: []TemplateReference 引用定位及绑定层次清单, error 页面读取错误
func (e *Editor) TemplateReferences() ([]TemplateReference, error) {
result := []TemplateReference{}
for i := range e.pages {
refs, err := e.PageTemplates(i)
if err != nil {
return nil, err
}
for _, ref := range refs {
result = append(result, TemplateReference{PageIndex: i, PageID: e.pages[i].ID, Template: ref})
}
}
return result, nil
}
// TemplateReferences 只读枚举当前文档及所选版本全部普通页面的模板引用,不创建编辑器或遍历其他文档、版本。
// 返回独立副本,按页面索引及页内引用文件顺序排列;无引用时返回空切片,读取失败时不返回部分结果。
// 返回: []TemplateReference 引用定位及绑定层次清单, error 文档或页面读取错误
func (r *Reader) TemplateReferences() ([]TemplateReference, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
result := []TemplateReference{}
for i, item := range doc.Pages.Page {
page, err := r.PageContentByIndex(i)
if err != nil {
return nil, err
}
for _, ref := range page.Template {
result = append(result, TemplateReference{PageIndex: i, PageID: item.ID, Template: ref})
}
}
return result, nil
}
// BindTemplate 在页面末尾绑定模板,空ZOrder沿用模板默认层次,重复绑定拒绝。 // BindTemplate 在页面末尾绑定模板,空ZOrder沿用模板默认层次,重复绑定拒绝。
// 入参: page 页面索引, id 模板标识, zOrder 页面覆盖层次 // 入参: page 页面索引, id 模板标识, zOrder 页面覆盖层次
// 返回: error 标识、层次或重复绑定错误 // 返回: error 标识、层次或重复绑定错误