mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
7f361a79cf
commit
237487b53c
5 files changed
+197
-64
No files matched your search
@@ -43,6 +43,7 @@
|
|||||||
<button id="annotationButton" class="button" type="button" data-short="注" title="关闭注解" aria-label="关闭注解" aria-pressed="true">注解</button>
|
<button id="annotationButton" class="button" type="button" data-short="注" title="关闭注解" aria-label="关闭注解" aria-pressed="true">注解</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tool-group">
|
<div class="tool-group">
|
||||||
|
<button id="editButton" class="button" type="button" data-short="编" title="编辑文档" aria-label="编辑文档" aria-pressed="false" disabled>编辑</button>
|
||||||
<select id="exportFormat" class="format-select" title="导出格式" aria-label="导出格式" disabled></select>
|
<select id="exportFormat" class="format-select" title="导出格式" aria-label="导出格式" disabled></select>
|
||||||
<select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled>
|
<select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled>
|
||||||
<option value="96">96</option>
|
<option value="96">96</option>
|
||||||
@@ -54,9 +55,9 @@
|
|||||||
<button id="exportButton" class="button" type="button" data-short="多" title="导出文档" aria-label="导出文档" aria-haspopup="dialog" aria-controls="exportPanel" disabled>文档</button>
|
<button id="exportButton" class="button" type="button" data-short="多" title="导出文档" aria-label="导出文档" aria-haspopup="dialog" aria-controls="exportPanel" disabled>文档</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div id="editorTools" class="editor-toolbar" role="toolbar" aria-label="创作工具" hidden>
|
<div id="editorTools" class="editor-toolbar" role="toolbar" aria-label="编辑工具" hidden>
|
||||||
<div class="editor-group editor-file">
|
<div class="editor-group editor-file">
|
||||||
<button id="saveButton" class="button" type="button" aria-label="保存OFD">保存</button>
|
<button id="saveButton" class="button" type="button" title="另存为OFD" aria-label="保存OFD" aria-keyshortcuts="Control+S Meta+S">保存</button>
|
||||||
<button id="undoButton" class="button" type="button" disabled>撤销</button>
|
<button id="undoButton" class="button" type="button" disabled>撤销</button>
|
||||||
<button id="redoButton" class="button" type="button" disabled>重做</button>
|
<button id="redoButton" class="button" type="button" disabled>重做</button>
|
||||||
<span>文件</span>
|
<span>文件</span>
|
||||||
@@ -121,6 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="editor-group editor-paragraph">
|
<div class="editor-group editor-paragraph">
|
||||||
<select id="textAlign" aria-label="段落对齐" title="段落对齐" disabled>
|
<select id="textAlign" aria-label="段落对齐" title="段落对齐" disabled>
|
||||||
|
<option value="" hidden>原文</option>
|
||||||
<option value="left">左齐</option>
|
<option value="left">左齐</option>
|
||||||
<option value="center">居中</option>
|
<option value="center">居中</option>
|
||||||
<option value="right">右齐</option>
|
<option value="right">右齐</option>
|
||||||
@@ -293,6 +295,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="panel-title">文档</div>
|
<div class="panel-title">文档</div>
|
||||||
|
<div id="editNotice" class="font-permission-hint" role="status" hidden></div>
|
||||||
<dl class="meta-list">
|
<dl class="meta-list">
|
||||||
<div>
|
<div>
|
||||||
<dt>文件</dt>
|
<dt>文件</dt>
|
||||||
|
|||||||
+171
-50
@@ -1,4 +1,4 @@
|
|||||||
import { CanvasEditor } from "./ofdgo_edit.js";
|
import { CanvasEditor, canEditObject } from "./ofdgo_edit.js";
|
||||||
import { FontManager } from "./ofdgo_font.js";
|
import { FontManager } from "./ofdgo_font.js";
|
||||||
|
|
||||||
const MM_TO_PX = 96 / 25.4;
|
const MM_TO_PX = 96 / 25.4;
|
||||||
@@ -52,6 +52,7 @@ const state = {
|
|||||||
searchIndex: -1,
|
searchIndex: -1,
|
||||||
navigationScroll: new Map(),
|
navigationScroll: new Map(),
|
||||||
fontSyncPending: false,
|
fontSyncPending: false,
|
||||||
|
fontRenderPending: false,
|
||||||
fontSyncing: false,
|
fontSyncing: false,
|
||||||
doc: null,
|
doc: null,
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
@@ -87,6 +88,8 @@ const el = {
|
|||||||
ofdInput: document.querySelector("#ofdInput"),
|
ofdInput: document.querySelector("#ofdInput"),
|
||||||
ofdButton: document.querySelector("#ofdButton"),
|
ofdButton: document.querySelector("#ofdButton"),
|
||||||
newButton: document.querySelector("#newButton"),
|
newButton: document.querySelector("#newButton"),
|
||||||
|
editButton: document.querySelector("#editButton"),
|
||||||
|
editNotice: document.querySelector("#editNotice"),
|
||||||
editorTools: document.querySelector("#editorTools"),
|
editorTools: document.querySelector("#editorTools"),
|
||||||
selectObjectButton: document.querySelector("#selectObjectButton"),
|
selectObjectButton: document.querySelector("#selectObjectButton"),
|
||||||
deleteObjectButton: document.querySelector("#deleteObjectButton"),
|
deleteObjectButton: document.querySelector("#deleteObjectButton"),
|
||||||
@@ -246,6 +249,7 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
fontControls: [el.textFont, el.textFontAdd],
|
fontControls: [el.textFont, el.textFontAdd],
|
||||||
busy: () => document.body.hasAttribute("aria-busy"),
|
busy: () => document.body.hasAttribute("aria-busy"),
|
||||||
rotation: () => state.rotation,
|
rotation: () => state.rotation,
|
||||||
|
canInsert: (index) => pageCan("insert", index),
|
||||||
onSelect: updateObjectControls,
|
onSelect: updateObjectControls,
|
||||||
onTransform: (item, change) => changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item,
|
onTransform: (item, change) => changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item,
|
||||||
change.x + item.x * (1 - change.scale), change.y + item.y * (1 - change.scale), change.scale),
|
change.x + item.x * (1 - change.scale), change.y + item.y * (1 - change.scale), change.scale),
|
||||||
@@ -256,6 +260,7 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
updatePendingChanges();
|
updatePendingChanges();
|
||||||
updateObjectControls(canvasEditor.selected);
|
updateObjectControls(canvasEditor.selected);
|
||||||
syncSelection();
|
syncSelection();
|
||||||
|
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||||
},
|
},
|
||||||
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
||||||
onEdit: editCanvasObject,
|
onEdit: editCanvasObject,
|
||||||
@@ -273,13 +278,14 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
if (!editing) {
|
if (!editing) {
|
||||||
updateTextFonts(canvasEditor.selected, true);
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
syncSelection();
|
syncSelection();
|
||||||
|
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function updatePendingChanges() {
|
function updatePendingChanges() {
|
||||||
const editing = canvasEditor.input;
|
const editing = canvasEditor.input;
|
||||||
setDirty(state.editing && (state.editorInfo?.revision !== state.savedRevision
|
setDirty(Boolean(state.editorInfo) && (state.editorInfo.revision !== state.savedRevision
|
||||||
|| Boolean(editing && (editing.input.value !== editing.item.text || editing.fontData)) || canvasEditor.cropChanged()));
|
|| Boolean(editing && (editing.input.value !== editing.item.text || editing.fontData)) || canvasEditor.cropChanged()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +458,10 @@ el.pageForm.addEventListener("submit", async (event) => {
|
|||||||
});
|
});
|
||||||
el.ofdButton.addEventListener("click", openOFDFile);
|
el.ofdButton.addEventListener("click", openOFDFile);
|
||||||
el.newButton.addEventListener("click", openCreatePanel);
|
el.newButton.addEventListener("click", openCreatePanel);
|
||||||
|
el.editButton.addEventListener("click", toggleEditor);
|
||||||
|
el.editButton.addEventListener("pointerdown", (event) => {
|
||||||
|
if (canvasEditor.input) event.preventDefault();
|
||||||
|
});
|
||||||
el.createCancel.addEventListener("click", () => el.createPanel.close());
|
el.createCancel.addEventListener("click", () => el.createPanel.close());
|
||||||
el.createForm.addEventListener("submit", createDocument);
|
el.createForm.addEventListener("submit", createDocument);
|
||||||
editorClick(el.insertTextButton, toggleTextTool);
|
editorClick(el.insertTextButton, toggleTextTool);
|
||||||
@@ -588,6 +598,11 @@ function handleKeyDown(event) {
|
|||||||
}
|
}
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
|
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "s" && state.editorInfo) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!document.body.hasAttribute("aria-busy") && !formDialogOpen()) exportFile(true, null, "ofd");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "a" && state.doc
|
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "a" && state.doc
|
||||||
&& el.viewerPanel.contains(target) && !target.closest("input, textarea, select, [contenteditable]")) {
|
&& el.viewerPanel.contains(target) && !target.closest("input, textarea, select, [contenteditable]")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -716,10 +731,62 @@ function discardChanges() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setEditorInfo(doc) {
|
function setEditorInfo(doc) {
|
||||||
state.editorInfo = { revision: doc.revision, canUndo: doc.canUndo, canRedo: doc.canRedo };
|
state.editorInfo = { revision: doc.revision, canUndo: doc.canUndo, canRedo: doc.canRedo,
|
||||||
|
pageCapabilities: doc.pageCapabilities, editWarnings: doc.editWarnings || [] };
|
||||||
setDirty(doc.revision !== state.savedRevision);
|
setDirty(doc.revision !== state.savedRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pageCan(capability, index = state.pageIndex) {
|
||||||
|
return Boolean(state.editorInfo?.pageCapabilities?.[index]?.[capability]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleEditor() {
|
||||||
|
if (!state.doc || !state.ready || document.body.hasAttribute("aria-busy") || formDialogOpen()) return;
|
||||||
|
if (state.editorInfo) {
|
||||||
|
if (!await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
|
||||||
|
state.editing = !state.editing;
|
||||||
|
canvasEditor.clear();
|
||||||
|
canvasEditor.setTool("");
|
||||||
|
if (state.editing) {
|
||||||
|
state.selectObjects = true;
|
||||||
|
setPan(false);
|
||||||
|
}
|
||||||
|
updateControls();
|
||||||
|
applyFit(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { pageIndex, fitMode, scale } = state;
|
||||||
|
const { scrollLeft, scrollTop } = el.viewerPanel;
|
||||||
|
const openSeq = ++state.openSeq;
|
||||||
|
setBusy(true, "正在准备编辑", null, "正在准备编辑");
|
||||||
|
try {
|
||||||
|
const doc = await callWASM("ofdgoEditDocument");
|
||||||
|
if (openSeq !== state.openSeq) return;
|
||||||
|
state.savedRevision = doc.revision;
|
||||||
|
setEditorInfo(doc);
|
||||||
|
state.editing = true;
|
||||||
|
state.selectObjects = true;
|
||||||
|
state.ofdBytes = null;
|
||||||
|
state.objectClipboard = null;
|
||||||
|
canvasEditor.clear();
|
||||||
|
setPan(false);
|
||||||
|
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode, scale,
|
||||||
|
keepPreview: true, previewScroll: { scrollLeft, scrollTop } });
|
||||||
|
} catch (err) {
|
||||||
|
if (openSeq === state.openSeq) showError(err, false);
|
||||||
|
} finally {
|
||||||
|
if (openSeq === state.openSeq) {
|
||||||
|
setBusy(false);
|
||||||
|
updateControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmTextReflow(item) {
|
||||||
|
return canEditObject(item, "reflow") && (canEditObject(item, "layoutKnown")
|
||||||
|
|| window.confirm("原文排版未知,此操作将重新排版。继续?"));
|
||||||
|
}
|
||||||
|
|
||||||
function openCreatePanel() {
|
function openCreatePanel() {
|
||||||
if (document.body.hasAttribute("aria-busy")) {
|
if (document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
@@ -748,6 +815,7 @@ async function createDocument(event) {
|
|||||||
}
|
}
|
||||||
state.editing = true;
|
state.editing = true;
|
||||||
state.objectClipboard = null;
|
state.objectClipboard = null;
|
||||||
|
state.fontRenderPending = false;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
delete state.textDefaults.fontChoice;
|
delete state.textDefaults.fontChoice;
|
||||||
updateTextFonts(null, true);
|
updateTextFonts(null, true);
|
||||||
@@ -772,6 +840,10 @@ async function createDocument(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function editCanvasObject(item) {
|
async function editCanvasObject(item) {
|
||||||
|
if (!state.editing || document.body.hasAttribute("aria-busy") || canvasEditor.input || item.items) return;
|
||||||
|
if (item.type === "TextObject") {
|
||||||
|
if (!confirmTextReflow(item)) return;
|
||||||
|
} else if (!canEditObject(item, "update")) return;
|
||||||
if (item.type === "PathObject") {
|
if (item.type === "PathObject") {
|
||||||
el.shapeWidth.focus();
|
el.shapeWidth.focus();
|
||||||
return;
|
return;
|
||||||
@@ -779,9 +851,6 @@ async function editCanvasObject(item) {
|
|||||||
if (item.type === "ImageObject") {
|
if (item.type === "ImageObject") {
|
||||||
return openInsertPanel(item);
|
return openInsertPanel(item);
|
||||||
}
|
}
|
||||||
if (!state.editing || document.body.hasAttribute("aria-busy") || canvasEditor.input) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
@@ -854,7 +923,7 @@ function currentTextStyle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleTextTool() {
|
async function toggleTextTool() {
|
||||||
if (!state.editing || document.body.hasAttribute("aria-busy")) return;
|
if (!state.editing || !pageCan("insert") || document.body.hasAttribute("aria-busy")) return;
|
||||||
if (canvasEditor.tool === "text") {
|
if (canvasEditor.tool === "text") {
|
||||||
canvasEditor.setTool("");
|
canvasEditor.setTool("");
|
||||||
return;
|
return;
|
||||||
@@ -865,10 +934,11 @@ async function toggleTextTool() {
|
|||||||
updateTextFonts(canvasEditor.selected, true);
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
}
|
}
|
||||||
if (openSeq !== state.openSeq) return;
|
if (openSeq !== state.openSeq) return;
|
||||||
const style = currentTextStyle();
|
const style = canEditObject(canvasEditor.selected, "layoutKnown") ? currentTextStyle() : state.textDefaults;
|
||||||
|
const font = state.textFonts[Number(el.textFont.value)];
|
||||||
state.textDefaults = { type: "TextObject", size: style.size, color: style.color, wrap: style.wrap,
|
state.textDefaults = { type: "TextObject", size: style.size, color: style.color, wrap: style.wrap,
|
||||||
align: style.align || "left", paragraphHeight: style.paragraphHeight || 0, letterSpacing: style.letterSpacing || 0,
|
align: style.align || "left", paragraphHeight: style.paragraphHeight || 0, letterSpacing: style.letterSpacing || 0,
|
||||||
fontChoice: state.textFonts[Number(el.textFont.value)] || state.textDefaults.fontChoice };
|
fontChoice: font && !font.disabled ? font : state.textDefaults.fontChoice };
|
||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
setPan(false);
|
setPan(false);
|
||||||
canvasEditor.setTool("text");
|
canvasEditor.setTool("text");
|
||||||
@@ -877,7 +947,7 @@ async function toggleTextTool() {
|
|||||||
|
|
||||||
async function beginCanvasText(index, box, surface, page) {
|
async function beginCanvasText(index, box, surface, page) {
|
||||||
const font = state.textDefaults.fontChoice || state.textFonts[Number(el.textFont.value)];
|
const font = state.textDefaults.fontChoice || state.textFonts[Number(el.textFont.value)];
|
||||||
if (!font) {
|
if (!font || font.disabled) {
|
||||||
setStatus("尚未添加字体");
|
setStatus("尚未添加字体");
|
||||||
el.textFontAdd.focus();
|
el.textFontAdd.focus();
|
||||||
return;
|
return;
|
||||||
@@ -902,7 +972,7 @@ async function beginCanvasText(index, box, surface, page) {
|
|||||||
function editorFonts(item) {
|
function editorFonts(item) {
|
||||||
const fonts = [...fontManager.userFonts.filter((font) => font.enabled), ...fontManager.catalog];
|
const fonts = [...fontManager.userFonts.filter((font) => font.enabled), ...fontManager.catalog];
|
||||||
if (item) {
|
if (item) {
|
||||||
fonts.unshift({ id: `embedded:${item.font}`, name: item.fontName, embedded: true });
|
fonts.unshift({ id: `embedded:${item.font}`, name: item.fontName || item.font, embedded: true, disabled: !canEditObject(item, "update") });
|
||||||
}
|
}
|
||||||
return fonts;
|
return fonts;
|
||||||
}
|
}
|
||||||
@@ -914,6 +984,7 @@ function setFontOptions(select, fonts, selected) {
|
|||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = String(index);
|
option.value = String(index);
|
||||||
option.textContent = font.fullName || font.name;
|
option.textContent = font.fullName || font.name;
|
||||||
|
option.disabled = Boolean(font.disabled);
|
||||||
select.append(option);
|
select.append(option);
|
||||||
});
|
});
|
||||||
select.value = fonts.length ? String(Math.max(0, fonts.findIndex(font => key(font) === key(selected)))) : "";
|
select.value = fonts.length ? String(Math.max(0, fonts.findIndex(font => key(font) === key(selected)))) : "";
|
||||||
@@ -937,7 +1008,7 @@ function updateTextFonts(item, refresh = false) {
|
|||||||
async function changeTextFont() {
|
async function changeTextFont() {
|
||||||
const item = canvasEditor.selected;
|
const item = canvasEditor.selected;
|
||||||
const font = state.textFonts[Number(el.textFont.value)];
|
const font = state.textFonts[Number(el.textFont.value)];
|
||||||
if (!state.ready || document.body.hasAttribute("aria-busy") || !font) return;
|
if (!state.ready || document.body.hasAttribute("aria-busy") || !font || font.disabled) return;
|
||||||
const editing = canvasEditor.input;
|
const editing = canvasEditor.input;
|
||||||
if (editing) {
|
if (editing) {
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
@@ -959,8 +1030,8 @@ async function changeTextFont() {
|
|||||||
if (openSeq === state.openSeq) setBusy(false);
|
if (openSeq === state.openSeq) setBusy(false);
|
||||||
}
|
}
|
||||||
} else if (item?.type === "TextObject") {
|
} else if (item?.type === "TextObject") {
|
||||||
if (!font.embedded) {
|
if (!font.embedded && canEditObject(item, "replaceFont")) {
|
||||||
await changeDocument("ofdgoUpdateText", item, item.text, fontManager.read(font), item.size, null);
|
await changeDocument("ofdgoUpdateText", item, canEditObject(item, "layoutKnown") ? item.text : null, fontManager.read(font), item.size, null);
|
||||||
updateTextFonts(canvasEditor.selected, true);
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1045,6 +1116,10 @@ async function changeParagraph(wrap = currentTextStyle().wrap) {
|
|||||||
updateObjectControls(canvasEditor.selected, true);
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!confirmTextReflow(item)) {
|
||||||
|
updateObjectControls(item, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await changeDocument("ofdgoLayoutText", item, null, null, Boolean(wrap), el.textAlign.value, height, spacing);
|
await changeDocument("ofdgoLayoutText", item, null, null, Boolean(wrap), el.textAlign.value, height, spacing);
|
||||||
updateObjectControls(canvasEditor.selected, true);
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
}
|
}
|
||||||
@@ -1068,6 +1143,10 @@ async function changeTextStyle(color) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (size !== item.size || fill !== null) {
|
if (size !== item.size || fill !== null) {
|
||||||
|
if (color ? !canEditObject(item, "update") : !confirmTextReflow(item)) {
|
||||||
|
updateObjectControls(item, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await changeDocument("ofdgoUpdateText", item, color ? null : item.text, null, size, fill);
|
await changeDocument("ofdgoUpdateText", item, color ? null : item.text, null, size, fill);
|
||||||
}
|
}
|
||||||
updateObjectControls(canvasEditor.selected, true);
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
@@ -1114,13 +1193,15 @@ function updateDrawingControls() {
|
|||||||
}
|
}
|
||||||
const disabled = !state.editing || !state.ready || state.exporting;
|
const disabled = !state.editing || !state.ready || state.exporting;
|
||||||
for (const [button, name] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"]]) {
|
for (const [button, name] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"]]) {
|
||||||
button.disabled = disabled;
|
button.disabled = disabled || !pageCan("insert");
|
||||||
button.setAttribute("aria-pressed", String(tool === name));
|
button.setAttribute("aria-pressed", String(tool === name));
|
||||||
}
|
}
|
||||||
el.selectObjectButton.setAttribute("aria-pressed", String(canvasEditor.enabled && !tool));
|
el.selectObjectButton.setAttribute("aria-pressed", String(canvasEditor.enabled && !tool));
|
||||||
el.shapeFill.disabled = el.shapeStroke.disabled = disabled || line;
|
const styleDisabled = disabled || (canvasEditor.selected?.type === "PathObject"
|
||||||
el.shapeFillColor.disabled = disabled || line || !el.shapeFill.checked;
|
? !canEditObject(canvasEditor.selected, "update") : !pageCan("insert"));
|
||||||
el.shapeStrokeColor.disabled = el.shapeWidth.disabled = disabled || !line && !el.shapeStroke.checked;
|
el.shapeFill.disabled = el.shapeStroke.disabled = styleDisabled || line;
|
||||||
|
el.shapeFillColor.disabled = styleDisabled || line || !el.shapeFill.checked;
|
||||||
|
el.shapeStrokeColor.disabled = el.shapeWidth.disabled = styleDisabled || !line && !el.shapeStroke.checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeDocument(name, item, ...args) {
|
async function changeDocument(name, item, ...args) {
|
||||||
@@ -1612,7 +1693,7 @@ function markWASMExited(wasmSeq = state.wasmSeq, err) {
|
|||||||
}
|
}
|
||||||
wasmRequests.clear();
|
wasmRequests.clear();
|
||||||
if (err) {
|
if (err) {
|
||||||
setStatus(state.editing ? "引擎中断,未保存内容无法恢复" : "引擎运行中断");
|
setStatus(state.editorInfo ? "引擎中断,未保存内容无法恢复" : "引擎运行中断");
|
||||||
}
|
}
|
||||||
updateControls();
|
updateControls();
|
||||||
scheduleWASMRecovery();
|
scheduleWASMRecovery();
|
||||||
@@ -1679,6 +1760,7 @@ async function openOFD(file) {
|
|||||||
state.editing = false;
|
state.editing = false;
|
||||||
state.objectClipboard = null;
|
state.objectClipboard = null;
|
||||||
state.editorInfo = null;
|
state.editorInfo = null;
|
||||||
|
state.fontRenderPending = false;
|
||||||
state.savedRevision = null;
|
state.savedRevision = null;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
@@ -1749,7 +1831,7 @@ async function loadLocalFonts() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await loadDocumentLocalFonts(available)) {
|
if (await loadDocumentLocalFonts(available)) {
|
||||||
await applyFontChange({ skipAutoFonts: true });
|
await applyFontChange();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err && err.name === "NotAllowedError") {
|
if (err && err.name === "NotAllowedError") {
|
||||||
@@ -1892,26 +1974,44 @@ function updateFontSummary() {
|
|||||||
el.availableFontSummary.textContent = total ? `${enabled}/${total}` : "0";
|
el.availableFontSummary.textContent = total ? `${enabled}/${total}` : "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyFontChange(options = {}) {
|
async function applyFontChange() {
|
||||||
updateFontSummary();
|
updateFontSummary();
|
||||||
renderFontList();
|
renderFontList();
|
||||||
if (!state.doc) {
|
if (!state.doc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.editing) {
|
if (canvasEditor.input || canvasEditor.crop) {
|
||||||
|
const openSeq = state.openSeq;
|
||||||
|
const fonts = await fontManager.files(fontManager.records());
|
||||||
|
if (openSeq !== state.openSeq) return;
|
||||||
|
await callWASM("ofdgoConfigure", fonts, state.renderAnnotations);
|
||||||
|
if (openSeq !== state.openSeq) return;
|
||||||
|
state.fontRenderPending = true;
|
||||||
setStatus(pageStatus(state.pageIndex, state.doc.pageCount));
|
setStatus(pageStatus(state.pageIndex, state.doc.pageCount));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await refreshFontPreview(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshFontPreview(fontsChanged) {
|
||||||
|
state.fontRenderPending = false;
|
||||||
await openDocument({
|
await openDocument({
|
||||||
pageIndex: state.pageIndex,
|
pageIndex: state.pageIndex,
|
||||||
fitMode: state.fitMode,
|
fitMode: state.fitMode,
|
||||||
scale: state.scale,
|
scale: state.scale,
|
||||||
skipAutoFonts: options.skipAutoFonts !== false,
|
skipAutoFonts: true,
|
||||||
reuseSession: true,
|
reuseSession: true,
|
||||||
fontsChanged: true,
|
fontsChanged,
|
||||||
|
keepPreview: Boolean(state.editorInfo),
|
||||||
|
previewScroll: { scrollLeft: el.viewerPanel.scrollLeft, scrollTop: el.viewerPanel.scrollTop },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshPendingFonts() {
|
||||||
|
if (!state.fontRenderPending || !state.ready || document.body.hasAttribute("aria-busy") || canvasEditor.input || canvasEditor.crop) return;
|
||||||
|
await refreshFontPreview(false);
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleAnnotations() {
|
async function toggleAnnotations() {
|
||||||
if (document.body.hasAttribute("aria-busy")) {
|
if (document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
@@ -2041,7 +2141,7 @@ function updateFontPermissionHint() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openDocument(options = {}) {
|
async function openDocument(options = {}) {
|
||||||
if (!state.ofdBytes && !state.editing) {
|
if (!state.ofdBytes && !state.editorInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openSeq = options.openSeq || (state.openSeq += 1);
|
const openSeq = options.openSeq || (state.openSeq += 1);
|
||||||
@@ -2190,7 +2290,7 @@ async function loadDocumentDetails(openSeq) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderPage(index, options = {}) {
|
async function renderPage(index, options = {}) {
|
||||||
if (!state.doc || (state.editing && document.body.hasAttribute("aria-busy") && !options.keepBusy)) {
|
if (!state.doc || (state.editorInfo && document.body.hasAttribute("aria-busy") && !options.keepBusy)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const openSeq = options.openSeq || state.openSeq;
|
const openSeq = options.openSeq || state.openSeq;
|
||||||
@@ -2321,7 +2421,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const saving = value === "ofd";
|
const saving = value === "ofd";
|
||||||
if (saving && !state.editing) {
|
if (saving && !state.editorInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const format = saving ? { value: "ofd", label: "OFD", extension: "ofd", mime: "application/ofd" } : exportFormatInfo(value);
|
const format = saving ? { value: "ofd", label: "OFD", extension: "ofd", mime: "application/ofd" } : exportFormatInfo(value);
|
||||||
@@ -2699,7 +2799,7 @@ function mountPageSVG(index, page, openSeq = state.openSeq) {
|
|||||||
svg.classList.add("ofd-svg");
|
svg.classList.add("ofd-svg");
|
||||||
surface.replaceChildren(svg);
|
surface.replaceChildren(svg);
|
||||||
surface.append(createTextLayer(page.text));
|
surface.append(createTextLayer(page.text));
|
||||||
if (state.editing) {
|
if (state.editorInfo) {
|
||||||
canvasEditor.mount(index, page, surface);
|
canvasEditor.mount(index, page, surface);
|
||||||
}
|
}
|
||||||
for (const link of page.links) {
|
for (const link of page.links) {
|
||||||
@@ -2830,6 +2930,11 @@ function copyEditorSelection(event) {
|
|||||||
if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !el.viewerPanel.contains(event.target)
|
if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !el.viewerPanel.contains(event.target)
|
||||||
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false;
|
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false;
|
||||||
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
||||||
|
if (!canEditObject(canvasEditor.selected, "copy")) {
|
||||||
|
event.preventDefault();
|
||||||
|
setStatus("当前对象不可复制");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const clipboard = { token: crypto.randomUUID(), page: state.doc.pages[items[0].index].id, offset: 0 };
|
const clipboard = { token: crypto.randomUUID(), page: state.doc.pages[items[0].index].id, offset: 0 };
|
||||||
event.clipboardData.setData(OBJECT_CLIPBOARD_TYPE, clipboard.token);
|
event.clipboardData.setData(OBJECT_CLIPBOARD_TYPE, clipboard.token);
|
||||||
event.clipboardData.setData("text/plain", items.map(item => item.text || "").filter(Boolean).join("\n"));
|
event.clipboardData.setData("text/plain", items.map(item => item.text || "").filter(Boolean).join("\n"));
|
||||||
@@ -2846,7 +2951,7 @@ function copyEditorSelection(event) {
|
|||||||
async function pasteEditorContent(event) {
|
async function pasteEditorContent(event) {
|
||||||
if (!state.editing || event.target.closest?.("input, textarea, select, [contenteditable]") || formDialogOpen()) return;
|
if (!state.editing || event.target.closest?.("input, textarea, select, [contenteditable]") || formDialogOpen()) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (document.body.hasAttribute("aria-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag) return;
|
if (document.body.hasAttribute("aria-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag || !pageCan("insert")) return;
|
||||||
const token = event.clipboardData.getData(OBJECT_CLIPBOARD_TYPE);
|
const token = event.clipboardData.getData(OBJECT_CLIPBOARD_TYPE);
|
||||||
const index = state.pageIndex, page = state.doc.pages[index];
|
const index = state.pageIndex, page = state.doc.pages[index];
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -2881,7 +2986,7 @@ async function pasteEditorContent(event) {
|
|||||||
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
||||||
if (!value.trim()) return;
|
if (!value.trim()) return;
|
||||||
const font = state.textFonts[Number(el.textFont.value)];
|
const font = state.textFonts[Number(el.textFont.value)];
|
||||||
if (!font) {
|
if (!font || font.disabled) {
|
||||||
setStatus("尚未添加字体");
|
setStatus("尚未添加字体");
|
||||||
el.textFontAdd.focus();
|
el.textFontAdd.focus();
|
||||||
return;
|
return;
|
||||||
@@ -4304,13 +4409,21 @@ function updateControls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateEditorTools() {
|
function updateEditorTools() {
|
||||||
|
el.editButton.disabled = !state.doc || !state.ready || state.exporting;
|
||||||
|
el.editButton.setAttribute("aria-pressed", String(state.editing));
|
||||||
|
el.editButton.title = state.editing ? "返回阅读" : "编辑文档";
|
||||||
|
el.editButton.setAttribute("aria-label", el.editButton.title);
|
||||||
|
el.editNotice.textContent = state.editorInfo?.editWarnings?.join(";") || "";
|
||||||
|
el.editNotice.hidden = !state.editing || !el.editNotice.textContent;
|
||||||
el.editorTools.hidden = !state.editing;
|
el.editorTools.hidden = !state.editing;
|
||||||
el.insertTextButton.disabled = el.insertImageButton.disabled = el.saveButton.disabled = !state.ready || state.exporting;
|
|
||||||
const pagesDisabled = !state.editing || !state.ready || state.exporting;
|
const pagesDisabled = !state.editing || !state.ready || state.exporting;
|
||||||
el.addPageButton.disabled = el.copyPageButton.disabled = el.pageSettingsButton.disabled = pagesDisabled;
|
el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert");
|
||||||
el.deletePageButton.disabled = pagesDisabled || state.doc?.pageCount <= 1;
|
el.saveButton.disabled = el.addPageButton.disabled = pagesDisabled;
|
||||||
el.movePagePrevButton.disabled = pagesDisabled || state.pageIndex === 0;
|
el.copyPageButton.disabled = pagesDisabled || !pageCan("copy");
|
||||||
el.movePageNextButton.disabled = pagesDisabled || state.pageIndex === state.doc?.pageCount - 1;
|
el.pageSettingsButton.disabled = pagesDisabled || !pageCan("resize");
|
||||||
|
el.deletePageButton.disabled = pagesDisabled || !pageCan("delete") || state.doc?.pageCount <= 1;
|
||||||
|
el.movePagePrevButton.disabled = pagesDisabled || !pageCan("move") || state.pageIndex === 0;
|
||||||
|
el.movePageNextButton.disabled = pagesDisabled || !pageCan("move") || state.pageIndex === state.doc?.pageCount - 1;
|
||||||
const enabled = state.editing && state.selectObjects && !state.panMode;
|
const enabled = state.editing && state.selectObjects && !state.panMode;
|
||||||
canvasEditor.setEnabled(enabled);
|
canvasEditor.setEnabled(enabled);
|
||||||
updateObjectControls(canvasEditor.selected);
|
updateObjectControls(canvasEditor.selected);
|
||||||
@@ -4320,28 +4433,34 @@ function updateEditorTools() {
|
|||||||
|
|
||||||
function updateObjectControls(item, reset = false) {
|
function updateObjectControls(item, reset = false) {
|
||||||
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
|
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
|
||||||
el.deleteObjectButton.disabled = el.objectAlign.disabled = disabled;
|
el.deleteObjectButton.disabled = disabled || !canEditObject(item, "delete");
|
||||||
el.copyObjectButton.disabled = disabled;
|
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy");
|
||||||
const cropping = Boolean(canvasEditor.crop);
|
const cropping = Boolean(canvasEditor.crop);
|
||||||
el.objectAlign.disabled = disabled || cropping;
|
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
||||||
el.objectRotate.disabled = el.objectFlip.disabled = disabled || cropping;
|
el.objectRotate.disabled = el.objectFlip.disabled = el.objectAlign.disabled;
|
||||||
el.cropImageButton.disabled = disabled || item.type !== "ImageObject";
|
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "update");
|
||||||
el.imageFit.disabled = el.cropImageButton.disabled || cropping;
|
el.imageFit.disabled = el.cropImageButton.disabled || cropping;
|
||||||
el.cropImageButton.textContent = cropping ? "完成" : "裁剪";
|
el.cropImageButton.textContent = cropping ? "完成" : "裁剪";
|
||||||
el.cropImageButton.setAttribute("aria-label", cropping ? "完成裁剪" : "裁剪图片");
|
el.cropImageButton.setAttribute("aria-label", cropping ? "完成裁剪" : "裁剪图片");
|
||||||
el.cropImageButton.setAttribute("aria-pressed", String(cropping));
|
el.cropImageButton.setAttribute("aria-pressed", String(cropping));
|
||||||
el.resetCropButton.textContent = cropping ? "取消" : "还原";
|
el.resetCropButton.textContent = cropping ? "取消" : "还原";
|
||||||
el.resetCropButton.setAttribute("aria-label", cropping ? "取消裁剪" : "还原图片");
|
el.resetCropButton.setAttribute("aria-label", cropping ? "取消裁剪" : "还原图片");
|
||||||
el.resetCropButton.disabled = disabled || !cropping && (!item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
el.resetCropButton.disabled = el.cropImageButton.disabled || !cropping && (!item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
||||||
el.objectDistribute.disabled = disabled || cropping || !item.items || item.items.length < 3;
|
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
||||||
el.editObjectButton.disabled = disabled || Boolean(item.items) || item.type === "PathObject";
|
el.editObjectButton.disabled = disabled || Boolean(item.items) || item.type === "PathObject"
|
||||||
|
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : "update");
|
||||||
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
||||||
const text = item?.type === "TextObject" ? item : state.textDefaults;
|
const text = item?.type === "TextObject" ? item : state.textDefaults;
|
||||||
el.textFont.disabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items);
|
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items);
|
||||||
el.textSize.disabled = el.textColor.disabled = el.textFont.disabled || Boolean(item?.draft);
|
el.textFont.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont");
|
||||||
|
el.textSize.disabled = textDisabled || Boolean(item?.draft) || text !== state.textDefaults && !canEditObject(item, "reflow");
|
||||||
|
el.textColor.disabled = textDisabled || Boolean(item?.draft) || text !== state.textDefaults && !canEditObject(item, "update");
|
||||||
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
||||||
el.textAlign.disabled = el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled;
|
el.textAlign.disabled = el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled;
|
||||||
el.textAlign.value = text.align || "left";
|
const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown");
|
||||||
|
el.textAlign.value = layoutKnown ? text.align || "left" : "";
|
||||||
|
el.textLineHeight.placeholder = layoutKnown ? "自动" : "原文";
|
||||||
|
el.textSpacing.placeholder = layoutKnown ? "0" : "原文";
|
||||||
el.textWrap.setAttribute("aria-pressed", String(Boolean(text.wrap)));
|
el.textWrap.setAttribute("aria-pressed", String(Boolean(text.wrap)));
|
||||||
if (reset || document.activeElement !== el.textLineHeight) {
|
if (reset || document.activeElement !== el.textLineHeight) {
|
||||||
el.textLineHeight.value = text.paragraphHeight ? String(textPoints(text.paragraphHeight)) : "";
|
el.textLineHeight.value = text.paragraphHeight ? String(textPoints(text.paragraphHeight)) : "";
|
||||||
@@ -4356,9 +4475,9 @@ function updateObjectControls(item, reset = false) {
|
|||||||
}
|
}
|
||||||
el.textSize.title = `字号 ${textPoints(text.size)} pt`;
|
el.textSize.title = `字号 ${textPoints(text.size)} pt`;
|
||||||
if (reset || el.textColor.disabled || document.activeElement !== el.textColor) {
|
if (reset || el.textColor.disabled || document.activeElement !== el.textColor) {
|
||||||
el.textColor.value = text.color;
|
el.textColor.value = text.color || "#000000";
|
||||||
}
|
}
|
||||||
if (item?.type === "PathObject") {
|
if (item?.type === "PathObject" && canEditObject(item, "update")) {
|
||||||
el.shapeFill.checked = item.fill;
|
el.shapeFill.checked = item.fill;
|
||||||
el.shapeStroke.checked = item.stroke;
|
el.shapeStroke.checked = item.stroke;
|
||||||
el.shapeFillColor.value = item.fillColor;
|
el.shapeFillColor.value = item.fillColor;
|
||||||
@@ -4373,9 +4492,10 @@ function updateObjectControls(item, reset = false) {
|
|||||||
const count = members[0]?.count || 0;
|
const count = members[0]?.count || 0;
|
||||||
const canRaise = orders.some((order, i) => order !== count - orders.length + i);
|
const canRaise = orders.some((order, i) => order !== count - orders.length + i);
|
||||||
const canLower = orders.some((order, i) => order !== i);
|
const canLower = orders.some((order, i) => order !== i);
|
||||||
el.objectOrder.disabled = disabled || cropping || count <= orders.length;
|
el.objectOrder.disabled = disabled || cropping || count <= orders.length || !canEditObject(item, "order")
|
||||||
|
|| members.some(member => member.layer !== members[0].layer);
|
||||||
for (const option of el.objectOrder.options) {
|
for (const option of el.objectOrder.options) {
|
||||||
option.disabled = disabled || (["up", "top"].includes(option.value) ? !canRaise : !canLower);
|
option.disabled = el.objectOrder.disabled || (["up", "top"].includes(option.value) ? !canRaise : !canLower);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4435,6 +4555,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
if (state.fontSyncPending) {
|
if (state.fontSyncPending) {
|
||||||
window.setTimeout(syncUserFonts, 0);
|
window.setTimeout(syncUserFonts, 0);
|
||||||
}
|
}
|
||||||
|
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.exportPanel.close();
|
el.exportPanel.close();
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ function paintShape(node, shape, box) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canEditObject(item, capability) {
|
||||||
|
return Boolean(item) && (item.items || [item]).every(object => Boolean(object.capabilities?.[capability]));
|
||||||
|
}
|
||||||
|
|
||||||
export class CanvasEditor {
|
export class CanvasEditor {
|
||||||
constructor(viewer, options) {
|
constructor(viewer, options) {
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
@@ -187,9 +191,10 @@ export class CanvasEditor {
|
|||||||
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
||||||
this.nodes.set(node, item);
|
this.nodes.set(node, item);
|
||||||
node.addEventListener("focus", () => this.select(item));
|
node.addEventListener("focus", () => this.select(item));
|
||||||
const handles = object.shape === "line" ? ["start", "end"]
|
const handles = !canEditObject(object, "transform") ? [] : object.shape === "line" ? ["start", "end"]
|
||||||
: object.shape ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
: object.shape ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
||||||
: object.type === "TextObject" ? ["nw", "ne", "e", "se", "sw", "w"] : ["nw", "ne", "sw", "se"];
|
: object.type === "TextObject" && canEditObject(object, "reflow") && canEditObject(object, "layoutKnown")
|
||||||
|
? ["nw", "ne", "e", "se", "sw", "w"] : ["nw", "ne", "sw", "se"];
|
||||||
for (const corner of handles) {
|
for (const corner of handles) {
|
||||||
const handle = document.createElement("span");
|
const handle = document.createElement("span");
|
||||||
handle.className = `edit-handle edit-${corner}`;
|
handle.className = `edit-handle edit-${corner}`;
|
||||||
@@ -275,7 +280,7 @@ export class CanvasEditor {
|
|||||||
if (items.length > 1) {
|
if (items.length > 1) {
|
||||||
const node = document.createElement("div");
|
const node = document.createElement("div");
|
||||||
node.className = "edit-layer edit-selection";
|
node.className = "edit-layer edit-selection";
|
||||||
for (const corner of ["nw", "ne", "sw", "se"]) {
|
for (const corner of canEditObject(this.selected, "transform") ? ["nw", "ne", "sw", "se"] : []) {
|
||||||
const handle = document.createElement("span");
|
const handle = document.createElement("span");
|
||||||
handle.className = `edit-handle edit-${corner}`;
|
handle.className = `edit-handle edit-${corner}`;
|
||||||
handle.dataset.corner = corner;
|
handle.dataset.corner = corner;
|
||||||
@@ -356,6 +361,7 @@ export class CanvasEditor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = this.selected;
|
const item = this.selected;
|
||||||
|
if (!canEditObject(item, "transform")) return;
|
||||||
this.drag = {
|
this.drag = {
|
||||||
item, pointerID: event.pointerId, corner: !item.items || target === item ? event.target.dataset.corner || "" : "",
|
item, pointerID: event.pointerId, corner: !item.items || target === item ? event.target.dataset.corner || "" : "",
|
||||||
rect: item.surface.getBoundingClientRect(), rotation: this.options.rotation(),
|
rect: item.surface.getBoundingClientRect(), rotation: this.options.rotation(),
|
||||||
@@ -559,7 +565,7 @@ export class CanvasEditor {
|
|||||||
startShape(event) {
|
startShape(event) {
|
||||||
const surface = event.target.closest(".page-surface") || (this.tool !== "text" && this.nearestSurface(event));
|
const surface = event.target.closest(".page-surface") || (this.tool !== "text" && this.nearestSurface(event));
|
||||||
const page = this.pages.get(surface);
|
const page = this.pages.get(surface);
|
||||||
if (!page) {
|
if (!page || !this.options.canInsert(page.index)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -667,11 +673,11 @@ export class CanvasEditor {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
this.drag ? this.cancel() : this.clear();
|
this.drag ? this.cancel() : this.clear();
|
||||||
} else if (event.key === "Delete") {
|
} else if (event.key === "Delete" && canEditObject(this.selected, "delete")) {
|
||||||
this.options.onDelete(this.selected);
|
this.options.onDelete(this.selected);
|
||||||
} else if (edit && !this.drag && !this.selected.items) {
|
} else if (edit && !this.drag && !this.selected.items) {
|
||||||
this.options.onEdit(this.selected);
|
this.options.onEdit(this.selected);
|
||||||
} else if (!this.drag && directions[event.key]) {
|
} else if (!this.drag && directions[event.key] && canEditObject(this.selected, "transform")) {
|
||||||
const [x, y] = directions[event.key];
|
const [x, y] = directions[event.key];
|
||||||
const [dx, dy] = [[x, y], [y, -x], [-x, -y], [-y, x]][this.options.rotation() / 90];
|
const [dx, dy] = [[x, y], [y, -x], [-x, -y], [-y, x]][this.options.rotation() / 90];
|
||||||
const step = event.shiftKey ? 10 : 1;
|
const step = event.shiftKey ? 10 : 1;
|
||||||
@@ -714,6 +720,7 @@ export class CanvasEditor {
|
|||||||
if (!this.options.fontControls?.includes(event.relatedTarget)) this.commitText();
|
if (!this.options.fontControls?.includes(event.relatedTarget)) this.commitText();
|
||||||
});
|
});
|
||||||
input.addEventListener("keydown", (event) => {
|
input.addEventListener("keydown", (event) => {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "s") return;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (event.isComposing || input.readOnly) {
|
if (event.isComposing || input.readOnly) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -668,7 +668,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
}
|
}
|
||||||
if box.W > 0 && box.H > 0 {
|
if box.W > 0 && box.H > 0 {
|
||||||
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects), "layer": layer.ID,
|
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects), "layer": layer.ID,
|
||||||
"capabilities": map[string]any{"update": capability.Update, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason}}
|
"capabilities": map[string]any{"update": capability.Update, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason}}
|
||||||
if object.Type == "ImageObject" {
|
if object.Type == "ImageObject" {
|
||||||
full, err := object.ImageObject.ImageBounds()
|
full, err := object.ImageObject.ImageBounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1027,7 +1027,7 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
|||||||
return editorSummary(), nil
|
return editorSummary(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateText 修改文字,null内容保留排版,null字体数据和颜色沿用对象属性
|
// updateText 修改文字,null内容保留定位且允许替换字体,null字体数据和颜色沿用对象属性
|
||||||
// 入参: args 页码、对象标识、内容、字体数据、字号和颜色
|
// 入参: args 页码、对象标识、内容、字体数据、字号和颜色
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func updateText(args []js.Value) (any, error) {
|
func updateText(args []js.Value) (any, error) {
|
||||||
@@ -1042,7 +1042,6 @@ func updateText(args []js.Value) (any, error) {
|
|||||||
if object.Type != "TextObject" {
|
if object.Type != "TextObject" {
|
||||||
return nil, fmt.Errorf("object %q is not text", id)
|
return nil, fmt.Errorf("object %q is not text", id)
|
||||||
}
|
}
|
||||||
if !args[2].IsNull() {
|
|
||||||
if !args[3].IsNull() {
|
if !args[3].IsNull() {
|
||||||
data, err := bytesFromJS(args[3])
|
data, err := bytesFromJS(args[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1053,6 +1052,7 @@ func updateText(args []js.Value) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !args[2].IsNull() {
|
||||||
object.TextObject.Size = args[4].Float()
|
object.TextObject.Size = args[4].Float()
|
||||||
_, layout := object.TextObject.TextLayout()
|
_, layout := object.TextObject.TextLayout()
|
||||||
if err := currentEditor.LayoutText(&object.TextObject, args[2].String(), layout); err != nil {
|
if err := currentEditor.LayoutText(&object.TextObject, args[2].String(), layout); err != nil {
|
||||||
@@ -1463,7 +1463,7 @@ func insertImage(args []js.Value) (any, error) {
|
|||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveDocument 分块写出新建文档
|
// saveDocument 分块写出当前编辑文档
|
||||||
// 入参: args 数据写出回调
|
// 入参: args 数据写出回调
|
||||||
// 返回: any 保存结果, error 错误信息
|
// 返回: any 保存结果, error 错误信息
|
||||||
func saveDocument(args []js.Value) (any, error) {
|
func saveDocument(args []js.Value) (any, error) {
|
||||||
|
|||||||
@@ -56,10 +56,11 @@ type editorSourcePage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因。
|
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因。
|
||||||
// Update表示完整替换内容,Transform表示保留文字定位的几何变换,Order仅限当前图层。
|
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限当前图层。
|
||||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复。
|
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复。
|
||||||
type ObjectCapabilities struct {
|
type ObjectCapabilities struct {
|
||||||
Update bool
|
Update bool
|
||||||
|
ReplaceFont bool
|
||||||
Reflow bool
|
Reflow bool
|
||||||
LayoutKnown bool
|
LayoutKnown bool
|
||||||
Transform bool
|
Transform bool
|
||||||
@@ -315,7 +316,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
return ObjectCapabilities{}, err
|
return ObjectCapabilities{}, err
|
||||||
}
|
}
|
||||||
object := layer.Objects[index]
|
object := layer.Objects[index]
|
||||||
all := ObjectCapabilities{Update: true, Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true}
|
all := ObjectCapabilities{Update: true, ReplaceFont: object.Type == "TextObject", Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true}
|
||||||
if !e.originalPage(page) {
|
if !e.originalPage(page) {
|
||||||
return all, nil
|
return all, nil
|
||||||
}
|
}
|
||||||
@@ -350,6 +351,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, err.Error()
|
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, err.Error()
|
||||||
}
|
}
|
||||||
if !e.sourceRGB() {
|
if !e.sourceRGB() {
|
||||||
|
all.ReplaceFont = false
|
||||||
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, "document uses a non-RGB default color space"
|
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, "document uses a non-RGB default color space"
|
||||||
}
|
}
|
||||||
if object.Type == "TextObject" {
|
if object.Type == "TextObject" {
|
||||||
|
|||||||
Reference in new issue
Block a user