feat(编辑能力): 优化水印选择,完善注解编辑
build.yaml / build (push) Successful in 37s

This commit is contained in:
xiaoqidun committed 2026-09-18 02:19:06 +08:00
1 parent ff06840bcd
commit ed11ade4e8
11 files changed
+468 -37

No files matched your search

+6 -2
View File
@@ -265,9 +265,13 @@
</form>
</dialog>
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
<form method="dialog">
<form id="annotationForm" method="dialog">
<div id="annotationDetail" class="annotation-detail"></div>
<div class="form-actions"><button class="button" type="submit" autofocus>关闭</button></div>
<div id="annotationFields" hidden>
<div class="form-row"><label for="annotationRemark">说明</label><textarea id="annotationRemark" rows="4"></textarea></div>
<div class="form-row"><label for="annotationCreator">作者</label><input id="annotationCreator" type="text"></div>
</div>
<div class="form-actions"><div id="annotationStatus" class="form-status" role="status"></div><button id="annotationClose" class="button" type="button" autofocus>关闭</button><button id="annotationSubmit" class="button" type="submit" hidden>确定</button></div>
</form>
</dialog>
<dialog id="pagePanel" class="form-dialog" aria-label="页面设置">
+8 -1
View File
@@ -798,7 +798,8 @@ button:not(:disabled):hover {
gap: 8px;
}
.form-actions > button:only-child {
.form-actions > button:only-child,
.form-actions > button:has(+ button[hidden]) {
grid-column: -2;
}
@@ -1297,6 +1298,8 @@ button.outline-link:hover {
.navigation-tabs[hidden],
.page-selection-tools[hidden],
.form-row[hidden],
.form-actions > [hidden],
.annotation-detail[hidden],
.tool-group[hidden],
.page-list[hidden],
.page-frame[hidden],
@@ -1513,6 +1516,10 @@ button.outline-link:hover {
cursor: default;
}
.edit-object.edit-background:not(.selected):not(.erasing) {
outline: none;
}
.edit-object.edit-contour,
.edit-object.edit-contour.selected,
.edit-object.edit-contour.erasing {
+61 -6
View File
@@ -27,6 +27,7 @@ let textMeasure = null;
const state = {
composite: null,
annotationEdit: null,
ready: false,
wasmExited: false,
wasmSeq: 0,
@@ -334,6 +335,13 @@ const el = {
signatureSummary: document.querySelector("#signatureSummary"),
signatureList: document.querySelector("#signatureList"),
annotationNote: document.querySelector("#annotationNote"),
annotationForm: document.querySelector("#annotationForm"),
annotationFields: document.querySelector("#annotationFields"),
annotationRemark: document.querySelector("#annotationRemark"),
annotationCreator: document.querySelector("#annotationCreator"),
annotationStatus: document.querySelector("#annotationStatus"),
annotationClose: document.querySelector("#annotationClose"),
annotationSubmit: document.querySelector("#annotationSubmit"),
annotationDetail: document.querySelector("#annotationDetail"),
docFontList: document.querySelector("#docFontList"),
docFontSummary: document.querySelector("#docFontSummary"),
@@ -363,7 +371,9 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
setStatus(reason || pageStatus(state.pageIndex, state.doc.pageCount));
}
},
onTransform: (item, change) => changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item,
onTransform: (item, change) => (item.items || [item]).every(object => object.type === "Annotation")
? changeAnnotations(item, "move", change.x, change.y)
: changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item,
change.x + item.x * (1 - change.scale), change.y + item.y * (1 - change.scale), change.scale),
onNudgeChange: () => {
updatePendingChanges();
@@ -379,8 +389,13 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
syncSelection();
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
},
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
onDelete: (item) => (item.items || [item]).every(object => object.type === "Annotation")
? changeAnnotations(item, "delete") : changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
onErase: (items, box, points) => {
if (items.some(item => item.type === "Annotation")) {
if (box || !items.every(item => item.type === "Annotation")) { setStatus(box ? "注解暂不支持局部擦除" : "注解与正文需分别操作"); return false; }
return changeAnnotations({ items, index: items[0].index, id: items.map(item => item.id) }, "delete");
}
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
const item = { index: items[0].index, id: items.map(item => item.id), scoped: items.every(item => item.scoped) };
@@ -480,6 +495,16 @@ el.infoForm.addEventListener("submit", async event => {
event.preventDefault();
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value)) el.infoPanel.close();
});
el.annotationClose.addEventListener("click", () => el.annotationNote.close());
el.annotationNote.addEventListener("close", () => { state.annotationEdit = null; });
el.annotationForm.addEventListener("submit", async event => {
if (!state.annotationEdit) return;
event.preventDefault();
if (await changeAnnotations(state.annotationEdit, "update", el.annotationRemark.value, el.annotationCreator.value)) {
el.annotationNote.close();
canvasEditor.focus();
}
});
el.paragraphForm.addEventListener("submit", async event => {
event.preventDefault();
if (document.body.hasAttribute("aria-busy") || !el.paragraphForm.reportValidity()) return;
@@ -1370,6 +1395,18 @@ async function createDocument(event) {
async function editCanvasObject(item) {
if (!state.editing || document.body.hasAttribute("aria-busy") || canvasEditor.input || item.items) return;
if (item.type === "Annotation") {
state.annotationEdit = item;
el.annotationRemark.value = item.remark || "";
el.annotationCreator.value = item.creator || "";
el.annotationFields.hidden = el.annotationSubmit.hidden = false;
el.annotationDetail.hidden = true;
el.annotationStatus.textContent = "";
el.annotationClose.textContent = "取消";
el.annotationNote.showModal();
el.annotationRemark.focus();
return;
}
if (canEditObject(item, "enter")) return enterCompositeScope(item.index, item.id);
if (item.type === "TextObject") {
if (!confirmTextReflow(item)) return;
@@ -1403,7 +1440,17 @@ async function editCanvasObject(item) {
function mountEditorObjects(index, page, surface) {
const scope = state.composite?.index === index ? state.composite : null;
canvasEditor.mount(index, scope ? { ...page, objects: scope.objects } : page, surface);
const annotations = state.renderAnnotations ? (page.annotations || []).filter(annotation => annotation.visible).map(annotation => ({
...annotation, id: `annotation:${annotation.id}`, type: "Annotation", width: annotation.width || 6, height: annotation.height || 6,
background: annotation.type === "Watermark" || annotation.x <= 0.1 && annotation.y <= 0.1
&& annotation.x + annotation.width >= page.width - 0.1 && annotation.y + annotation.height >= page.height - 0.1,
capabilities: { update: true, move: true, delete: true },
})) : [];
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : [...page.objects, ...annotations] }, surface);
}
function changeAnnotations(item, action, ...args) {
return changeDocument("ofdgoChangeAnnotations", { ...item, id: item.items ? item.id : [item.id] }, action, ...args);
}
function resetCompositeScope() {
@@ -1953,7 +2000,8 @@ async function changeDocument(name, item, ...args) {
rememberEditorView(before, revision);
return true;
}
const clearSelection = !item || ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
const clearSelection = !item || name === "ofdgoChangeAnnotations" && args[0] === "delete"
|| ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoGroupObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
state.selectObjects = true;
canvasEditor.setTool("");
@@ -1981,7 +2029,9 @@ async function changeDocument(name, item, ...args) {
}
} catch (err) {
if (openSeq === state.openSeq) {
if (el.batchPagesPanel.open) {
if (el.annotationNote.open) {
el.annotationStatus.textContent = err.message;
} else if (el.batchPagesPanel.open) {
el.batchPagesStatus.textContent = err.message;
} else if (el.objectStylePanel.open) {
el.objectStyleStatus.textContent = err.message;
@@ -5270,6 +5320,10 @@ function mountAnnotationNotes(index, surface, annotations) {
note.addEventListener("click", () => {
if (state.editing || document.body.hasAttribute("aria-busy")) return;
el.annotationDetail.replaceChildren();
el.annotationFields.hidden = el.annotationSubmit.hidden = true;
el.annotationDetail.hidden = false;
el.annotationStatus.textContent = "";
el.annotationClose.textContent = "关闭";
const content = document.createElement("div");
content.textContent = annotation.remark;
el.annotationDetail.append(content);
@@ -5746,7 +5800,7 @@ function updateObjectControls(item, reset = false) {
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
el.editObjectButton.title = canEditObject(item, "enter") ? "进入组合" : item?.type === "ImageObject" ? "替换图片" : "修改对象";
el.editObjectButton.title = canEditObject(item, "enter") ? "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
const selection = selectedText(item);
@@ -5866,6 +5920,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
el.pageForm.inert = busy;
el.paragraphForm.inert = busy;
el.infoForm.inert = busy;
el.annotationForm.inert = busy;
el.importForm.inert = busy && !state.importing;
for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy;
el.importPages.disabled = busy || el.importRange.value !== "custom";
+19 -8
View File
@@ -176,7 +176,10 @@ function paintShape(node, shape, box) {
}
export function canEditObject(item, capability) {
return Boolean(item) && (item.items || [item]).every(object => Boolean(object.capabilities?.[capability]));
if (!item) return false;
const objects = item.items || [item];
if (objects.some(object => object.type === "Annotation") && objects.some(object => object.type !== "Annotation")) return false;
return objects.every(object => Boolean(object.capabilities?.[capability] || capability === "move" && object.capabilities?.transform));
}
export function missingGlyphMessage(diagnostic, action = "请更换字体") {
@@ -188,6 +191,7 @@ export function missingGlyphMessage(diagnostic, action = "请更换字体") {
}
export function objectEditReason(item) {
if (item?.items?.some(object => object.type === "Annotation") && item.items.some(object => object.type !== "Annotation")) return "注解与正文需分别操作";
const labels = { fontUnavailable: "字体不可用", unsupportedColor: "暂不支持此颜色", unsupportedStyle: "暂不支持此样式",
unsupportedContainer: "暂不支持此对象结构", invalidObject: "对象数据异常" };
const reasons = (item?.items || (item ? [item] : [])).map(object => {
@@ -278,18 +282,20 @@ export class CanvasEditor {
const layer = document.createElement("div");
layer.className = "edit-layer";
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object], [data-ofd-child]")].map((node, order) => [node.getAttribute("data-ofd-object") || node.getAttribute("data-ofd-child"), { node, order }]));
const objects = [...page.objects].sort((a, b) => (artwork.get(a.id)?.order ?? 0) - (artwork.get(b.id)?.order ?? 0));
const objects = [...page.objects].sort((a, b) => Number(Boolean(b.background)) - Number(Boolean(a.background))
|| (artwork.get(a.id)?.order ?? 0) - (artwork.get(b.id)?.order ?? 0));
for (const object of objects) {
const node = document.createElement("div");
node.className = "edit-object";
node.classList.toggle("read-only", !canEditObject(object, "transform"));
node.classList.toggle("edit-background", Boolean(object.background));
node.classList.toggle("read-only", !canEditObject(object, "move"));
node.title = objectEditReason(object);
if (lineShape(object.shape)) {
node.classList.add("edit-line");
}
node.tabIndex = 0;
node.setAttribute("role", "button");
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象", CompositeObject: "复合对象", CompositeGraphicUnit: "复合对象" }[object.type]);
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象", CompositeObject: "复合对象", CompositeGraphicUnit: "复合对象", Annotation: "注解对象" }[object.type]);
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
item.artwork = artwork.get(object.id)?.node;
if (object.type === "PathObject" && (object.shape || object.outline) || (object.scoped || object.type === "CompositeObject" || object.type === "CompositeGraphicUnit") && object.contours?.length) {
@@ -527,8 +533,9 @@ export class CanvasEditor {
node = target?.node;
this.focus();
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
if (!target) {
if (!target || target.background && !this.items().includes(target)) {
this.startMarquee(event, additive);
if (this.drag?.marquee) this.drag.target = target;
return;
}
if (additive && !event.target.dataset.corner) {
@@ -540,7 +547,7 @@ export class CanvasEditor {
return;
}
const item = this.selected;
if (!canEditObject(item, "transform")) return;
if (!canEditObject(item, "move")) return;
this.drag = {
item, pointerID: event.pointerId, corner: !item.items || target === item ? resizeCorner(node, event) : "",
rect: item.surface.getBoundingClientRect(), rotation: this.options.rotation(),
@@ -739,6 +746,7 @@ export class CanvasEditor {
this.drag = null;
drag.preview.remove();
this.viewer.releasePointerCapture(drag.pointerID);
if (drag.target && !drag.moved) this.setSelection([...drag.base, drag.target]);
return;
}
if (drag.shape) {
@@ -810,7 +818,8 @@ export class CanvasEditor {
if (!page) return;
event.preventDefault();
this.focus();
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)),
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node))
.filter(item => this.tool === "erase-object" || !item.background),
erased: new Set(), shape: "rectangle", pointerID: event.pointerId, clientX: event.clientX, clientY: event.clientY,
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
@@ -866,9 +875,11 @@ export class CanvasEditor {
const box = { x: Math.min(from.x, to.x), y: Math.min(from.y, to.y), width: Math.abs(to.x-from.x), height: Math.abs(to.y-from.y) };
paintShape(drag.node, "rectangle", box);
if (Math.hypot(event.clientX-drag.clientX, event.clientY-drag.clientY) < 3) return;
drag.moved = true;
const items = [...drag.base];
for (const node of drag.surface.querySelectorAll(".edit-object")) {
const item = this.nodes.get(node);
if (item.background && (item.x < box.x || item.y < box.y || item.x + item.width > box.x + box.width || item.y + item.height > box.y + box.height)) continue;
if (!items.includes(item) && item.x <= box.x+box.width && item.x+item.width >= box.x && item.y <= box.y+box.height && item.y+item.height >= box.y) {
items.push(item);
}
@@ -1030,7 +1041,7 @@ export class CanvasEditor {
} else if (edit && !this.drag && !this.selected.items) {
if (this.nudge) this.commitNudge().then(saved => { if (saved && this.selected) this.options.onEdit(this.selected); });
else this.options.onEdit(this.selected);
} else if (!this.drag && directions[event.key] && canEditObject(this.selected, "transform")) {
} else if (!this.drag && directions[event.key] && canEditObject(this.selected, "move")) {
const [x, y] = directions[event.key];
const [dx, dy] = [[x, y], [y, -x], [-x, -y], [-y, x]][this.options.rotation() / 90];
const step = event.shiftKey ? 10 : 1;