mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
7bdb03f7bd
commit
523f66983e
6 files changed
+462
-78
No files matched your search
@@ -364,7 +364,7 @@
|
|||||||
<div id="resourceEmpty" class="resource-empty" hidden>暂无资源</div>
|
<div id="resourceEmpty" class="resource-empty" hidden>暂无资源</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row"><label for="resourceReference">引用</label><select id="resourceReference" aria-label="引用位置"></select></div>
|
<div class="form-row"><label for="resourceReference">引用</label><div class="resource-target"><select id="resourceReference" aria-label="引用位置"></select><button id="resourceLocate" class="button" type="button">定位</button></div></div>
|
||||||
<div class="resource-actions">
|
<div class="resource-actions">
|
||||||
<button id="resourceAdd" class="button" type="button" title="添加资源" aria-label="添加资源">添加</button>
|
<button id="resourceAdd" class="button" type="button" title="添加资源" aria-label="添加资源">添加</button>
|
||||||
<button id="resourcePreview" class="button" type="button">预览</button>
|
<button id="resourcePreview" class="button" type="button">预览</button>
|
||||||
@@ -372,16 +372,14 @@
|
|||||||
<button id="resourceRename" class="button" type="button" hidden>改名</button>
|
<button id="resourceRename" class="button" type="button" hidden>改名</button>
|
||||||
<button id="resourceReplace" class="button" type="button">替换</button>
|
<button id="resourceReplace" class="button" type="button">替换</button>
|
||||||
<button id="resourceDelete" class="button" type="button">删除</button>
|
<button id="resourceDelete" class="button" type="button">删除</button>
|
||||||
<button id="resourceLocate" class="button" type="button">定位</button>
|
|
||||||
<button id="resourceClose" class="button" type="button">关闭</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="resourceBinding" class="resource-binding" hidden>
|
<div id="resourceBinding" class="resource-binding" hidden>
|
||||||
<div class="form-row"><label for="resourcePage">页码</label><select id="resourcePage"></select></div>
|
<div class="form-row"><label for="resourcePage">页码</label><select id="resourcePage"></select></div>
|
||||||
<div class="form-row"><label for="resourceObject">图元</label><select id="resourceObject"></select></div>
|
<div class="form-row"><label for="resourceObject">图元</label><div class="resource-target"><select id="resourceObject"></select><button id="resourcePick" class="button" type="button">选取</button></div></div>
|
||||||
<div class="form-actions"><button id="resourceUnbind" class="button" type="button">解绑</button><button id="resourceBind" class="button" type="button">绑定</button></div>
|
<div class="form-actions"><button id="resourceUnbind" class="button" type="button">解绑</button><button id="resourceBind" class="button" type="button">绑定</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="resourceViewer" class="resource-viewer" hidden></div>
|
<div id="resourceViewer" class="resource-viewer" hidden></div>
|
||||||
<div id="resourceStatus" class="form-status" role="status"></div>
|
<div class="form-actions"><div id="resourceStatus" class="form-status" role="status"></div><button id="resourceClose" class="button" type="button">关闭</button></div>
|
||||||
<input id="resourceFile" type="file" hidden>
|
<input id="resourceFile" type="file" hidden>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
+24
-2
@@ -1552,6 +1552,11 @@ body[data-hide-meta] .meta-panel {
|
|||||||
width: 600px;
|
width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-dialog form > .form-actions {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.resource-heading,
|
.resource-heading,
|
||||||
.resource-option {
|
.resource-option {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1641,8 +1646,25 @@ body[data-hide-meta] .meta-panel {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-actions > :last-child {
|
.resource-target {
|
||||||
margin-left: auto;
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-target select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-target button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-picking,
|
||||||
|
.resource-picking .edit-object {
|
||||||
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-binding {
|
.resource-binding {
|
||||||
|
|||||||
+156
-34
@@ -23,8 +23,8 @@ const batch = { items: [], formats: [], running: false, canceled: false, worker:
|
|||||||
const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "FontRow", "FontMode", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)]));
|
const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "FontRow", "FontMode", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)]));
|
||||||
const fontBinding = { font: null, choices: [], sequence: 0 };
|
const fontBinding = { font: null, choices: [], sequence: 0 };
|
||||||
const fontBindingElements = Object.fromEntries(["Panel", "Form", "Title", "Name", "SourceRow", "Source", "File", "Hint", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#fontBinding${name}`)]));
|
const fontBindingElements = Object.fromEntries(["Panel", "Form", "Title", "Name", "SourceRow", "Source", "File", "Hint", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#fontBinding${name}`)]));
|
||||||
const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "Add", "Preview", "Export", "Rename", "Replace", "Delete", "Locate", "Close", "Binding", "Page", "Object", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)]));
|
const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "Add", "Preview", "Export", "Rename", "Replace", "Delete", "Locate", "Close", "Binding", "Page", "Object", "Pick", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)]));
|
||||||
const resourceManager = { items: [], selected: 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 };
|
||||||
|
|
||||||
let wasmPromise = null;
|
let wasmPromise = null;
|
||||||
let remoteDownload = null;
|
let remoteDownload = null;
|
||||||
@@ -736,19 +736,17 @@ resourceElements.Close.addEventListener("click", () => resourceElements.Panel.cl
|
|||||||
resourceElements.Form.addEventListener("submit", event => event.preventDefault());
|
resourceElements.Form.addEventListener("submit", event => event.preventDefault());
|
||||||
resourceElements.Panel.addEventListener("cancel", event => { if (resourceManager.busy) event.preventDefault(); });
|
resourceElements.Panel.addEventListener("cancel", event => { if (resourceManager.busy) event.preventDefault(); });
|
||||||
resourceElements.Panel.addEventListener("close", () => { resourceManager.sequence++; releaseResourcePreview(); });
|
resourceElements.Panel.addEventListener("close", () => { resourceManager.sequence++; releaseResourcePreview(); });
|
||||||
resourceElements.Kind.addEventListener("change", renderResourceList);
|
resourceElements.Kind.addEventListener("change", () => resourceTask(renderResourceList));
|
||||||
resourceElements.Reference.addEventListener("change", updateResourceControls);
|
resourceElements.Reference.addEventListener("change", () => resourceTask(syncResourceReference));
|
||||||
resourceElements.Page.addEventListener("change", () => resourceTask(loadResourceTargets));
|
resourceElements.Page.addEventListener("change", () => resourceTask(() => loadResourceTargets("")));
|
||||||
resourceElements.Object.addEventListener("change", updateResourceControls);
|
resourceElements.Object.addEventListener("change", updateResourceControls);
|
||||||
|
resourceElements.Pick.addEventListener("click", () => resourceTask(beginResourcePick));
|
||||||
resourceElements.Preview.addEventListener("click", () => resourceTask(previewManagedResource));
|
resourceElements.Preview.addEventListener("click", () => resourceTask(previewManagedResource));
|
||||||
resourceElements.Export.addEventListener("click", () => {
|
resourceElements.Export.addEventListener("click", () => {
|
||||||
const item = resourceManager.selected;
|
const item = resourceManager.selected;
|
||||||
if (item) downloadResource({ id: JSON.stringify([item.kind, item.id]), fileName: resourceFileName(item) }, "ofdgoExportResource", "资源");
|
if (item) downloadResource({ id: JSON.stringify([item.kind, item.id]), fileName: resourceFileName(item) }, "ofdgoExportResource", "资源");
|
||||||
});
|
});
|
||||||
resourceElements.Locate.addEventListener("click", () => {
|
resourceElements.Locate.addEventListener("click", () => resourceTask(locateManagedResource));
|
||||||
const reference = resourceManager.selected?.references?.[Number(resourceElements.Reference.value)];
|
|
||||||
if (reference?.page >= 0) { resourceElements.Panel.close(); renderPage(reference.page); }
|
|
||||||
});
|
|
||||||
resourceElements.Add.addEventListener("click", () => {
|
resourceElements.Add.addEventListener("click", () => {
|
||||||
if (!state.editing) return;
|
if (!state.editing) return;
|
||||||
const kind = resourceElements.Kind.value;
|
const kind = resourceElements.Kind.value;
|
||||||
@@ -1432,6 +1430,13 @@ const sealPosition = Object.fromEntries(["Layer", "Stamp", "Image"].map(name =>
|
|||||||
let sealPreview = null;
|
let sealPreview = null;
|
||||||
document.querySelector("#signPick").addEventListener("click", openSealPosition);
|
document.querySelector("#signPick").addEventListener("click", openSealPosition);
|
||||||
el.viewerPanel.addEventListener("pointerdown", beginSealDrag, true);
|
el.viewerPanel.addEventListener("pointerdown", beginSealDrag, true);
|
||||||
|
el.viewerPanel.addEventListener("pointerdown", pickResourceObject, true);
|
||||||
|
document.addEventListener("keydown", event => {
|
||||||
|
if (!resourceManager.pick || el.objectPicker.open) return;
|
||||||
|
if (event.key === "Escape") { event.preventDefault(); event.stopImmediatePropagation(); finishResourcePick(); return; }
|
||||||
|
if (event.target.closest?.("input, textarea, select")) return;
|
||||||
|
if (event.key !== "Tab") { event.preventDefault(); event.stopImmediatePropagation(); }
|
||||||
|
}, true);
|
||||||
el.viewerPanel.addEventListener("pointermove", moveSealDrag, true);
|
el.viewerPanel.addEventListener("pointermove", moveSealDrag, true);
|
||||||
el.viewerPanel.addEventListener("pointerup", confirmSealPosition, true);
|
el.viewerPanel.addEventListener("pointerup", confirmSealPosition, true);
|
||||||
el.viewerPanel.addEventListener("pointercancel", () => { if (sealPreview) finishSealPosition(); }, true);
|
el.viewerPanel.addEventListener("pointercancel", () => { if (sealPreview) finishSealPosition(); }, true);
|
||||||
@@ -1757,7 +1762,7 @@ function handleKeyDown(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formDialogOpen() {
|
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
|
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
|
||||||
|| 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4710,6 +4715,7 @@ function updateFontPermissionHint() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearSecurityForms() {
|
function clearSecurityForms() {
|
||||||
|
finishResourcePick(false);
|
||||||
if (sealPreview) finishSealPosition(false);
|
if (sealPreview) finishSealPosition(false);
|
||||||
finishCredentials(null);
|
finishCredentials(null);
|
||||||
clearEncryptionForm();
|
clearEncryptionForm();
|
||||||
@@ -5071,10 +5077,11 @@ async function renderPage(index, options = {}) {
|
|||||||
|
|
||||||
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; }
|
||||||
resourceElements.Panel.showModal();
|
resourceElements.Panel.showModal();
|
||||||
resourceElements.Page.replaceChildren(...state.doc.pages.map((page, index) => new Option(`第${index + 1}页`, String(index))));
|
resourceElements.Page.replaceChildren(...state.doc.pages.map((page, index) => new Option(`第${index + 1}页`, String(index))));
|
||||||
resourceElements.Page.value = String(state.pageIndex);
|
resourceElements.Page.value = String(state.pageIndex);
|
||||||
await resourceTask(async () => { await loadManagedResources(); if (state.editing) await loadResourceTargets(); });
|
await resourceTask(loadManagedResources);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resourceTask(action) {
|
async function resourceTask(action) {
|
||||||
@@ -5110,10 +5117,10 @@ async function loadManagedResources() {
|
|||||||
const items = await callWASM("ofdgoResources");
|
const items = await callWASM("ofdgoResources");
|
||||||
if (sequence !== state.openSeq || !resourceElements.Panel.open) return;
|
if (sequence !== state.openSeq || !resourceElements.Panel.open) return;
|
||||||
resourceManager.items = items || [];
|
resourceManager.items = items || [];
|
||||||
renderResourceList();
|
await renderResourceList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResourceList() {
|
async function renderResourceList() {
|
||||||
const items = resourceManager.items.filter(item => item.kind === resourceElements.Kind.value);
|
const items = resourceManager.items.filter(item => item.kind === resourceElements.Kind.value);
|
||||||
const selected = items.find(item => item.id === resourceManager.selected?.id && item.kind === resourceManager.selected?.kind) || items[0];
|
const selected = items.find(item => item.id === resourceManager.selected?.id && item.kind === resourceManager.selected?.kind) || items[0];
|
||||||
resourceElements.List.replaceChildren();
|
resourceElements.List.replaceChildren();
|
||||||
@@ -5130,21 +5137,24 @@ function renderResourceList() {
|
|||||||
span.textContent = value;
|
span.textContent = value;
|
||||||
button.append(span);
|
button.append(span);
|
||||||
}
|
}
|
||||||
button.addEventListener("click", () => selectManagedResource(item));
|
button.addEventListener("click", () => resourceTask(() => selectManagedResource(item)));
|
||||||
button.addEventListener("keydown", event => {
|
button.addEventListener("keydown", event => {
|
||||||
const index = items.indexOf(item);
|
const index = items.indexOf(item);
|
||||||
const target = { ArrowDown: Math.min(items.length - 1, index + 1), ArrowUp: Math.max(0, index - 1), Home: 0, End: items.length - 1 }[event.key];
|
const target = { ArrowDown: Math.min(items.length - 1, index + 1), ArrowUp: Math.max(0, index - 1), Home: 0, End: items.length - 1 }[event.key];
|
||||||
if (target == null) return;
|
if (target == null) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
selectManagedResource(items[target]);
|
resourceTask(() => selectManagedResource(items[target])).then(() => {
|
||||||
resourceElements.List.children[target].focus();
|
if (resourceElements.Panel.open && resourceManager.selected === items[target]) resourceElements.List.children[target]?.focus();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
resourceElements.List.append(button);
|
resourceElements.List.append(button);
|
||||||
}
|
}
|
||||||
selectManagedResource(selected);
|
await selectManagedResource(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectManagedResource(item) {
|
async function selectManagedResource(item) {
|
||||||
|
const same = item?.id === resourceManager.selected?.id && item?.kind === resourceManager.selected?.kind;
|
||||||
|
const reference = same ? resourceElements.Reference.value : "0";
|
||||||
releaseResourcePreview();
|
releaseResourcePreview();
|
||||||
resourceManager.selected = item || null;
|
resourceManager.selected = item || null;
|
||||||
const items = resourceManager.items.filter(entry => entry.kind === resourceElements.Kind.value);
|
const items = resourceManager.items.filter(entry => entry.kind === resourceElements.Kind.value);
|
||||||
@@ -5156,7 +5166,10 @@ function selectManagedResource(item) {
|
|||||||
const roles = {Font: "字体", ResourceID: "资源", Substitution: "替换", ImageMask: "蒙版", Thumbnail: "缩略", AttachID: "附件"};
|
const roles = {Font: "字体", ResourceID: "资源", Substitution: "替换", ImageMask: "蒙版", Thumbnail: "缩略", AttachID: "附件"};
|
||||||
resourceElements.Reference.replaceChildren(...references.map((ref, index) => new Option(`${ref.page >= 0 ? `第${ref.page + 1}页` : ref.file}${ref.object ? ` · ${ref.object}` : ""} · ${roles[ref.role] || ref.role}`, String(index))));
|
resourceElements.Reference.replaceChildren(...references.map((ref, index) => new Option(`${ref.page >= 0 ? `第${ref.page + 1}页` : ref.file}${ref.object ? ` · ${ref.object}` : ""} · ${roles[ref.role] || ref.role}`, String(index))));
|
||||||
if (!references.length) resourceElements.Reference.add(new Option(!item || item.complete ? "暂无引用" : "引用未知", ""));
|
if (!references.length) resourceElements.Reference.add(new Option(!item || item.complete ? "暂无引用" : "引用未知", ""));
|
||||||
|
else resourceElements.Reference.value = references[Number(reference)] ? String(Number(reference)) : "0";
|
||||||
updateResourceControls();
|
updateResourceControls();
|
||||||
|
if (same) await loadResourceTargets();
|
||||||
|
else await syncResourceReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateResourceControls() {
|
function updateResourceControls() {
|
||||||
@@ -5174,19 +5187,127 @@ function updateResourceControls() {
|
|||||||
resourceElements.Replace.textContent = item?.kind === "Font" ? item.file ? "外置" : "嵌入" : "替换";
|
resourceElements.Replace.textContent = item?.kind === "Font" ? item.file ? "外置" : "嵌入" : "替换";
|
||||||
resourceElements.Delete.disabled = disabled || !item || item.kind !== "Attachment" && (!item.complete || Boolean(item.references?.length));
|
resourceElements.Delete.disabled = disabled || !item || item.kind !== "Attachment" && (!item.complete || Boolean(item.references?.length));
|
||||||
resourceElements.Delete.title = item?.kind === "Attachment" ? "删除附件" : item?.references?.length ? "资源仍被引用" : "删除资源";
|
resourceElements.Delete.title = item?.kind === "Attachment" ? "删除附件" : item?.references?.length ? "资源仍被引用" : "删除资源";
|
||||||
resourceElements.Locate.disabled = disabled || !(item?.references?.[Number(resourceElements.Reference.value)]?.page >= 0);
|
resourceElements.Locate.disabled = disabled || !item?.references?.length;
|
||||||
resourceElements.Binding.hidden = !editing || !actionable;
|
resourceElements.Binding.hidden = !editing || !actionable;
|
||||||
resourceElements.Bind.disabled = resourceElements.Unbind.disabled = disabled || !actionable || !resourceElements.Object.value;
|
const target = resourceManager.targets.find(target => target.id === resourceElements.Object.value);
|
||||||
|
resourceElements.Pick.disabled = disabled || !editing || !actionable || Boolean(state.composite);
|
||||||
|
resourceElements.Bind.disabled = disabled || !actionable || !target || target.bound;
|
||||||
|
resourceElements.Unbind.disabled = disabled || !actionable || !target?.bound;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadResourceTargets() {
|
async function syncResourceReference() {
|
||||||
|
const reference = resourceManager.selected?.references?.[Number(resourceElements.Reference.value)];
|
||||||
|
if (reference?.page >= 0) resourceElements.Page.value = String(reference.page);
|
||||||
|
await loadResourceTargets(reference?.page >= 0 ? reference.object : "");
|
||||||
|
updateResourceControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadResourceTargets(preferred) {
|
||||||
|
const resource = resourceManager.selected;
|
||||||
|
if (preferred === undefined) {
|
||||||
|
const reference = resource?.references?.[Number(resourceElements.Reference.value)];
|
||||||
|
preferred = resourceElements.Object.value || (reference?.page === Number(resourceElements.Page.value) ? reference.object : "");
|
||||||
|
}
|
||||||
|
resourceManager.targets = [];
|
||||||
resourceElements.Object.replaceChildren(new Option("选择图元", ""));
|
resourceElements.Object.replaceChildren(new Option("选择图元", ""));
|
||||||
|
if (!state.editing || !resource || !["Audio", "Video", "Attachment"].includes(resource.kind)) return;
|
||||||
const page = Number(resourceElements.Page.value), sequence = state.openSeq;
|
const page = Number(resourceElements.Page.value), sequence = state.openSeq;
|
||||||
const items = await callWASM("ofdgoResourceTargets", page);
|
const items = await callWASM("ofdgoResourceTargets", page, resource.kind, resource.id);
|
||||||
if (!resourceElements.Panel.open || sequence !== state.openSeq) return;
|
if (!resourceElements.Panel.open || sequence !== state.openSeq || resource !== resourceManager.selected || page !== Number(resourceElements.Page.value)) return;
|
||||||
for (const item of items || []) resourceElements.Object.add(new Option(`${item.id} · ${(item.text || "图元").slice(0, 50)}`, item.id));
|
resourceManager.targets = items || [];
|
||||||
|
const types = {TextObject: "文字", PathObject: "图形", ImageObject: "图片", CompositeObject: "组合", CompositeGraphicUnit: "组合"};
|
||||||
|
for (const item of resourceManager.targets) resourceElements.Object.add(new Option(`${item.id} · ${(item.text || types[item.type] || "图元").slice(0, 50)}${item.bound ? " · 已绑定" : ""}`, item.id));
|
||||||
const selected = canvasEditor.selected;
|
const selected = canvasEditor.selected;
|
||||||
if (selected?.index === page && !selected.scoped) resourceElements.Object.value = selected.id;
|
if (!preferred && selected?.index === page && !selected.scoped) preferred = selected.id;
|
||||||
|
resourceElements.Object.value = resourceManager.targets.some(item => item.id === preferred) ? preferred : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function locateManagedResource() {
|
||||||
|
const item = resourceManager.selected, sequence = state.openSeq;
|
||||||
|
if (!item?.references?.length) return;
|
||||||
|
const locations = await callWASM("ofdgoResourceLocations", item.kind, item.id, Number(resourceElements.Reference.value));
|
||||||
|
if (sequence !== state.openSeq || !resourceElements.Panel.open || item !== resourceManager.selected) return;
|
||||||
|
if (!locations?.length) { resourceElements.Status.textContent = "暂无可见位置"; return; }
|
||||||
|
const focus = async location => {
|
||||||
|
if (sequence !== state.openSeq) return;
|
||||||
|
resourceElements.Panel.close();
|
||||||
|
const box = location.bounds;
|
||||||
|
await focusPageRegion({page: location.page, x: box.X, y: box.Y, width: box.W, height: box.H}, "资源", "center");
|
||||||
|
if (sequence !== state.openSeq || !state.editing) return;
|
||||||
|
const target = canvasEditor.pageItems(location.page - 1).find(item => item.id === location.object && !item.scoped);
|
||||||
|
if (target) canvasEditor.select(target);
|
||||||
|
};
|
||||||
|
if (locations.length === 1) await focus(locations[0]);
|
||||||
|
else resourceChoices(locations, location => `第${location.page}页 · ${location.object}`, location => resourceTask(() => focus(location)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceChoices(items, label, choose) {
|
||||||
|
el.objectPickerList.replaceChildren();
|
||||||
|
for (const item of items) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "button";
|
||||||
|
button.textContent = label(item);
|
||||||
|
button.addEventListener("click", () => { el.objectPicker.close(); choose(item); });
|
||||||
|
el.objectPickerList.append(button);
|
||||||
|
}
|
||||||
|
el.objectPicker.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function beginResourcePick() {
|
||||||
|
if (!state.editing || state.composite || !["Audio", "Video", "Attachment"].includes(resourceManager.selected?.kind)) return;
|
||||||
|
const sequence = state.openSeq;
|
||||||
|
if (!await renderPage(Number(resourceElements.Page.value), {fit: false})) return;
|
||||||
|
if (sequence !== state.openSeq || !resourceElements.Panel.open) return;
|
||||||
|
resourceManager.pick = {sequence, status: el.statusText.textContent};
|
||||||
|
resourceElements.Panel.close();
|
||||||
|
canvasEditor.enabled = false;
|
||||||
|
el.viewerPanel.classList.add("resource-picking");
|
||||||
|
setBusy(false);
|
||||||
|
setStatus("选取图元");
|
||||||
|
el.viewerPanel.focus({preventScroll: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishResourcePick(reopen = true) {
|
||||||
|
const picking = resourceManager.pick;
|
||||||
|
if (!picking) return;
|
||||||
|
resourceManager.pick = null;
|
||||||
|
el.viewerPanel.classList.remove("resource-picking");
|
||||||
|
if (picking.sequence === state.openSeq) {
|
||||||
|
updateEditorTools();
|
||||||
|
setBusy(document.body.hasAttribute("aria-busy"));
|
||||||
|
setStatus(picking.status);
|
||||||
|
if (reopen && !resourceElements.Panel.open) {
|
||||||
|
resourceElements.Panel.showModal();
|
||||||
|
resourceElements.Pick.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickResourceObject(event) {
|
||||||
|
const picking = resourceManager.pick;
|
||||||
|
if (!picking || event.button !== 0) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
if (resourceManager.busy || document.body.hasAttribute("aria-busy")) return;
|
||||||
|
const items = canvasEditor.itemsAt(event).filter(item => !item.scoped && !item.draft);
|
||||||
|
if (!items.length) { setStatus("未选中图元"); return; }
|
||||||
|
resourceTask(async () => {
|
||||||
|
const resource = resourceManager.selected, page = items[0].index;
|
||||||
|
const targets = await callWASM("ofdgoResourceTargets", page, resource.kind, resource.id);
|
||||||
|
if (resourceManager.pick !== picking || picking.sequence !== state.openSeq) return;
|
||||||
|
const available = items.filter(item => targets?.some(target => target.id === item.id));
|
||||||
|
if (!available.length) { setStatus("图元不可绑定"); return; }
|
||||||
|
const choose = async item => {
|
||||||
|
if (resourceManager.pick !== picking || picking.sequence !== state.openSeq) return;
|
||||||
|
finishResourcePick();
|
||||||
|
resourceElements.Page.value = String(item.index);
|
||||||
|
await loadResourceTargets(item.id);
|
||||||
|
resourceElements.Status.textContent = "已选取";
|
||||||
|
};
|
||||||
|
if (available.length === 1) await choose(available[0]);
|
||||||
|
else resourceChoices(available, item => `${item.id} · ${(item.text || "图元").slice(0, 50)}`, item => resourceTask(() => choose(item)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseResourceFile(action) {
|
function chooseResourceFile(action) {
|
||||||
@@ -5207,7 +5328,7 @@ async function changeManagedResource(action, item, file) {
|
|||||||
: await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, Number(resourceElements.Page.value), resourceElements.Object.value);
|
: await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, Number(resourceElements.Page.value), resourceElements.Object.value);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
await loadManagedResources();
|
await loadManagedResources();
|
||||||
await loadResourceTargets();
|
if (action === "bind" || action === "unbind") resourceElements.Status.textContent = action === "bind" ? "已绑定" : "已解绑";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8922,7 +9043,7 @@ function appendSignaturePolicy(row, label, checked, ok) {
|
|||||||
appendSignatureCheck(row, label, checked, ok);
|
appendSignatureCheck(row, label, checked, ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function focusPageRegion(region, label) {
|
async function focusPageRegion(region, label, alignment = "nearest") {
|
||||||
const pageIndex = region.page - 1;
|
const pageIndex = region.page - 1;
|
||||||
if (!state.doc || pageIndex < 0 || document.body.hasAttribute("aria-busy")) {
|
if (!state.doc || pageIndex < 0 || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
@@ -8935,11 +9056,11 @@ async function focusPageRegion(region, label) {
|
|||||||
if (openSeq !== state.openSeq || pageIndex !== state.pageIndex) {
|
if (openSeq !== state.openSeq || pageIndex !== state.pageIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
highlightPageRegion(region);
|
highlightPageRegion(region, alignment);
|
||||||
setStatus(`已定位${label}(第${region.page}页)`);
|
setStatus(`已定位${label}(第${region.page}页)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightPageRegion(region) {
|
function highlightPageRegion(region, alignment = "nearest") {
|
||||||
clearRegionHighlights();
|
clearRegionHighlights();
|
||||||
const pageIndex = region.page - 1;
|
const pageIndex = region.page - 1;
|
||||||
const shell = pageShell(pageIndex);
|
const shell = pageShell(pageIndex);
|
||||||
@@ -8955,11 +9076,12 @@ function highlightPageRegion(region) {
|
|||||||
shell.querySelector(".page-surface").append(mark);
|
shell.querySelector(".page-surface").append(mark);
|
||||||
if (state.pageWindow) {
|
if (state.pageWindow) {
|
||||||
const box = mark.getBoundingClientRect(), viewer = el.viewerPanel.getBoundingClientRect();
|
const box = mark.getBoundingClientRect(), viewer = el.viewerPanel.getBoundingClientRect();
|
||||||
if (box.top < viewer.top) scrollViewerBy(box.top - viewer.top);
|
if (alignment === "center") scrollViewerBy(box.top - viewer.top + (box.height - el.viewerPanel.clientHeight) / 2);
|
||||||
|
else if (box.top < viewer.top) scrollViewerBy(box.top - viewer.top);
|
||||||
else if (box.bottom > viewer.top + el.viewerPanel.clientHeight) scrollViewerBy(Math.min(box.top - viewer.top, box.bottom - viewer.top - el.viewerPanel.clientHeight));
|
else if (box.bottom > viewer.top + el.viewerPanel.clientHeight) scrollViewerBy(Math.min(box.top - viewer.top, box.bottom - viewer.top - el.viewerPanel.clientHeight));
|
||||||
if (box.left < viewer.left) el.viewerPanel.scrollLeft += box.left - viewer.left;
|
if (box.left < viewer.left) el.viewerPanel.scrollLeft += box.left - viewer.left;
|
||||||
else if (box.right > viewer.left + el.viewerPanel.clientWidth) el.viewerPanel.scrollLeft += box.right - viewer.left - el.viewerPanel.clientWidth;
|
else if (box.right > viewer.left + el.viewerPanel.clientWidth) el.viewerPanel.scrollLeft += box.right - viewer.left - el.viewerPanel.clientWidth;
|
||||||
} else mark.scrollIntoView({ block: "nearest", inline: "nearest" });
|
} else mark.scrollIntoView({ block: alignment, inline: "nearest" });
|
||||||
window.setTimeout(() => mark.remove(), 1800);
|
window.setTimeout(() => mark.remove(), 1800);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9522,7 +9644,7 @@ function updateEditorTools() {
|
|||||||
const moveDisabled = selectionDisabled || selectedPages.some(index => !pageCan("move", index));
|
const moveDisabled = selectionDisabled || selectedPages.some(index => !pageCan("move", index));
|
||||||
el.movePagePrevButton.disabled = moveDisabled || selectedPages[0] === 0;
|
el.movePagePrevButton.disabled = moveDisabled || selectedPages[0] === 0;
|
||||||
el.movePageNextButton.disabled = moveDisabled || selectedPages.at(-1) === state.doc?.pageCount - 1;
|
el.movePageNextButton.disabled = moveDisabled || selectedPages.at(-1) === state.doc?.pageCount - 1;
|
||||||
const enabled = state.editing && state.selectObjects && !state.panMode && !sealPreview;
|
const enabled = state.editing && state.selectObjects && !state.panMode && !sealPreview && !resourceManager.pick;
|
||||||
canvasEditor.setEnabled(enabled);
|
canvasEditor.setEnabled(enabled);
|
||||||
updateObjectControls(canvasEditor.selected);
|
updateObjectControls(canvasEditor.selected);
|
||||||
el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting;
|
el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting;
|
||||||
@@ -9680,7 +9802,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
updateDocumentControls();
|
updateDocumentControls();
|
||||||
renderSecurity();
|
renderSecurity();
|
||||||
updateDisplayControls();
|
updateDisplayControls();
|
||||||
el.editButton.disabled = busy || Boolean(sealPreview) || !state.doc || !state.ready || state.exporting;
|
el.editButton.disabled = busy || Boolean(sealPreview || resourceManager.pick) || !state.doc || !state.ready || state.exporting;
|
||||||
el.createForm.inert = busy && !state.imageImporting;
|
el.createForm.inert = busy && !state.imageImporting;
|
||||||
updateCreateOptions();
|
updateCreateOptions();
|
||||||
el.insertForm.inert = busy;
|
el.insertForm.inert = busy;
|
||||||
@@ -9703,7 +9825,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
el.outlineList.inert = busy;
|
el.outlineList.inert = busy;
|
||||||
el.navigationTabs.inert = busy;
|
el.navigationTabs.inert = busy;
|
||||||
el.pageList.inert = busy;
|
el.pageList.inert = busy;
|
||||||
el.editorTools.inert = busy || Boolean(sealPreview);
|
el.editorTools.inert = busy || Boolean(sealPreview || resourceManager.pick);
|
||||||
el.fontList.inert = busy;
|
el.fontList.inert = busy;
|
||||||
el.docFontList.inert = busy;
|
el.docFontList.inert = busy;
|
||||||
if (!busy) {
|
if (!busy) {
|
||||||
|
|||||||
+14
-13
@@ -223,6 +223,7 @@ func RunWASM() {
|
|||||||
registerAsyncCallback("ofdgoExportMedia", exportMedia)
|
registerAsyncCallback("ofdgoExportMedia", exportMedia)
|
||||||
registerCallback("ofdgoResources", documentResources)
|
registerCallback("ofdgoResources", documentResources)
|
||||||
registerCallback("ofdgoResourceTargets", resourceTargets)
|
registerCallback("ofdgoResourceTargets", resourceTargets)
|
||||||
|
registerCallback("ofdgoResourceLocations", resourceLocations)
|
||||||
registerCallback("ofdgoChangeResource", changeResource)
|
registerCallback("ofdgoChangeResource", changeResource)
|
||||||
registerAsyncCallback("ofdgoExportResource", exportResource)
|
registerAsyncCallback("ofdgoExportResource", exportResource)
|
||||||
registerCallback("ofdgoImages", documentImages)
|
registerCallback("ofdgoImages", documentImages)
|
||||||
@@ -808,24 +809,24 @@ func documentResources(args []js.Value) (any, error) {
|
|||||||
return currentSession.Reader.Resources(context.Background())
|
return currentSession.Reader.Resources(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
// resourceTargets 列出当前页可由普通对象接口编辑动作的图元
|
// resourceTargets 列出当前页可绑定指定资源的图元及已有绑定状态
|
||||||
// 入参: args 零基页码
|
// 入参: args 零基页码、资源类型和标识
|
||||||
// 返回: any 图元列表, error 页面读取错误
|
// 返回: any 图元列表, error 页面读取错误
|
||||||
func resourceTargets(args []js.Value) (any, error) {
|
func resourceTargets(args []js.Value) (any, error) {
|
||||||
if currentEditor == nil || len(args) != 1 {
|
if currentEditor == nil || len(args) != 3 {
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
}
|
}
|
||||||
items, err := currentSession.Renderer.StructureObjects(args[0].Int())
|
return currentEditor.ResourceTargets(context.Background(), args[1].String(), args[2].String(), args[0].Int())
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
|
// resourceLocations 定位当前文档与版本中的资源引用实例
|
||||||
|
// 入参: args 资源类型、标识和引用索引
|
||||||
|
// 返回: any 页面图元范围, error 引用或度量错误
|
||||||
|
func resourceLocations(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil || len(args) != 3 {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
result := items[:0]
|
return currentSession.Renderer.ResourceLocations(context.Background(), args[0].String(), args[1].String(), args[2].Int())
|
||||||
for _, item := range items {
|
|
||||||
if _, err := currentEditor.Object(args[0].Int(), item.ID); err == nil {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeResource 调用库层资源编辑,浏览器不改写资源声明和包路径
|
// changeResource 调用库层资源编辑,浏览器不改写资源声明和包路径
|
||||||
|
|||||||
+89
-24
@@ -26,6 +26,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResourceTarget 描述当前页可绑定资源动作的图元,Bound表示已有该资源的点击动作
|
||||||
|
type ResourceTarget struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Bound bool `json:"bound"`
|
||||||
|
}
|
||||||
|
|
||||||
// Resources 获取当前编辑快照的资源,覆盖正文、模板、注解、图案和复合图元
|
// Resources 获取当前编辑快照的资源,覆盖正文、模板、注解、图案和复合图元
|
||||||
// 入参: ctx 取消上下文
|
// 入参: ctx 取消上下文
|
||||||
// 返回: []ResourceInfo 资源及引用位置, error 快照或结构错误
|
// 返回: []ResourceInfo 资源及引用位置, error 快照或结构错误
|
||||||
@@ -37,6 +45,46 @@ func (e *Editor) Resources(ctx context.Context) ([]ResourceInfo, error) {
|
|||||||
return reader.Resources(ctx)
|
return reader.Resources(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResourceTargets 列出当前页可独立绑定音视频或附件动作的图元及绑定状态
|
||||||
|
// 模板和共享复合资源的内部成员不作为普通页面图元返回
|
||||||
|
// 入参: ctx 取消上下文, kind 为Audio、Video或Attachment, id 资源标识, page 零基页码
|
||||||
|
// 返回: []ResourceTarget 按绘制顺序排列的候选, error 资源、页面或取消错误
|
||||||
|
func (e *Editor) ResourceTargets(ctx context.Context, kind, id string, page int) ([]ResourceTarget, error) {
|
||||||
|
if kind != "Audio" && kind != "Video" && kind != "Attachment" {
|
||||||
|
return nil, fmt.Errorf("resource does not support actions")
|
||||||
|
}
|
||||||
|
items, err := e.Resources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := findResource(items, kind, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
content, err := e.page(page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var result []ResourceTarget
|
||||||
|
for _, layer := range content.Content.Layer {
|
||||||
|
for _, object := range layer.Objects {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
actions, err := resourceObjectActions(object)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
target := ResourceTarget{ID: editorObjectID(object), Type: object.Type}
|
||||||
|
if object.Type == "TextObject" {
|
||||||
|
target.Text = object.TextObject.Text()
|
||||||
|
}
|
||||||
|
target.Bound = slices.ContainsFunc(actions, func(action Action) bool { return resourceActionMatches(action, kind, id) })
|
||||||
|
result = append(result, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// ImportMedia 导入可独立管理的音视频资源,即使尚未绑定动作也保留在当前文档中
|
// ImportMedia 导入可独立管理的音视频资源,即使尚未绑定动作也保留在当前文档中
|
||||||
// 不解码或转码,格式由调用方提供;修改隔离其他文档及历史版本,支持撤销
|
// 不解码或转码,格式由调用方提供;修改隔离其他文档及历史版本,支持撤销
|
||||||
// 入参: ctx 取消上下文, kind 为Audio或Video, format 媒体格式, data 完整媒体数据
|
// 入参: ctx 取消上下文, kind 为Audio或Video, format 媒体格式, data 完整媒体数据
|
||||||
@@ -459,33 +507,13 @@ func (e *Editor) bindResourceAction(ctx context.Context, kind, id string, page i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var actions []Action
|
actions, err := resourceObjectActions(target)
|
||||||
switch target.Type {
|
if err != nil {
|
||||||
case "TextObject":
|
return err
|
||||||
actions = target.TextObject.Actions
|
|
||||||
case "ImageObject":
|
|
||||||
actions = target.ImageObject.Actions
|
|
||||||
case "Path", "PathObject":
|
|
||||||
actions = target.PathObject.Actions
|
|
||||||
case "CompositeObject", "CompositeGraphicUnit":
|
|
||||||
actions = target.CompositeGraphicUnit.Actions
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("object does not support actions")
|
|
||||||
}
|
}
|
||||||
found := false
|
found := false
|
||||||
actions = slices.DeleteFunc(slices.Clone(actions), func(action Action) bool {
|
actions = slices.DeleteFunc(slices.Clone(actions), func(action Action) bool {
|
||||||
if action.Event != "CLICK" {
|
match := resourceActionMatches(action, kind, id)
|
||||||
return false
|
|
||||||
}
|
|
||||||
match := false
|
|
||||||
switch kind {
|
|
||||||
case "Attachment":
|
|
||||||
match = action.GotoA != nil && action.GotoA.AttachID == id
|
|
||||||
case "Audio":
|
|
||||||
match = action.Sound != nil && sameResourceID(action.Sound.ResourceID, id)
|
|
||||||
case "Video":
|
|
||||||
match = action.Movie != nil && sameResourceID(action.Movie.ResourceID, id) && (action.Movie.Operator == "" || action.Movie.Operator == "Play")
|
|
||||||
}
|
|
||||||
found = found || match
|
found = found || match
|
||||||
return match && !bind
|
return match && !bind
|
||||||
})
|
})
|
||||||
@@ -509,3 +537,40 @@ func (e *Editor) bindResourceAction(ctx context.Context, kind, id string, page i
|
|||||||
}
|
}
|
||||||
return e.SetObjectActions(page, []string{object}, actions)
|
return e.SetObjectActions(page, []string{object}, actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resourceObjectActions 读取支持动作的图元,不复制或修改动作
|
||||||
|
// 入参: object 页面图元
|
||||||
|
// 返回: []Action 原动作, error 不支持动作的图元类型
|
||||||
|
func resourceObjectActions(object GraphicObject) ([]Action, error) {
|
||||||
|
switch object.Type {
|
||||||
|
case "TextObject":
|
||||||
|
return object.TextObject.Actions, nil
|
||||||
|
case "ImageObject":
|
||||||
|
return object.ImageObject.Actions, nil
|
||||||
|
case "Path", "PathObject":
|
||||||
|
return object.PathObject.Actions, nil
|
||||||
|
case "CompositeObject", "CompositeGraphicUnit":
|
||||||
|
return object.CompositeGraphicUnit.Actions, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("object does not support actions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resourceActionMatches 检查资源的点击动作,视频停止或暂停不视为播放绑定
|
||||||
|
// 入参: action 动作, kind 资源类型, id 资源标识
|
||||||
|
// 返回: bool 是否匹配
|
||||||
|
func resourceActionMatches(action Action, kind, id string) bool {
|
||||||
|
if action.Event != "CLICK" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch kind {
|
||||||
|
case "Attachment":
|
||||||
|
return action.GotoA != nil && action.GotoA.AttachID == id
|
||||||
|
case "Audio":
|
||||||
|
return action.Sound != nil && sameResourceID(action.Sound.ResourceID, id)
|
||||||
|
case "Video":
|
||||||
|
return action.Movie != nil && sameResourceID(action.Movie.ResourceID, id) && (action.Movie.Operator == "" || action.Movie.Operator == "Play")
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceLocation 保存资源引用的页面实例范围,Page从1开始,Bounds单位为毫米
|
||||||
|
// Object为承载引用的图元标识;隐藏或完全裁剪的图元不产生定位结果
|
||||||
|
type ResourceLocation struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Bounds Box `json:"bounds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceLocations 定位当前文档及版本内的一处资源引用,返回各页面上的可见使用范围
|
||||||
|
// 模板、图案和复合资源沿引用关系定位承载图元,不将资源声明位置当作页面位置
|
||||||
|
// 入参: ctx 取消上下文, kind 资源类型, id 资源标识, reference 为Resources返回的引用索引
|
||||||
|
// 返回: []ResourceLocation 页面位置,无可见图元时为空, error 引用、度量或取消错误
|
||||||
|
func (r *Renderer) ResourceLocations(ctx context.Context, kind, id string, reference int) ([]ResourceLocation, error) {
|
||||||
|
catalog, err := r.Reader.resourceCatalog(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item, err := findResource(catalog.items, kind, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if reference < 0 || reference >= len(item.References) {
|
||||||
|
return nil, fmt.Errorf("resource reference not found")
|
||||||
|
}
|
||||||
|
ref := item.References[reference]
|
||||||
|
wanted := make(map[string]bool)
|
||||||
|
users := make(map[string][]string)
|
||||||
|
for _, file := range catalog.scan.files {
|
||||||
|
root, err := parseEditorXMLContext(ctx, file.data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var visit func(*editorXML, string) error
|
||||||
|
visit = func(node *editorXML, object string) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !node.matchesOFD(node.name.Local) || node.matchesOFD("Attachments") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject"}, node.name.Local) {
|
||||||
|
object = node.attr("ID")
|
||||||
|
}
|
||||||
|
owner := resourceReferenceOwner(node)
|
||||||
|
add := func(role, value string) {
|
||||||
|
if owner == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if role != "AttachID" {
|
||||||
|
if dependency := editorResourceID(value); dependency != "" && dependency != owner {
|
||||||
|
users[dependency] = append(users[dependency], owner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match := value == item.ID
|
||||||
|
if kind != "Attachment" {
|
||||||
|
match = sameResourceID(value, item.ID)
|
||||||
|
}
|
||||||
|
if file.name == ref.File && object == ref.Object && role == ref.Role && match {
|
||||||
|
wanted[owner] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, attr := range node.attrs {
|
||||||
|
if attr.Name.Space == "" && (editorResourceReferenceKind(attr.Name.Local, root.matchesOFD("Res")) == editorReferenceID || attr.Name.Local == "AttachID") {
|
||||||
|
add(attr.Name.Local, attr.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains([]string{"Thumbnail", "Substitution"}, node.name.Local) {
|
||||||
|
add(node.name.Local, strings.TrimSpace(editorImportText(file.data, node)))
|
||||||
|
}
|
||||||
|
if owner != "" && node.parent != nil && node.parent.matchesOFD("Layer") {
|
||||||
|
layer := editorResourceID(node.parent.attr("ID"))
|
||||||
|
users[layer] = append(users[layer], owner)
|
||||||
|
}
|
||||||
|
for _, child := range node.children {
|
||||||
|
if err := visit(child, object); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := visit(root, ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue := slices.Collect(maps.Keys(wanted))
|
||||||
|
for len(queue) != 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
for _, user := range users[id] {
|
||||||
|
if !wanted[user] {
|
||||||
|
wanted[user] = true
|
||||||
|
queue = append(queue, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc, err := r.Reader.DocContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
view := *r
|
||||||
|
view.RenderAnnotations, view.pageContext = true, ctx
|
||||||
|
var result []ResourceLocation
|
||||||
|
for page := range doc.Pages.Page {
|
||||||
|
if ref.Page >= 0 && ref.Page != page || len(wanted) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content, err := r.Reader.PageContentByIndexContext(ctx, page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targets := make(map[string]*StructureTarget, len(wanted))
|
||||||
|
for id := range wanted {
|
||||||
|
targets[id] = &StructureTarget{ID: id}
|
||||||
|
}
|
||||||
|
visitor := &structureVisitor{ctx: ctx, renderer: &view, targets: targets}
|
||||||
|
if err := view.WalkPage(content, visitor); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, id := range slices.Sorted(maps.Keys(targets)) {
|
||||||
|
box := targets[id].Bounds
|
||||||
|
if box.W > 0 && box.H > 0 {
|
||||||
|
result = append(result, ResourceLocation{Page: page + 1, Object: id, Bounds: box})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resourceReferenceOwner 查找承载引用的资源定义或最外层图元,图层只承接其自身的绘制参数
|
||||||
|
// 入参: node 引用节点
|
||||||
|
// 返回: string 图元或资源标识,无法映射时为空
|
||||||
|
func resourceReferenceOwner(node *editorXML) string {
|
||||||
|
id := ""
|
||||||
|
for current := node; current != nil; current = current.parent {
|
||||||
|
if !current.matchesOFD(current.name.Local) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject", "CompositeGraphicUnit"}, current.name.Local) || current.parent != nil && current.name.Local == editorResourceElement(current.parent.name.Local) {
|
||||||
|
id = editorResourceID(current.attr("ID"))
|
||||||
|
}
|
||||||
|
if id == "" && current.matchesOFD("Layer") {
|
||||||
|
id = editorResourceID(current.attr("ID"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
Reference in new issue
Block a user